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
« 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
5from django.conf import settings
6from django.core.files.storage import DefaultStorage
7from django.db.models.fields.files import FieldFile, ImageFieldFile
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
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}")
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.
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.
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)
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.
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
55 url = storage.url(file_name, attachment, expire_seconds)
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}"
61 return url
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
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 )
82 return get_media_url(file, absolute_url=absolute_url)
85def fetch_thumbnails(images: list, sizes=None):
86 """Prefetches thumbnails from the database or redis efficiently.
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))
95 if not images:
96 return
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)
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)
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 )