Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
d7d3c57
added default expiration time to the qualification to make it as vers…
samuelson-ben Aug 21, 2025
f6d940a
addind default expiration time field for QualificationForm
samuelson-ben Aug 21, 2025
696c52f
added default expiratrion time for import
samuelson-ben Aug 21, 2025
7a15c34
added regex for input validation
samuelson-ben Aug 21, 2025
f92927a
adding translations for expiration time
samuelson-ben Aug 21, 2025
36d67f6
adding template for relative time field
samuelson-ben Sep 15, 2025
63641fd
adding widget for custom relative time fields
samuelson-ben Sep 15, 2025
8e64c21
added relative time field
samuelson-ben Sep 15, 2025
71305ae
added defaultexpirationtimefield and implemented to qualification model
samuelson-ben Sep 15, 2025
aaeb3b0
Changed Option for expiration time
samuelson-ben Sep 15, 2025
341b3e2
added translations for default expiration time
samuelson-ben Sep 15, 2025
d427317
add labels
samuelson-ben Sep 15, 2025
6827e7f
add hidding for unused fields
samuelson-ben Sep 15, 2025
d8ff0d6
translation for labels of subwidgets
samuelson-ben Sep 15, 2025
bdf2ac0
improved styling for mobile devices
samuelson-ben Sep 15, 2025
da6225b
init commit for plugin
samuelson-ben Oct 3, 2025
1622d7d
added own modelgroup for relative time types
samuelson-ben Oct 8, 2025
314735c
refactored widget for variable use
samuelson-ben Oct 8, 2025
0cabaf9
refactored fields for variable use
samuelson-ben Oct 8, 2025
5a52f25
added plugin
samuelson-ben Oct 13, 2025
7acd8d8
removed old plugin(name)
samuelson-ben Oct 13, 2025
57aa11b
copied old templates
samuelson-ben Oct 13, 2025
4d72127
added model from old plugin
samuelson-ben Oct 13, 2025
219a339
added add view (only view, not save)
samuelson-ben Oct 13, 2025
2e534a4
added views for own, all and add
samuelson-ben Oct 13, 2025
91cf98c
added urls for own, all, add
samuelson-ben Oct 13, 2025
877515e
added signals to add to settings
samuelson-ben Oct 13, 2025
0b12fcd
added human readable names
samuelson-ben Oct 15, 2025
23608d8
reentered which fields to show in the list view.
samuelson-ben Oct 15, 2025
3b20b12
corrected views an added update view
samuelson-ben Oct 15, 2025
c244add
added url for update view
samuelson-ben Oct 15, 2025
1dee014
corrected tempaltes for list views
samuelson-ben Oct 15, 2025
4943068
corrected templates
samuelson-ben Oct 16, 2025
b36d3df
added user_comment to model
samuelson-ben Oct 16, 2025
ffb2204
seperated form for checking
samuelson-ben Oct 16, 2025
e157ef8
configured view for new form
samuelson-ben Oct 16, 2025
d931e4d
changed names in urls
samuelson-ben Oct 16, 2025
c1403bc
removed batch
samuelson-ben Oct 19, 2025
83f71d3
added falsly deleted import
samuelson-ben Oct 19, 2025
6e2e1c9
removed unused templates
samuelson-ben Oct 21, 2025
784ecd9
removed unused fields in form of comments
samuelson-ben Oct 21, 2025
41e9655
restructured templates
samuelson-ben Oct 21, 2025
1181fc3
refactoring model and adding __str__ method
samuelson-ben Oct 21, 2025
ac0f006
reworking form into multiple simpler forms
samuelson-ben Oct 21, 2025
36ea6bb
corrected active state for nav items
samuelson-ben Oct 21, 2025
124822a
added and corrected url names
samuelson-ben Oct 21, 2025
de3558f
restructured add view and adding check and delete views
samuelson-ben Oct 21, 2025
16a7207
added calculation for expiration date
samuelson-ben Oct 25, 2025
387ea3e
corrected event observing
samuelson-ben Oct 26, 2025
3e49074
added notifications
samuelson-ben Oct 26, 2025
e303003
changed name
samuelson-ben Oct 30, 2025
15bf226
rework field and widget to use fixed values and MultiValueField
jeriox Dec 4, 2025
fc85285
rework request to use consequence handler
jeriox Dec 5, 2025
9b863b3
implement other types
jeriox Dec 6, 2025
368c06b
Merge pull request #1 from ephios-dev/expiration-time-rewrite
samuelson-ben Dec 7, 2025
c75003f
Revert "simplify expiration handling"
samuelson-ben Dec 7, 2025
3530ede
Merge pull request #2 from samuelson-ben/revert-1-expiration-time-rew…
samuelson-ben Dec 7, 2025
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
6 changes: 6 additions & 0 deletions ephios/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
UserParticipationView,
UserProfileMeView,
UserViewSet,
calculate_expiration_date,
)
from ephios.extra.permissions import staff_required

