Skip to content

Fix #190 -- Use field breakpoints and container width to calculate media query #193

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions pictures/contrib/rest_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
__all__ = ["PictureField"]

from pictures import utils
from pictures.conf import get_settings
from pictures.models import PictureFieldFile, SimplePicture


Expand All @@ -33,13 +32,14 @@
"width": obj.width,
"height": obj.height,
}
field = obj.field

Check warning on line 35 in pictures/contrib/rest_framework.py

View check run for this annotation

Codecov / codecov/patch

pictures/contrib/rest_framework.py#L35

Added line #L35 was not covered by tests

# if the request has query parameters, filter the payload
try:
query_params: QueryDict = self.context["request"].GET
except KeyError:
ratios = self.aspect_ratios
container = get_settings().CONTAINER_WIDTH
container = field.container_width

Check warning on line 42 in pictures/contrib/rest_framework.py

View check run for this annotation

Codecov / codecov/patch

pictures/contrib/rest_framework.py#L42

Added line #L42 was not covered by tests
breakpoints = {}
else:
ratios = (
Expand All @@ -49,12 +49,12 @@
try:
container = int(container)
except TypeError:
container = get_settings().CONTAINER_WIDTH
container = field.container_width

Check warning on line 52 in pictures/contrib/rest_framework.py

View check run for this annotation

Codecov / codecov/patch

pictures/contrib/rest_framework.py#L52

Added line #L52 was not covered by tests
except ValueError as e:
raise ValueError(f"Container width is not a number: {container}") from e
breakpoints = {
bp: int(query_params.get(f"{self.field_name}_{bp}"))
for bp in get_settings().BREAKPOINTS
for bp in field.breakpoints
if f"{self.field_name}_{bp}" in query_params
}
if set(ratios) - set(self.aspect_ratios or obj.aspect_ratios.keys()):
Expand All @@ -71,7 +71,9 @@
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),
"media": utils.sizes(
field=field, container_width=container, **breakpoints
),
}
for ratio, sources in obj.aspect_ratios.items()
if ratio in ratios or not ratios
Expand Down
7 changes: 4 additions & 3 deletions pictures/templatetags/pictures.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
@register.simple_tag()
def picture(field_file, img_alt=None, ratio=None, container=None, **kwargs):
settings = get_settings()
container = container or settings.CONTAINER_WIDTH
field = field_file.field
container = container or field.container_width
tmpl = loader.get_template("pictures/picture.html")
breakpoints = {}
picture_attrs = {}
Expand All @@ -29,7 +30,7 @@ def picture(field_file, img_alt=None, ratio=None, container=None, **kwargs):
f"Invalid ratio: {ratio}. Choices are: {', '.join(filter(None, field_file.aspect_ratios.keys()))}"
) from e
for key, value in kwargs.items():
if key in settings.BREAKPOINTS:
if key in field.breakpoints:
breakpoints[key] = value
elif key.startswith("picture_"):
picture_attrs[key[8:]] = value
Expand All @@ -43,7 +44,7 @@ def picture(field_file, img_alt=None, ratio=None, container=None, **kwargs):
"alt": img_alt,
"ratio": (ratio or "3/2").replace("/", "x"),
"sources": sources,
"media": utils.sizes(container_width=container, **breakpoints),
"media": utils.sizes(field=field, container_width=container, **breakpoints),
"picture_attrs": picture_attrs,
"img_attrs": img_attrs,
"use_placeholders": settings.USE_PLACEHOLDERS,
Expand Down
24 changes: 13 additions & 11 deletions pictures/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,22 @@
__all__ = ["sizes", "source_set", "placeholder"]


def _grid(*, _columns=12, **breakpoint_sizes):
settings = conf.get_settings()
for key in breakpoint_sizes.keys() - settings.BREAKPOINTS.keys():
def _grid(*, field, _columns=12, **breakpoint_sizes):
for key in breakpoint_sizes.keys() - field.breakpoints:
raise KeyError(
f"Invalid breakpoint: {key}. Choices are: {', '.join(settings.BREAKPOINTS.keys())}"
f"Invalid breakpoint: {key}. Choices are: {', '.join(field.breakpoints.keys())}"
)
prev_size = _columns
for key, value in settings.BREAKPOINTS.items():
for key, value in field.breakpoints.items():
prev_size = breakpoint_sizes.get(key, prev_size)
yield key, prev_size / _columns


def _media_query(*, container_width: int | None = None, **breakpoints: int):
settings = conf.get_settings()
def _media_query(*, field, container_width: int | None = None, **breakpoints: int):
prev_ratio = None
prev_width = 0
for key, ratio in breakpoints.items():
width = settings.BREAKPOINTS[key]
width = field.breakpoints[key]
if container_width and width >= container_width:
yield f"(min-width: {prev_width}px) and (max-width: {container_width - 1}px) {math.floor(ratio * 100)}vw"
break
Expand All @@ -53,9 +51,13 @@ def _media_query(*, container_width: int | None = None, **breakpoints: int):
yield f"{container_width}px" if container_width else "100vw"


def sizes(*, cols=12, container_width: int | None = None, **breakpoints: int) -> str:
breakpoints = dict(_grid(_columns=cols, **breakpoints))
return ", ".join(_media_query(container_width=container_width, **breakpoints))
def sizes(
*, field, cols=12, container_width: int | None = None, **breakpoints: int
) -> str:
breakpoints = dict(_grid(field=field, _columns=cols, **breakpoints))
return ", ".join(
_media_query(field=field, container_width=container_width, **breakpoints)
)


