Coverage for documents/conditionals.py: 51%
77 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 09:07 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 09:07 +0000
1from datetime import UTC
2from datetime import datetime
4from django.conf import settings
5from django.core.cache import cache
6from rest_framework.request import Request
8from documents.caching import CACHE_5_MINUTES
9from documents.caching import CACHE_50_MINUTES
10from documents.caching import CLASSIFIER_HASH_KEY
11from documents.caching import CLASSIFIER_MODIFIED_KEY
12from documents.caching import CLASSIFIER_VERSION_KEY
13from documents.caching import get_thumbnail_modified_key
14from documents.classifier import DocumentClassifier
15from documents.models import Document
16from documents.versioning import resolve_effective_document_by_pk
19def suggestions_etag(request, pk: int) -> str | None:
20 """
21 Returns an optional string for the ETag, allowing browser caching of
22 suggestions if the classifier has not been changed and the suggested dates
23 setting is also unchanged
25 """
26 # If no model file, no etag at all
27 if not settings.MODEL_FILE.exists(): 27 ↛ 30line 27 didn't jump to line 30 because the condition on line 27 was always true
28 return None
29 # Check cache information
30 cache_hits = cache.get_many(
31 [CLASSIFIER_VERSION_KEY, CLASSIFIER_HASH_KEY],
32 )
33 # If the version differs somehow, no etag
34 if (
35 CLASSIFIER_VERSION_KEY in cache_hits
36 and cache_hits[CLASSIFIER_VERSION_KEY] != DocumentClassifier.FORMAT_VERSION
37 ):
38 return None
39 elif CLASSIFIER_HASH_KEY in cache_hits:
40 # Refresh the cache and return the hash digest and the dates setting
41 cache.touch(CLASSIFIER_HASH_KEY, CACHE_5_MINUTES)
42 return f"{cache_hits[CLASSIFIER_HASH_KEY]}:{settings.NUMBER_OF_SUGGESTED_DATES}"
43 return None
46def suggestions_last_modified(request, pk: int) -> datetime | None:
47 """
48 Returns the datetime of classifier last modification. This is slightly off,
49 as there is not way to track the suggested date setting modification, but it seems
50 unlikely that changes too often
51 """
52 # No file, no last modified
53 if not settings.MODEL_FILE.exists(): 53 ↛ 55line 53 didn't jump to line 55 because the condition on line 53 was always true
54 return None
55 cache_hits = cache.get_many(
56 [CLASSIFIER_VERSION_KEY, CLASSIFIER_MODIFIED_KEY],
57 )
58 # If the version differs somehow, no last modified
59 if (
60 CLASSIFIER_VERSION_KEY in cache_hits
61 and cache_hits[CLASSIFIER_VERSION_KEY] != DocumentClassifier.FORMAT_VERSION
62 ):
63 return None
64 elif CLASSIFIER_MODIFIED_KEY in cache_hits:
65 # Refresh the cache and return the last modified
66 cache.touch(CLASSIFIER_MODIFIED_KEY, CACHE_5_MINUTES)
67 return cache_hits[CLASSIFIER_MODIFIED_KEY]
68 return None
71def metadata_etag(request, pk: int) -> str | None:
72 """
73 Metadata responses include metadata as well as document fields, so include
74 the modification time with the checksum so metadata-only changes invalidate cache.
75 """
76 doc = resolve_effective_document_by_pk(pk, request).document
77 if doc is None: 77 ↛ 79line 77 didn't jump to line 79 because the condition on line 77 was always true
78 return None
79 return f"{doc.checksum}:{doc.modified.isoformat()}"
82def metadata_last_modified(request, pk: int) -> datetime | None:
83 """
84 Metadata is extracted from the original file, so use its modified. Strictly speaking, this is
85 not the modification of the original file, but of the database object, but might as well
86 error on the side of more cautious
87 """
88 doc = resolve_effective_document_by_pk(pk, request).document
89 if doc is None: 89 ↛ 91line 89 didn't jump to line 91 because the condition on line 89 was always true
90 return None
91 return doc.modified
94def preview_etag(request, pk: int) -> str | None:
95 """
96 ETag for the document preview, using the original or archive checksum, depending on the request
97 """
98 doc = resolve_effective_document_by_pk(pk, request).document
99 if doc is None: 99 ↛ 101line 99 didn't jump to line 101 because the condition on line 99 was always true
100 return None
101 use_original = (
102 hasattr(request, "query_params")
103 and "original" in request.query_params
104 and request.query_params["original"] == "true"
105 )
106 return doc.checksum if use_original else doc.archive_checksum
109def preview_last_modified(request, pk: int) -> datetime | None:
110 """
111 Uses the documents modified time to set the Last-Modified header. Not strictly
112 speaking correct, but close enough and quick
113 """
114 doc = resolve_effective_document_by_pk(pk, request).document
115 if doc is None: 115 ↛ 117line 115 didn't jump to line 117 because the condition on line 115 was always true
116 return None
117 return doc.modified
120def thumbnail_etag(request: Request, pk: int) -> str | None:
121 """
122 Thumbnails are version-dependent, so use the effective document checksum as
123 the ETag to invalidate cache when the latest version changes.
124 """
125 doc = resolve_effective_document_by_pk(pk, request).document
126 if doc is None: 126 ↛ 128line 126 didn't jump to line 128 because the condition on line 126 was always true
127 return None
128 return doc.checksum
131def thumbnail_last_modified(request: Request, pk: int) -> datetime | None:
132 """
133 Returns the filesystem last modified either from cache or from filesystem.
134 Cache should be (slightly?) faster than filesystem
135 """
136 try:
137 doc = resolve_effective_document_by_pk(pk, request).document
138 if doc is None: 138 ↛ 140line 138 didn't jump to line 140 because the condition on line 138 was always true
139 return None
140 if not doc.thumbnail_path.exists():
141 return None
142 # Use the effective document id for cache key
143 doc_key = get_thumbnail_modified_key(doc.id)
145 cache_hit = cache.get(doc_key)
146 if cache_hit is not None:
147 cache.touch(doc_key, CACHE_50_MINUTES)
148 return cache_hit
150 # No cache, get the timestamp and cache the datetime
151 last_modified = datetime.fromtimestamp(
152 doc.thumbnail_path.stat().st_mtime,
153 tz=UTC,
154 )
155 cache.set(doc_key, last_modified, CACHE_50_MINUTES)
156 return last_modified
157 except (Document.DoesNotExist, OSError): # pragma: no cover
158 return None