Expand Down Expand Up @@ -109,5 +110,10 @@
SpectacularSwaggerSplitView.as_view(url_name="openapi-schema"),
name="swagger-ui",
),
path(
"qualifications/default-expiration-date/calculate/",
calculate_expiration_date,
name="default_expiration_time_calculate"
),
path("", include(router.urls)),
]
87 changes: 86 additions & 1 deletion ephios/api/views/users.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from datetime import date
from django.http import JsonResponse
from django_filters.rest_framework import DjangoFilterBackend
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_GET
from oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope
from rest_framework import viewsets
from rest_framework.exceptions import PermissionDenied
Expand All @@ -22,7 +26,11 @@
UserinfoParticipationSerializer,
UserProfileSerializer,
)
from ephios.core.models import AbstractParticipation, UserProfile
from ephios.core.models import (
AbstractParticipation,
UserProfile,
Qualification,
)


class UserProfileMeView(RetrieveAPIView):
Expand Down Expand Up @@ -84,3 +92,80 @@ def get_queryset(self):
return AbstractParticipation.objects.filter(
localparticipation__user=self.kwargs.get("user")
).select_related("shift", "shift__event", "shift__event__type")


@require_GET
def calculate_expiration_date(request):
qualification_id = request.GET.get("qualification")
qualification_date_str = request.GET.get("qualification_date")

# Eingaben prüfen
if not qualification_id:
return JsonResponse(
{
"error": _("No qualification selected."),
"expiration_date": "",
},
status=400,
)
if not qualification_date_str:
return JsonResponse(
{
"error": _("No qualification date provided."),
"expiration_date": "",
},
status=400,
)

try:
qualification = Qualification.objects.get(pk=qualification_id)
except Qualification.DoesNotExist:
return JsonResponse(
{
"error": _("Selected qualification does not exist."),
"expiration_date": "",
},
status=400,
)
try:
qualification_date = date.fromisoformat(qualification_date_str)
except ValueError:
return JsonResponse(
{
"error": _("Invalid qualification date format."),
"expiration_date": "",
},
status=400,
)

# Default Expiration Time prüfen
default_expiration = getattr(qualification, "default_expiration_time", None)
if not default_expiration:
return JsonResponse(
{
"error": _("This qualification has no default expiration time defined."),
"expiration_date": "",
},
status=200,
)

# Ablaufdatum berechnen
try:
expiration_date = default_expiration.apply_to(qualification_date)
except Exception as e:
return JsonResponse(
{
"error": _("Error while calculating expiration date: %(error)s") % {"error": str(e)},
"expiration_date": "",
},
status=500,
)

# Erfolg
return JsonResponse(
{
"error": "",
"expiration_date": expiration_date.isoformat() if expiration_date else "",
},
status=200,
)
25 changes: 22 additions & 3 deletions ephios/core/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from ephios.extra.fields import EndOfDayDateTimeField
from ephios.extra.fields import EndOfDayDateTimeField, RelativeTimeField
from ephios.extra.json import CustomJSONDecoder, CustomJSONEncoder
from ephios.extra.widgets import CustomDateInput
from ephios.extra.relative_time import RelativeTimeModelField
from ephios.extra.widgets import CustomDateInput, RelativeTimeWidget
from ephios.modellogging.log import (
ModelFieldsLogConfig,
add_log_recorder,
Expand Down Expand Up @@ -275,6 +276,17 @@ class QualificationManager(models.Manager):
def get_by_natural_key(self, qualification_uuid, *args):
return self.get(uuid=qualification_uuid)

class DefaultExpirationTimeField(RelativeTimeModelField):
"""
A model field whose formfield is a RelativeTimeField
"""

def formfield(self, **kwargs):
return super().formfield(
widget = RelativeTimeWidget,
form_class=RelativeTimeField,
**kwargs,
)

class Qualification(Model):
uuid = models.UUIDField(unique=True, default=uuid.uuid4, verbose_name="UUID")
Expand All @@ -294,6 +306,14 @@ class Qualification(Model):
symmetrical=False,
blank=True,
)
default_expiration_time = DefaultExpirationTimeField(
verbose_name=_("Default expiration time"),
help_text=_(
"The default expiration time for this qualification."
),
null=True,
blank=True,
)
is_imported = models.BooleanField(verbose_name=_("imported"), default=True)