def source_set(
Expand Down
10 changes: 10 additions & 0 deletions tests/test_templatetags.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ def test_picture__additional_attrs__type_error(image_upload_file):
assert "Invalid keyword argument: does_not_exist" in str(e.value)


@pytest.mark.django_db
def test_picture__field_defaults(image_upload_file):
profile = Profile.objects.create(name="Spiderman", other_picture=image_upload_file)
html = picture(profile.other_picture, ratio="3/2", small=2, medium=3)
assert (
'sizes="(min-width: 0px) and (max-width: 399px) 16vw, (min-width: 400px) and (max-width: 599px) 25vw, 150px"'
in html
)


@pytest.mark.django_db
def test_img_url(image_upload_file):
profile = Profile.objects.create(name="Spiderman", picture=image_upload_file)
Expand Down
31 changes: 18 additions & 13 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@

from pictures import utils
from pictures.models import SimplePicture
from tests.testapp.models import SimpleModel


class TestGrid:
def test_default(self):
assert list(utils._grid()) == [
assert list(utils._grid(field=SimpleModel.picture.field)) == [
("xs", 1.0),
("s", 1.0),
("m", 1.0),
Expand All @@ -16,7 +17,7 @@ def test_default(self):
]

def test_small_up(self):
assert list(utils._grid(xs=6)) == [
assert list(utils._grid(field=SimpleModel.picture.field, xs=6)) == [
("xs", 0.5),
("s", 0.5),
("m", 0.5),
Expand All @@ -25,7 +26,7 @@ def test_small_up(self):
]

def test_mixed(self):
assert list(utils._grid(s=6, l=9)) == [
assert list(utils._grid(field=SimpleModel.picture.field, s=6, l=9)) == [
("xs", 1.0),
("s", 0.5),
("m", 0.5),
Expand All @@ -35,50 +36,54 @@ def test_mixed(self):

def test_key_error(self):
with pytest.raises(KeyError) as e:
list(utils._grid(xxxxl=6))
list(utils._grid(field=SimpleModel.picture.field, xxxxl=6))
assert "Invalid breakpoint: xxxxl. Choices are: xs, s, m, l, xl" in str(e.value)


class TestSizes:
def test_default(self):
assert utils.sizes() == "100vw"
assert utils.sizes(field=SimpleModel.picture.field) == "100vw"

def test_default__container(self):
assert (
utils.sizes(container_width=1200)
utils.sizes(field=SimpleModel.picture.field, container_width=1200)
== "(min-width: 0px) and (max-width: 1199px) 100vw, 1200px"
)

def test_bottom_up(self):
assert utils.sizes(xs=6) == "50vw"
assert utils.sizes(field=SimpleModel.picture.field, xs=6) == "50vw"

def test_bottom_up__container(self):
assert (
utils.sizes(container_width=1200, xs=6)
utils.sizes(field=SimpleModel.picture.field, container_width=1200, xs=6)
== "(min-width: 0px) and (max-width: 1199px) 50vw, 600px"
)

def test_medium_up(self):
assert utils.sizes(s=6) == "(min-width: 0px) and (max-width: 767px) 100vw, 50vw"
assert (
utils.sizes(field=SimpleModel.picture.field, s=6)
== "(min-width: 0px) and (max-width: 767px) 100vw, 50vw"
)

def test_medium_up__container(self):
assert (
utils.sizes(container_width=1200, s=6)
utils.sizes(field=SimpleModel.picture.field, container_width=1200, s=6)
== "(min-width: 0px) and (max-width: 767px) 100vw,"
" (min-width: 768px) and (max-width: 1199px) 50vw,"
" 600px"
)

def test_mixed(self):
assert (
utils.sizes(s=6, l=9) == "(min-width: 0px) and (max-width: 767px) 100vw,"
utils.sizes(field=SimpleModel.picture.field, s=6, l=9)
== "(min-width: 0px) and (max-width: 767px) 100vw,"
" (min-width: 768px) and (max-width: 1199px) 50vw,"
" 75vw"
)

def test_mixed__container(self):
assert (
utils.sizes(container_width=1200, s=6, l=9)
utils.sizes(field=SimpleModel.picture.field, container_width=1200, s=6, l=9)
== "(min-width: 0px) and (max-width: 767px) 100vw,"
" (min-width: 768px) and (max-width: 1199px) 75vw,"
" 600px"
Expand All @@ -87,7 +92,7 @@ def test_mixed__container(self):
def test_container__smaller_than_breakpoint(self):
with pytest.warns() as records:
assert (
utils.sizes(container_width=500)
utils.sizes(field=SimpleModel.picture.field, container_width=500)
== "(min-width: 0px) and (max-width: 499px) 100vw, 500px"
)
assert str(records[0].message) == (
Expand Down
30 changes: 30 additions & 0 deletions tests/testapp/migrations/0006_profile_other_picture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 4.2.7 on 2024-12-14 13:15

from django.db import migrations

import pictures.models


class Migration(migrations.Migration):

dependencies = [
("testapp", "0005_alter_profile_picture"),
]

operations = [
migrations.AddField(
model_name="profile",
name="other_picture",
field=pictures.models.PictureField(
aspect_ratios=[None, "1/1", "3/2", "16/9"],
blank=True,
breakpoints=["small", "medium", "large"],
container_width=600,
file_types=["WEBP"],
grid_columns=12,
pixel_densities=[1, 2],
upload_to="testapp/profile/",
null=True,
),
),
]
13 changes: 13 additions & 0 deletions tests/testapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ class Profile(models.Model):
blank=True,
)

other_picture = PictureField(
upload_to="testapp/profile/",
aspect_ratios=[None, "1/1", "3/2", "16/9"],
breakpoints={
"small": 200,
"medium": 400,
"large": 800,
},
container_width=600,
blank=True,
null=True,
)

def get_absolute_url(self):
return reverse("profile_detail", kwargs={"pk": self.pk})

Expand Down
Loading