|
8 | 8 | from datetime import datetime, timedelta |
9 | 9 | from io import BytesIO, StringIO |
10 | 10 | from pathlib import Path |
11 | | -from urllib.parse import parse_qs, urlencode, urlparse |
12 | 11 |
|
13 | 12 | import pandas as pd |
14 | 13 | from django.apps import apps |
15 | 14 | from django.db.migrations.operations.base import Operation |
| 15 | +from django.db.models import Max |
16 | 16 | from django.forms.models import modelform_factory |
17 | | -from django.utils.cache import set_response_etag |
18 | | -from django.utils.timezone import now |
19 | 17 | from openpyxl.utils import get_column_letter |
20 | 18 | from treebeard.mp_tree import MP_Node |
21 | 19 |
|
@@ -355,75 +353,27 @@ def get_all_subclasses(cls): |
355 | 353 | normalize = str.maketrans(normal_map) |
356 | 354 |
|
357 | 355 |
|
358 | | -class Timer: |
| 356 | +def make_condition_funcs(model): |
359 | 357 | """ |
360 | | - See http://www.machinalis.com/blog/how-to-unit-test-python/ |
| 358 | + Create etag and last_modfied functions for use with Django's condition decorator. |
| 359 | + All models inheriting from common.Model have a 'modified' timestamp field from TimeStampedModel. |
361 | 360 | """ |
362 | 361 |
|
363 | | - def start(self): |
364 | | - self._start = now() |
| 362 | + def get_last_modified(request, *args, **kwargs): |
| 363 | + # Return the most recent modification timestamp for the model. |
| 364 | + result = model.objects.aggregate(last_modified=Max("modified")) |
| 365 | + return result["last_modified"] |
365 | 366 |
|
366 | | - def stop(self): |
367 | | - self._stop = now() |
| 367 | + def get_etag(request, *args, **kwargs): |
| 368 | + """ |
| 369 | + Return a weak ETag based on the most recent modification timestamp. |
368 | 370 |
|
369 | | - def elapsed(self): |
370 | | - try: |
371 | | - return self._stop - self._start |
372 | | - except AttributeError: |
373 | | - return now() - self._start |
374 | | - |
375 | | - def assertHasDate(self, date): |
376 | | - assert self._start <= date <= self._stop, "%s was not during the timer" % date |
377 | | - |
378 | | - def assertNotHasDate(self, date): |
379 | | - assert not (self._start <= date <= self._stop), "%s was during the timer" % date |
380 | | - |
381 | | - |
382 | | -@contextlib.contextmanager |
383 | | -def timekeeper(): |
384 | | - t = Timer() |
385 | | - t.start() |
386 | | - try: |
387 | | - yield t |
388 | | - finally: |
389 | | - t.stop() |
390 | | - |
391 | | - |
392 | | -def get_etag_for_cachedrequest(request, *args, **kwargs): |
393 | | - """ |
394 | | - Generate an ETag for the request based on path and query parameters. |
395 | | - If the request includes a _refresh parameter, returns None to force a cache miss. |
396 | | - """ |
397 | | - u = urlparse(request.get_full_path()) |
398 | | - query = parse_qs(u.query, keep_blank_values=True) |
399 | | - query.pop("_store_result", None) |
400 | | - |
401 | | - # Check for refresh parameter - if present, return None to force cache miss |
402 | | - _refresh = query.pop("_refresh", None) |
403 | | - if _refresh: |
404 | | - return None |
405 | | - |
406 | | - u = u._replace(query=urlencode(sorted(query.items()), True)) |
407 | | - path = u.geturl() |
408 | | - |
409 | | - if "format" in kwargs: |
410 | | - path += f"|format={kwargs['format']}" |
411 | | - |
412 | | - etag_hash = hashlib.md5(path.encode()).hexdigest() |
413 | | - |
414 | | - return f'"{etag_hash}"' |
415 | | - |
416 | | - |
417 | | -def set_etag_for_response(response): |
418 | | - """ |
419 | | - Add the etag header to a response. |
420 | | -
|
421 | | - This is a thin wrapper around `django.utils.cache.set_response_etag`, to report |
422 | | - the time taken to calculate the header. |
423 | | - """ |
424 | | - with timekeeper() as t: |
425 | | - response = set_response_etag(response) |
426 | | - if response.has_header("ETag"): |
427 | | - logger.info("Created etag header in %s seconds" % round(t.elapsed().total_seconds(), 2)) |
| 371 | + Uses weak ETag (W/ prefix) because we're comparing semantic equivalence of the data, |
| 372 | + not byte-for-byte equality. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag |
| 373 | + """ |
| 374 | + last_modified = get_last_modified(request, *args, **kwargs) |
| 375 | + if last_modified is None: |
| 376 | + return None |
| 377 | + return f'W/"{hashlib.md5(last_modified.isoformat().encode()).hexdigest()}"' |
428 | 378 |
|
429 | | - return response |
| 379 | + return get_etag, get_last_modified |
0 commit comments