objects = QualificationManager()
Expand All @@ -317,7 +337,6 @@ def natural_key(self):

natural_key.dependencies = ["core.QualificationCategory"]


register_model_for_logging(
Qualification,
ModelFieldsLogConfig(),
Expand Down
90 changes: 90 additions & 0 deletions ephios/extra/fields.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import datetime

from django.utils.translation import gettext as _
from django import forms
from django.forms.utils import from_current_timezone
from ephios.extra.relative_time import RelativeTimeTypeRegistry
from ephios.extra.widgets import RelativeTimeWidget

import json

class EndOfDayDateTimeField(forms.DateTimeField):
"""
Expand All @@ -21,3 +25,89 @@ def to_python(self, value):
day=result.day,
)
)

class RelativeTimeField(forms.JSONField):
"""
A form field that dynamically adapts to all registered RelativeTime types.
"""

widget = RelativeTimeWidget

def bound_data(self, data, initial):
if isinstance(data, list):
return data
return super().bound_data(data, initial)

def to_python(self, value):
if not value:
return None

try:
# Determine all known types and their parameters
type_names = [name for name, _ in RelativeTimeTypeRegistry.all()]

if isinstance(value, list):
# first element = type index
type_index = int(value[0]) if value and value[0] is not None else 0
type_name = type_names[type_index] if 0 <= type_index < len(type_names) else None
handler = RelativeTimeTypeRegistry.get(type_name)
if not handler:
raise ValueError(_("Invalid choice"))

params = {}
# remaining values correspond to all known parameters
all_param_names = sorted({p for _, h in RelativeTimeTypeRegistry.all() for p in getattr(h, "fields", [])})
for param_name, param_value in zip(all_param_names, value[1:]):
if param_value not in (None, ""):
params[param_name] = int(param_value)
return {"type": type_name, **params}

if isinstance(value, str):
data = json.loads(value)
else:
data = value

if not isinstance(data, dict):
raise ValueError("Not a dict")

type_name = data.get("type")
handler = RelativeTimeTypeRegistry.get(type_name)
if not handler:
raise ValueError(_("Unknown type"))

# basic validation: ensure required params exist
for param in getattr(handler, "fields", []):
if param not in data:
raise ValueError(_("Missing field: {param}").format(param=param))

return data

except (json.JSONDecodeError, ValueError, TypeError) as e:
raise forms.ValidationError(
_("Invalid format: {error}").format(error=e)
) from e

def prepare_value(self, value):
if value is None:
return [0] + [None] * len({p for _, h in RelativeTimeTypeRegistry.all() for p in getattr(h, "fields", [])})

if isinstance(value, list):
return value

if isinstance(value, str):
try:
value = json.loads(value)
except json.JSONDecodeError:
return [0] + [None] * len({p for _, h in RelativeTimeTypeRegistry.all() for p in getattr(h, "fields", [])})

if not isinstance(value, dict):
return [0] + [None] * len({p for _, h in RelativeTimeTypeRegistry.all() for p in getattr(h, "fields", [])})

type_names = [name for name, _ in RelativeTimeTypeRegistry.all()]
type_name = value.get("type", "no_expiration")
type_index = type_names.index(type_name) if type_name in type_names else 0

all_param_names = sorted({p for _, h in RelativeTimeTypeRegistry.all() for p in getattr(h, "fields", [])})
params = [value.get(p) for p in all_param_names]

return [type_index] + params
Loading