Coverage for documents/conditionals.py: 51%

77 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 09:07 +0000

1from datetime import UTC 

2from datetime import datetime 

3 

4from django.conf import settings 

5from django.core.cache import cache 

6from rest_framework.request import Request 

7 

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 

17 

18 

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 

24 

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 

44 

45 

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 

69 

70 

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()}" 

80 

81 

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 

92 

93 

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 

107 

108 

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 

118 

119 

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 

129 

130 

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) 

144 

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 

149 

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