Coverage for website/utils/media/services.py: 83.56%

51 statements  

« prev     ^ index     » next       coverage.py v7.6.7, created at 2025-08-14 10:31 +0000

1import os 

2from functools import partial 

3from secrets import token_hex 

4 

5from django.conf import settings 

6from django.core.files.storage import DefaultStorage 

7from django.db.models.fields.files import FieldFile, ImageFieldFile 

8 

9from thumbnails.backends.metadata import ImageMeta 

10from thumbnails.fields import fetch_thumbnails as fetch_thumbnails_redis 

11from thumbnails.files import ThumbnailedImageFile 

12from thumbnails.images import Thumbnail 

13from thumbnails.models import ThumbnailMeta 

14 

15 

16def _generic_upload_to(instance, filename, prefix: str, token_bytes: int): 

17 ext = os.path.splitext(filename)[1] 

18 return os.path.join(prefix, f"{token_hex(token_bytes)}{ext}") 

19 

20 

21def get_upload_to_function(prefix: str, token_bytes: int = 8): 

22 """Return a partial function that can be used as the upload_to argument of a FileField. 

23 

24 This is useful to avoid having numerous functions that all do the same thing, with 

25 different prefixes. Using a partial function makes it serializable for migrations. 

26 See: https://docs.djangoproject.com/en/4.2/topics/migrations/#migration-serializing. 

27 

28 The resulting function returns paths like `<prefix>/<random hex string>.<ext>`. 

29 """ 

30 return partial(_generic_upload_to, prefix=prefix, token_bytes=token_bytes) 

31 

32 

33def get_media_url( 

34 file, 

35 attachment=False, 

36 absolute_url: bool = False, 

37 expire_seconds: int | None = None, 

38): 

39 """Get the url of the provided media file to serve in a browser. 

40 

41 If the file is private a signature will be added. 

42 Do NOT use this with user input 

43 :param file: The file field or path. 

44 :param attachment: Filename to use for the attachment or False to not download as attachment. 

45 :param absolute_url: True if we want the full url including the scheme and domain. 

46 :param expire_seconds: The number of seconds the url should be valid for if on S3. 

47 :return: The url of the media. 

48 """ 

49 storage = DefaultStorage() 

50 file_name = file 

51 if isinstance(file, ImageFieldFile | FieldFile | Thumbnail): 51 ↛ 55line 51 didn't jump to line 55 because the condition on line 51 was always true

52 storage = file.storage 

53 file_name = file.name 

54 

55 url = storage.url(file_name, attachment, expire_seconds) 

56 

57 # If the url is not absolute, but we want an absolute url, add the base url. 

58 if absolute_url and not url.startswith(("http://", "https://")): 

59 url = f"{settings.BASE_URL}{url}" 

60 

61 return url 

62 

63 

64def get_thumbnail_url( 

65 file, 

66 size: str, 

67 absolute_url: bool = False, 

68 expire_seconds: int | None = None, 

69): 

70 name = file 

71 if isinstance(file, ImageFieldFile | FieldFile): 71 ↛ 74line 71 didn't jump to line 74 because the condition on line 71 was always true

72 name = file.name 

73 

74 if isinstance(file, ThumbnailedImageFile): 74 ↛ 82line 74 didn't jump to line 82 because the condition on line 74 was always true

75 if not name.endswith(".svg"): 75 ↛ 82line 75 didn't jump to line 82 because the condition on line 75 was always true

76 if size in settings.THUMBNAIL_SIZES: 76 ↛ 82line 76 didn't jump to line 82 because the condition on line 76 was always true

77 return get_media_url( 

78 getattr(file.thumbnails, size), 

79 absolute_url=absolute_url, 

80 ) 

81 

82 return get_media_url(file, absolute_url=absolute_url) 

83 

84 

85def fetch_thumbnails(images: list, sizes=None): 

86 """Prefetches thumbnails from the database or redis efficiently. 

87 

88 :param images: A list of images to prefetch thumbnails for. 

89 :param sizes: A list of sizes to prefetch. If None, all sizes will be prefetched. 

90 :return: None 

91 """ 

92 # Filter out empty ImageFieldFiles. 

93 images = list(filter(bool, images)) 

94 

95 if not images: 

96 return 

97 

98 if ( 98 ↛ 102line 98 didn't jump to line 102

99 settings.THUMBNAILS["METADATA"]["BACKEND"] 

100 != "thumbnails.backends.metadata.DatabaseBackend" 

101 ): 

102 return fetch_thumbnails_redis(images, sizes) 

103 

104 image_dict = {image.thumbnails.source_image.name: image for image in images} 

105 thumbnails = ThumbnailMeta.objects.select_related("source").filter( 

106 source__name__in=image_dict.keys() 

107 ) 

108 if sizes: 108 ↛ 109line 108 didn't jump to line 109 because the condition on line 108 was never true

109 thumbnails.filter(size__in=sizes) 

110 

111 for source_name, thumb_name, thumb_size in thumbnails.values_list( 

112 "source__name", "name", "size" 

113 ): 

114 thumbnails = image_dict[source_name].thumbnails 

115 if not thumbnails._thumbnails: 

116 thumbnails._thumbnails = {} 

117 image_meta = ImageMeta(source_name, thumb_name, thumb_size) 

118 thumbnails._thumbnails[thumb_size] = Thumbnail( 

119 image_meta, storage=thumbnails.storage 

120 )