Skip to content
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

Fix/backend/drop multiselect field dependency #280

Merged
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
18 changes: 2 additions & 16 deletions backend/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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]),
),
]
2 changes: 1 addition & 1 deletion backend/timed/employment/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand Down
2 changes: 1 addition & 1 deletion backend/timed/employment/tests/test_location.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
117 changes: 99 additions & 18 deletions backend/timed/models.py
Original file line number Diff line number Diff line change
@@ -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\
<br>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())
1 change: 0 additions & 1 deletion backend/timed/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
50 changes: 50 additions & 0 deletions backend/timed/tests/test_weekdays.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 1 addition & 2 deletions backend/timed/tracking/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading