|
8 | 8 |
|
9 | 9 | import django.forms |
10 | 10 | import django.forms.formsets |
| 11 | +import requests |
11 | 12 | import reversion |
12 | 13 | from django.apps import apps |
13 | 14 | from django.conf import settings |
14 | 15 | from django.contrib import messages |
15 | 16 | from django.contrib.admin.views.decorators import staff_member_required |
16 | 17 | from django.contrib.auth.decorators import login_required |
| 18 | +from django.core.cache import cache |
17 | 19 | from django.core.mail import mail_admins, mail_managers |
18 | 20 | from django.db import transaction |
19 | 21 | from django.db.models import Case, Count, F, Func, Prefetch, Q, Value, When |
|
56 | 58 | from ..models import BleachMeasurement, Excerpt, Organism, Protein, Spectrum, State |
57 | 59 |
|
58 | 60 | if TYPE_CHECKING: |
59 | | - import maxminddb |
60 | | - |
61 | 61 | from proteins.forms.forms import BaseStateFormSet |
62 | 62 |
|
63 | 63 | logger = logging.getLogger(__name__) |
@@ -122,82 +122,43 @@ def __init__(self, response): |
122 | 122 | self.response = response |
123 | 123 |
|
124 | 124 |
|
125 | | -def maxmind_db() -> str: |
126 | | - """Create and return a temporary file containing the MaxMind database. |
| 125 | +def _mask_ip_for_caching(ip: str) -> str: |
| 126 | + """Mask IP to /16 (IPv4) or /48 (IPv6) for caching.""" |
| 127 | + if not ip or not isinstance(ip, str): |
| 128 | + return "" |
| 129 | + if ":" in ip: # IPv6 |
| 130 | + # Mask to /48 (first 3 groups) |
| 131 | + parts = ip.split(":") |
| 132 | + return ":".join(parts[:3]) + "::" if len(parts) >= 3 else ip |
| 133 | + else: # IPv4 |
| 134 | + parts = ip.split(".") |
| 135 | + return f"{parts[0]}.{parts[1]}.0.0" if len(parts) == 4 else "" |
127 | 136 |
|
128 | | - Uses Django cache to store the database path across workers. |
129 | | - The file persists on disk but the path is cached for 24 hours. |
130 | | - """ |
131 | | - import io |
132 | | - import os |
133 | | - import tarfile |
134 | | - import tempfile |
135 | | - |
136 | | - import requests |
137 | | - from django.conf import settings |
138 | | - from django.core.cache import cache |
139 | | - |
140 | | - # Try to get cached path first |
141 | | - cache_key = "maxmind_db_path" |
142 | | - cached_path = cache.get(cache_key) |
143 | | - if cached_path and os.path.exists(cached_path): |
144 | | - return cached_path |
145 | | - |
146 | | - # Download and cache the database |
147 | | - url = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key={}&suffix=tar.gz" |
148 | | - url = url.format(settings.MAXMIND_API_KEY) |
149 | | - response = requests.get(url) |
150 | | - response.raise_for_status() |
151 | | - with tarfile.open(fileobj=io.BytesIO(response.content), mode="r:gz") as tar: |
152 | | - for member in tar.getmembers(): |
153 | | - if member.name.endswith(".mmdb"): |
154 | | - mmdb_file = tar.extractfile(member) |
155 | | - if mmdb_file is not None: |
156 | | - tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mmdb") |
157 | | - tmp.write(mmdb_file.read()) |
158 | | - tmp.close() |
159 | | - # Cache the path for 24 hours |
160 | | - cache.set(cache_key, tmp.name, 60 * 60 * 24) |
161 | | - return tmp.name |
162 | | - return "" |
163 | | - |
164 | | - |
165 | | -def maxmind_reader() -> "maxminddb.Reader | None": |
166 | | - """Get MaxMind database reader. |
167 | | -
|
168 | | - Uses Django cache to minimize memory usage. The reader is cached |
169 | | - for 1 hour to balance memory usage and performance. |
170 | | - """ |
171 | | - from django.core.cache import cache |
172 | | - from maxminddb import open_database |
173 | 137 |
|
174 | | - cache_key = "maxmind_reader" |
175 | | - reader = cache.get(cache_key) |
176 | | - if reader is not None: |
177 | | - return reader |
| 138 | +def get_country_code(request) -> str: |
| 139 | + """Get country code from IP address using cached API lookup.""" |
| 140 | + |
| 141 | + x_forwarded_for = request.headers.get("x-forwarded-for") |
| 142 | + if x_forwarded_for: |
| 143 | + ip = x_forwarded_for.split(",")[0].strip() |
| 144 | + else: |
| 145 | + ip = request.META.get("REMOTE_ADDR") |
178 | 146 |
|
179 | 147 | try: |
180 | | - if db := maxmind_db(): |
181 | | - reader = open_database(db) |
182 | | - # Cache reader for 1 hour |
183 | | - cache.set(cache_key, reader, 60 * 60) |
184 | | - return reader |
| 148 | + if not (masked_ip := _mask_ip_for_caching(ip)): |
| 149 | + return "" |
| 150 | + |
| 151 | + cache_key = f"country_code:{masked_ip}" |
| 152 | + country_code = cache.get(cache_key) |
| 153 | + if country_code is not None: |
| 154 | + return country_code |
| 155 | + |
| 156 | + response = requests.get(f"https://ipapi.co/{ip}/country/", timeout=2) |
| 157 | + country_code = response.text.strip() |
| 158 | + cache.set(cache_key, country_code, 14 * 60 * 60 * 24) # cache for 14 days |
| 159 | + return country_code |
185 | 160 | except Exception: |
186 | | - pass |
187 | | - return None |
188 | | - |
189 | | - |
190 | | -def get_country_code(request) -> str: |
191 | | - # Definitely should be used inside a try/exc block |
192 | | - if reader := maxmind_reader(): |
193 | | - x_forwarded_for = request.headers.get("x-forwarded-for") |
194 | | - if x_forwarded_for: |
195 | | - ip = x_forwarded_for.split(",")[0] |
196 | | - else: |
197 | | - ip = request.META.get("REMOTE_ADDR") |
198 | | - if response := reader.get(ip): |
199 | | - return str(response["country"]["iso_code"]) # pyright: ignore[reportIndexIssue] |
200 | | - return "" |
| 161 | + return "" |
201 | 162 |
|
202 | 163 |
|
203 | 164 | class ProteinDetailView(DetailView): |
|
0 commit comments