diff --git a/pictures/contrib/rest_framework.py b/pictures/contrib/rest_framework.py index 0db6a29..5d0a43d 100644 --- a/pictures/contrib/rest_framework.py +++ b/pictures/contrib/rest_framework.py @@ -34,28 +34,17 @@ def to_representation(self, obj: PictureFieldFile): "height": obj.height, } - payload = { - **base_payload, - "ratios": { - ratio: { - "sources": { - f"image/{file_type.lower()}": sizes - for file_type, sizes in sources.items() - if file_type in self.file_types or not self.file_types - }, - } - for ratio, sources in obj.aspect_ratios.items() - if ratio in self.aspect_ratios or not self.aspect_ratios - }, - } - # if the request has query parameters, filter the payload try: query_params: QueryDict = self.context["request"].GET except KeyError: - pass + ratios = self.aspect_ratios + container = get_settings().CONTAINER_WIDTH + breakpoints = {} else: - ratio = query_params.get(f"{self.field_name}_ratio") + ratios = ( + query_params.getlist(f"{self.field_name}_ratio") + ) or self.aspect_ratios container = query_params.get(f"{self.field_name}_container") try: container = int(container) @@ -68,16 +57,25 @@ def to_representation(self, obj: PictureFieldFile): for bp in get_settings().BREAKPOINTS if f"{self.field_name}_{bp}" in query_params } - if ratio is not None: - try: - payload["ratios"] = {ratio: payload["ratios"][ratio]} - except KeyError as e: - raise ValueError( - f"Invalid ratio: {ratio}. Choices are: {', '.join(filter(None, obj.aspect_ratios.keys()))}" - ) from e - else: - payload["ratios"][ratio]["media"] = utils.sizes( - container_width=container, **breakpoints - ) + if set(ratios) - set(self.aspect_ratios or obj.aspect_ratios.keys()): + raise ValueError( + f"Invalid ratios: {', '.join(ratios)}. Choices are: {', '.join(filter(None, obj.aspect_ratios.keys()))}" + ) + + payload = { + **base_payload, + "ratios": { + ratio: { + "sources": { + f"image/{file_type.lower()}": sizes + for file_type, sizes in sources.items() + if file_type in self.file_types or not self.file_types + }, + "media": utils.sizes(container_width=container, **breakpoints), + } + for ratio, sources in obj.aspect_ratios.items() + if ratio in ratios or not ratios + }, + } return json.loads(json.dumps(payload, default=default)) diff --git a/tests/contrib/test_rest_framework.py b/tests/contrib/test_rest_framework.py index 8bd6d0d..2d5d865 100644 --- a/tests/contrib/test_rest_framework.py +++ b/tests/contrib/test_rest_framework.py @@ -78,7 +78,8 @@ def test_to_representation(self, image_upload_file, settings): "600": "/media/testapp/profile/image/600w.webp", "700": "/media/testapp/profile/image/700w.webp", } - } + }, + "media": "(min-width: 0px) and (max-width: 1199px) 100vw, 1200px", }, "1/1": { "sources": { @@ -92,7 +93,8 @@ def test_to_representation(self, image_upload_file, settings): "600": "/media/testapp/profile/image/1/600w.webp", "700": "/media/testapp/profile/image/1/700w.webp", } - } + }, + "media": "(min-width: 0px) and (max-width: 1199px) 100vw, 1200px", }, "3/2": { "sources": { @@ -106,7 +108,8 @@ def test_to_representation(self, image_upload_file, settings): "600": "/media/testapp/profile/image/3_2/600w.webp", "700": "/media/testapp/profile/image/3_2/700w.webp", } - } + }, + "media": "(min-width: 0px) and (max-width: 1199px) 100vw, 1200px", }, "16/9": { "sources": { @@ -120,7 +123,8 @@ def test_to_representation(self, image_upload_file, settings): "600": "/media/testapp/profile/image/16_9/600w.webp", "700": "/media/testapp/profile/image/16_9/700w.webp", } - } + }, + "media": "(min-width: 0px) and (max-width: 1199px) 100vw, 1200px", }, }, } @@ -179,7 +183,7 @@ def test_to_representation__raise_value_error( with pytest.raises(ValueError) as e: serializer.data["image"] - assert str(e.value) == "Invalid ratio: 21/11. Choices are: 1/1, 3/2, 16/9" + assert str(e.value) == "Invalid ratios: 21/11. Choices are: 1/1, 3/2, 16/9" @pytest.mark.django_db def test_to_representation__blank(self, rf, image_upload_file, settings): @@ -195,6 +199,86 @@ def test_to_representation__blank(self, rf, image_upload_file, settings): assert serializer.data["image"] is None + @pytest.mark.django_db + def test_to_representation__no_get_params(self, rf, image_upload_file, settings): + settings.PICTURES["USE_PLACEHOLDERS"] = False + + profile = models.Profile.objects.create(picture=image_upload_file) + request = rf.get("/") + request.GET._mutable = True + request.GET["foo"] = "bar" + serializer = ProfileSerializer(profile, context={"request": request}) + assert serializer.data["image_mobile"] == { + "url": "/media/testapp/profile/image.png", + "width": 800, + "height": 800, + "ratios": { + "3/2": { + "sources": { + "image/webp": { + "800": "/media/testapp/profile/image/3_2/800w.webp", + "100": "/media/testapp/profile/image/3_2/100w.webp", + "200": "/media/testapp/profile/image/3_2/200w.webp", + "300": "/media/testapp/profile/image/3_2/300w.webp", + "400": "/media/testapp/profile/image/3_2/400w.webp", + "500": "/media/testapp/profile/image/3_2/500w.webp", + "600": "/media/testapp/profile/image/3_2/600w.webp", + "700": "/media/testapp/profile/image/3_2/700w.webp", + } + }, + "media": "(min-width: 0px) and (max-width: 1199px) 100vw, 1200px", + } + }, + } + + @pytest.mark.django_db + def test_to_representation__multiple_ratios(self, rf, image_upload_file, settings): + settings.PICTURES["USE_PLACEHOLDERS"] = False + + profile = models.Profile.objects.create(picture=image_upload_file) + request = rf.get("/") + request.GET._mutable = True + request.GET.setlist("image_ratio", ["3/2", "16/9"]) + serializer = ProfileSerializer(profile, context={"request": request}) + print(serializer.data["image"]) + assert serializer.data["image"] == { + "url": "/media/testapp/profile/image.png", + "width": 800, + "height": 800, + "ratios": { + "3/2": { + "sources": { + "image/webp": { + "800": "/media/testapp/profile/image/3_2/800w.webp", + "100": "/media/testapp/profile/image/3_2/100w.webp", + "200": "/media/testapp/profile/image/3_2/200w.webp", + "300": "/media/testapp/profile/image/3_2/300w.webp", + "400": "/media/testapp/profile/image/3_2/400w.webp", + "500": "/media/testapp/profile/image/3_2/500w.webp", + "600": "/media/testapp/profile/image/3_2/600w.webp", + "700": "/media/testapp/profile/image/3_2/700w.webp", + } + }, + "media": "(min-width: 0px) and (max-width: 1199px) 100vw, 1200px", + }, + "16/9": { + "sources": { + "image/webp": { + "800": "/media/testapp/profile/image/16_9/800w.webp", + "100": "/media/testapp/profile/image/16_9/100w.webp", + "200": "/media/testapp/profile/image/16_9/200w.webp", + "300": "/media/testapp/profile/image/16_9/300w.webp", + "400": "/media/testapp/profile/image/16_9/400w.webp", + "500": "/media/testapp/profile/image/16_9/500w.webp", + "600": "/media/testapp/profile/image/16_9/600w.webp", + "700": "/media/testapp/profile/image/16_9/700w.webp", + } + }, + "media": "(min-width: 0px) and (max-width: 1199px) 100vw, 1200px", + }, + }, + } + @pytest.mark.django_db def test_to_representation__with_container(self, rf, image_upload_file, settings): settings.PICTURES["USE_PLACEHOLDERS"] = False @@ -304,7 +388,8 @@ def test_to_representation__with_prefiltered_aspect_ratio_and_source( "600": "/media/testapp/profile/image/3_2/600w.webp", "700": "/media/testapp/profile/image/3_2/700w.webp", } - } + }, + "media": "(min-width: 0px) and (max-width: 1199px) 100vw, 1200px", } }, }