From eb368b17e6194b37f9e25c863c1d7f4742521177 Mon Sep 17 00:00:00 2001 From: Arthur Deierlein Date: Mon, 17 Jun 2024 14:56:09 +0200 Subject: [PATCH 1/2] chore(backend/deps): drop django-multiselectfield --- backend/poetry.lock | 18 ++---------------- backend/pyproject.toml | 1 - 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index a19f0d146..93c5b9152 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "appnope" @@ -525,20 +525,6 @@ setuptools = "*" exchange = ["certifi"] test = ["django-stubs", "mixer", "mypy", "pytest (>=3.1.0,<7.0)", "pytest-cov", "pytest-django", "pytest-pythonpath"] -[[package]] -name = "django-multiselectfield" -version = "0.1.12" -description = "Django multiple select field" -optional = false -python-versions = "*" -files = [ - {file = "django-multiselectfield-0.1.12.tar.gz", hash = "sha256:d0a4c71568fb2332c71478ffac9f8708e01314a35cf923dfd7a191343452f9f9"}, - {file = "django_multiselectfield-0.1.12-py3-none-any.whl", hash = "sha256:c270faa7f80588214c55f2d68cbddb2add525c2aa830c216b8a198de914eb470"}, -] - -[package.dependencies] -django = ">=1.4" - [[package]] name = "django-nested-inline" version = "0.4.6" @@ -1972,4 +1958,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "71c29ee83d362bdb03193f552eea36bca00d9f809ea5ec076dea08a2d9186ef4" +content-hash = "b33d763a79df9041a51f0530fecf9ea6760fcf979d5721e9b4043df5fe6a92d5" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 3ebc3a1e8..c0e57a31a 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -25,7 +25,6 @@ django = "^4.2.11" # might remove this once we find out how the jsonapi extras_require work django-cors-headers = "^4.3.1" django-filter = "^24.2" -django-multiselectfield = "^0.1.12" django-prometheus = "^2.3.1" djangorestframework = "^3.15.1" djangorestframework-jsonapi = "^7.0.1" From bde799c45a00be27d5d74c646c849c9ac0b93d14 Mon Sep 17 00:00:00 2001 From: Arthur Deierlein Date: Tue, 18 Jun 2024 13:19:37 +0200 Subject: [PATCH 2/2] fix(backend): drop django-multiselectfield dependency --- .../0016_alter_location_workdays.py | 19 +++ backend/timed/employment/models.py | 2 +- .../timed/employment/tests/test_location.py | 2 +- backend/timed/models.py | 117 +++++++++++++++--- backend/timed/settings.py | 1 - backend/timed/tests/test_weekdays.py | 50 ++++++++ backend/timed/tracking/serializers.py | 3 +- 7 files changed, 171 insertions(+), 23 deletions(-) create mode 100644 backend/timed/employment/migrations/0016_alter_location_workdays.py create mode 100644 backend/timed/tests/test_weekdays.py diff --git a/backend/timed/employment/migrations/0016_alter_location_workdays.py b/backend/timed/employment/migrations/0016_alter_location_workdays.py new file mode 100644 index 000000000..e521bb0fc --- /dev/null +++ b/backend/timed/employment/migrations/0016_alter_location_workdays.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.11 on 2024-06-18 10:47 + +from django.db import migrations +import timed.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('employment', '0015_user_is_accountant'), + ] + + operations = [ + migrations.AlterField( + model_name='location', + name='workdays', + field=timed.models.WeekdaysField(default=[1, 2, 3, 4, 5]), + ), + ] diff --git a/backend/timed/employment/models.py b/backend/timed/employment/models.py index 92e03616b..9e36759d2 100644 --- a/backend/timed/employment/models.py +++ b/backend/timed/employment/models.py @@ -28,7 +28,7 @@ class Location(models.Model): """ name = models.CharField(max_length=50, unique=True) - workdays = WeekdaysField(default=[str(day) for day in range(1, 6)]) + workdays = WeekdaysField(default=list(range(1, 6))) """ Workdays defined per location, default is Monday - Friday """ diff --git a/backend/timed/employment/tests/test_location.py b/backend/timed/employment/tests/test_location.py index 9fa052492..06d6854a5 100644 --- a/backend/timed/employment/tests/test_location.py +++ b/backend/timed/employment/tests/test_location.py @@ -40,7 +40,7 @@ def test_location_list( data = response.json()["data"] assert len(data) == expected if expected: - assert data[0]["attributes"]["workdays"] == ([str(day) for day in range(1, 6)]) + assert data[0]["attributes"]["workdays"] == ",".join(map(str, range(1, 6))) @pytest.mark.parametrize( diff --git a/backend/timed/models.py b/backend/timed/models.py index 3b89580e5..4da59f57d 100644 --- a/backend/timed/models.py +++ b/backend/timed/models.py @@ -1,31 +1,112 @@ """Basic model and field classes to be used in all apps.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from django.core.exceptions import ValidationError +from django.db.models import CharField +from django.forms.fields import CharField as CharFormField +from django.utils.text import capfirst from django.utils.translation import gettext_lazy as _ -from multiselectfield import MultiSelectField -from multiselectfield.utils import get_max_length +if TYPE_CHECKING: + from typing import Self + +MO, TU, WE, TH, FR, SA, SU = DAYS = range(1, 8) + +WEEKDAYS = ( + (MO, _("Monday")), + (TU, _("Tuesday")), + (WE, _("Wednesday")), + (TH, _("Thursday")), + (FR, _("Friday")), + (SA, _("Saturday")), + (SU, _("Sunday")), +) + + +class WeekdaysList(list): + """List used for weekdays.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.validate() + + def __str__(self): + return ",".join(map(str, self)) + + def validate(self) -> None: + if invalid_days := [day for day in self if day not in DAYS]: + raise ValidationError( + [ + ValidationError( + _("Invalid day: %(value)s"), + code="invalid", + params={"value": day}, + ) + for day in invalid_days + ] + ) -class WeekdaysField(MultiSelectField): + @classmethod + def from_string(cls: type[Self], value: str) -> Self[int]: + try: + days = cls([int(day) for day in value.split(",")]) + except ValueError as exc: + raise ValidationError( + _("Invalid weekdays: %(value)s"), + code="invalid", + params={"value": value}, + ) from exc + + days.validate() + return days + + +class WeekdaysFormField(CharFormField): # pragma: no cover + def to_python(self, value): + if isinstance(value, str): + return WeekdaysList.from_string(value) + return value + + +class WeekdaysField(CharField): """Multi select field using weekdays as choices. Stores weekdays as comma-separated values in database as iso week day (MON = 1, SUN = 7). """ - MO, TU, WE, TH, FR, SA, SU = range(1, 8) + def from_db_value(self, value, expression, connection): # noqa: ARG002 + if value is None: # pragma: no cover + return value + return WeekdaysList.from_string(value) - WEEKDAYS = ( - (MO, _("Monday")), - (TU, _("Tuesday")), - (WE, _("Wednesday")), - (TH, _("Thursday")), - (FR, _("Friday")), - (SA, _("Saturday")), - (SU, _("Sunday")), - ) + def to_python(self, value): + if isinstance(value, str): + return WeekdaysList.from_string(value) + if isinstance(value, list): + return WeekdaysList(value) + return value # pragma: no cover - def __init__(self, *args, **kwargs): - """Initialize multi select with choices weekdays.""" - kwargs["choices"] = self.WEEKDAYS - kwargs["max_length"] = get_max_length(self.WEEKDAYS, None) - super().__init__(*args, **kwargs) + def get_prep_value(self, value): + if value is not None: + return str(self.to_python(value)) + return value # pragma: no cover + + def formfield(self, **kwargs): # pragma: no cover + defaults = { + "required": not self.blank, + "help_text": f"Comma Seperated List of days. MON = 1, SUN = 7\ +
e.g. '1,2,3,4,5' -> {', '.join(str(wd[1]) for wd in WEEKDAYS[:5])}", + "label": capfirst(self.verbose_name), + } + + if self.has_default(): + defaults["initial"] = self.get_default() + + return WeekdaysFormField(**defaults) + + def get_default(self): + return self.to_python(self._get_default()) diff --git a/backend/timed/settings.py b/backend/timed/settings.py index db8253f6c..dec688be6 100644 --- a/backend/timed/settings.py +++ b/backend/timed/settings.py @@ -53,7 +53,6 @@ def default(default_dev=env.NOTSET, default_prod=env.NOTSET): INSTALLED_APPS = [ "timed.apps.TimedAdminConfig", "django.contrib.humanize", - "multiselectfield.apps.MultiSelectFieldConfig", "django.forms", "django.contrib.auth", "django.contrib.contenttypes", diff --git a/backend/timed/tests/test_weekdays.py b/backend/timed/tests/test_weekdays.py new file mode 100644 index 000000000..df429422f --- /dev/null +++ b/backend/timed/tests/test_weekdays.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import pytest +from django.core.exceptions import ValidationError + +from timed.employment.models import Location +from timed.models import WeekdaysList + + +@pytest.mark.django_db(transaction=True) +@pytest.mark.parametrize( + ("value", "error"), + [ + (list(range(1, 2)), False), + (list(range(1, 3)), False), + (list(range(1, 4)), False), + (list(range(1, 5)), False), + (list(range(1, 6)), False), + (list(range(1, 7)), False), + (list(range(1, 8)), False), + (list(range(2, 9)), True), + ("1,2", False), + ("1,2,3,4,5", False), + ("1,2,3,4,5,6", False), + ("1,2,3,4,5,6,7,8", True), + (list(range(5, 11)), True), + (list(range(9, 11)), True), + ("not,valid", True), + ], +) +def test_weekdays_field(value: list[int], error: bool, location_factory): # noqa: FBT001 + try: + location_id = location_factory(workdays=value).id + Location.objects.get(id=location_id) + assert not error + except ValidationError: + assert error + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ([1, 2, 3], "1,2,3"), + ([1, 2, 3, 4], "1,2,3,4"), + ([1, 2, 3, 5], "1,2,3,5"), + ], +) +def test_weekdayslist(value, expected): + assert WeekdaysList.from_string(expected) == value + assert str(WeekdaysList(value)) == expected diff --git a/backend/timed/tracking/serializers.py b/backend/timed/tracking/serializers.py index 41d7a1298..a2034d2b8 100644 --- a/backend/timed/tracking/serializers.py +++ b/backend/timed/tracking/serializers.py @@ -453,8 +453,7 @@ def validate(self, data: dict) -> dict: ).exists(): raise ValidationError(_("You can't create an absence on a public holiday")) - workdays = [int(day) for day in location.workdays] - if data.get("date").isoweekday() not in workdays: + if data.get("date").isoweekday() not in location.workdays: raise ValidationError(_("You can't create an absence on a weekend")) return data