We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Age checking for signup consideres wrong date
When checking the participant age, the age at the current date is evaluated instead of the age at the event date

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `ephios/event_management/signup.py`
Content:
```
1 import functools
2 import json
3 from argparse import Namespace
4 from dataclasses import dataclass, field
5 from datetime import date
6 from typing import List, Optional
7 
8 import django.dispatch
9 from django import forms
10 from django.contrib import messages
11 from django.contrib.auth import get_user_model
12 from django.core.exceptions import ValidationError
13 from django.db import transaction
14 from django.db.models import QuerySet
15 from django.shortcuts import redirect
16 from django.template import Context, Template
17 from django.template.defaultfilters import yesno
18 from django.utils import formats, timezone
19 from django.utils.functional import cached_property
20 from django.utils.translation import gettext_lazy as _
21 from django.views import View
22 
23 from ephios.event_management.models import AbstractParticipation, LocalParticipation, Shift
24 from ephios.extra.widgets import CustomSplitDateTimeWidget
25 from ephios.user_management.models import Qualification
26 
27 register_signup_methods = django.dispatch.Signal(providing_args=[])
28 
29 
30 def all_signup_methods():
31     for _, methods in register_signup_methods.send(None):
32         yield from methods
33 
34 
35 def signup_method_from_slug(slug, shift=None):
36     for method in all_signup_methods():
37         if method.slug == slug:
38             return method(shift)
39     raise ValueError(_("Signup Method '{slug}' was not found.").format(slug=slug))
40 
41 
42 @dataclass(frozen=True)
43 class AbstractParticipant:
44     first_name: str
45     last_name: str
46     qualifications: QuerySet = field(hash=False)
47     date_of_birth: date
48     email: Optional[str]  # if set to None, no notifications are sent
49 
50     @property
51     def age(self):
52         today, born = date.today(), self.date_of_birth
53         return today.year - born.year - ((today.month, today.day) < (born.month, born.day))
54 
55     def __str__(self):
56         return f"{self.first_name} {self.last_name}"
57 
58     def new_participation(self, shift):
59         raise NotImplementedError
60 
61     def participation_for(self, shift):
62         """Return the participation object for a shift. Return None if it does not exist."""
63         raise NotImplementedError
64 
65     def collect_all_qualifications(self):
66         """We collect using breath first search with one query for every layer of inclusion."""
67         all_qualifications = set(self.qualifications)
68         current = self.qualifications
69         while current:
70             next = (
71                 Qualification.objects.filter(included_by__in=current)
72                 .exclude(id__in=(q.id for q in all_qualifications))
73                 .distinct()
74             )
75             all_qualifications |= set(next)
76             current = next
77         return all_qualifications
78 
79     def has_qualifications(self, qualifications):
80         return set(qualifications) <= self.collect_all_qualifications()
81 
82 
83 @dataclass(frozen=True)
84 class LocalUserParticipant(AbstractParticipant):
85     user: get_user_model()
86 
87     def new_participation(self, shift):
88         return LocalParticipation(shift=shift, user=self.user)
89 
90     def participation_for(self, shift):
91         try:
92             return LocalParticipation.objects.get(shift=shift, user=self.user)
93         except LocalParticipation.DoesNotExist:
94             return None
95 
96 
97 class ParticipationError(ValidationError):
98     pass
99 
100 
101 class ConfigurationForm(forms.Form):
102     pass
103 
104 
105 def check_event_is_active(method, participant):
106     if not method.shift.event.active:
107         return ParticipationError(_("The event is not active."))
108 
109 
110 def check_participation_state_for_signup(method, participant):
111     participation = participant.participation_for(method.shift)
112     if participation is not None:
113         if participation.state == AbstractParticipation.States.REQUESTED:
114             return ParticipationError(
115                 _("You have already requested your participation for {shift}").format(
116                     shift=method.shift
117                 )
118             )
119         elif participation.state == AbstractParticipation.States.CONFIRMED:
120             return ParticipationError(
121                 _("You are already signed up for {shift}.").format(shift=method.shift)
122             )
123         elif participation.state == AbstractParticipation.States.RESPONSIBLE_REJECTED:
124             return ParticipationError(
125                 _("You are rejected from {shift}.").format(shift=method.shift)
126             )
127 
128 
129 def check_participation_state_for_decline(method, participant):
130     participation = participant.participation_for(method.shift)
131     if participation is not None:
132         if (
133             participation.state == AbstractParticipation.States.CONFIRMED
134             and not method.configuration.user_can_decline_confirmed
135         ):
136             return ParticipationError(
137                 _("You are bindingly signed up for {shift}.").format(shift=method.shift)
138             )
139         elif participation.state == AbstractParticipation.States.RESPONSIBLE_REJECTED:
140             return ParticipationError(
141                 _("You are rejected from {shift}.").format(shift=method.shift)
142             )
143         elif participation.state == AbstractParticipation.States.USER_DECLINED:
144             return ParticipationError(
145                 _("You have already declined participating in {shift}.").format(shift=method.shift)
146             )
147 
148 
149 def check_inside_signup_timeframe(method, participant):
150     last_time = method.shift.end_time
151     if method.configuration.signup_until is not None:
152         last_time = min(last_time, method.configuration.signup_until)
153     if timezone.now() > last_time:
154         return ParticipationError(_("The signup period is over."))
155 
156 
157 def check_participant_age(method, participant):
158     minimum_age = method.configuration.minimum_age
159     if minimum_age is not None and participant.age < minimum_age:
160         return ParticipationError(
161             _("You are too young. The minimum age is {age}.").format(age=minimum_age)
162         )
163 
164 
165 class BaseSignupMethod:
166     slug = "abstract"
167     verbose_name = "abstract"
168     description = """"""
169 
170     # use _ == gettext_lazy!
171     registration_button_text = _("Sign up")
172     signup_success_message = _("You have successfully signed up for {shift}.")
173     signup_error_message = _("Signing up failed: {error}")
174     decline_success_message = _("You have successfully declined {shift}.")
175     decline_error_message = _("Declining failed: {error}")
176 
177     def __init__(self, shift):
178         self.shift = shift
179         self.configuration = Namespace(
180             **{name: config["default"] for name, config in self.get_configuration_fields().items()}
181         )
182         if shift is not None:
183             for key, value in shift.signup_configuration.items():
184                 setattr(self.configuration, key, value)
185 
186     @property
187     def signup_view_class(self):
188         return BaseSignupView
189 
190     @cached_property
191     def signup_view(self):
192         return self.signup_view_class.as_view(method=self, shift=self.shift)
193 
194     @property
195     def signup_checkers(self):
196         return [
197             check_event_is_active,
198             check_participation_state_for_signup,
199             check_inside_signup_timeframe,
200             check_participant_age,
201         ]
202 
203     @property
204     def decline_checkers(self):
205         return [
206             check_event_is_active,
207             check_participation_state_for_decline,
208             check_inside_signup_timeframe,
209         ]
210 
211     @functools.lru_cache()
212     def get_signup_errors(self, participant) -> List[ParticipationError]:
213         return [
214             error
215             for checker in self.signup_checkers
216             if (error := checker(self, participant)) is not None
217         ]
218 
219     @functools.lru_cache()
220     def get_decline_errors(self, participant):
221         return [
222             error
223             for checker in self.decline_checkers
224             if (error := checker(self, participant)) is not None
225         ]
226 
227     def can_decline(self, participant):
228         return not self.get_decline_errors(participant)
229 
230     def can_sign_up(self, participant):
231         return not self.get_signup_errors(participant)
232 
233     def get_participation_for(self, participant):
234         return participant.participation_for(self.shift) or participant.new_participation(
235             self.shift
236         )
237 
238     def perform_signup(self, participant: AbstractParticipant, **kwargs):
239         """
240         Configure a participation object for the given participant according to the method's configuration.
241         `kwargs` may contain further instructions from a e.g. a form.
242         """
243         if errors := self.get_signup_errors(participant):
244             raise ParticipationError(errors)
245         return self.get_participation_for(participant)
246 
247     def perform_decline(self, participant, **kwargs):
248         """Create and configure a declining participation object for the given participant. `kwargs` may contain further instructions from a e.g. a form."""
249         if errors := self.get_decline_errors(participant):
250             raise ParticipationError(errors)
251         participation = self.get_participation_for(participant)
252         participation.state = AbstractParticipation.States.USER_DECLINED
253         participation.save()
254         return participation
255 
256     def get_configuration_fields(self):
257         return {
258             "minimum_age": {
259                 "formfield": forms.IntegerField(required=False),
260                 "default": 16,
261                 "publish_with_label": _("Minimum age"),
262             },
263             "signup_until": {
264                 "formfield": forms.SplitDateTimeField(
265                     required=False, widget=CustomSplitDateTimeWidget
266                 ),
267                 "default": None,
268                 "publish_with_label": _("Signup until"),
269                 "format": functools.partial(formats.date_format, format="SHORT_DATETIME_FORMAT"),
270             },
271             "user_can_decline_confirmed": {
272                 "formfield": forms.BooleanField(
273                     label=_("Confirmed users can decline by themselves"),
274                     required=False,
275                     help_text=_("only if the signup timeframe has not ended"),
276                 ),
277                 "default": False,
278                 "publish_with_label": _("Can decline after confirmation"),
279                 "format": yesno,
280             },
281         }
282 
283     def get_signup_info(self):
284         """
285         Return key/value pairs about the configuration to show in the shift info box.
286         """
287         fields = self.get_configuration_fields()
288         return {
289             label: field.get("format", str)(value)
290             for key, field in fields.items()
291             if (label := field.get("publish_with_label", False))
292             and (value := getattr(self.configuration, key))
293         }
294 
295     def render_shift_state(self, request):
296         """
297         Render html that will be shown in the shift info box.
298         Use it to inform about the current state of the shift and participations.
299         """
300         return ""
301 
302     def get_configuration_form(self, *args, **kwargs):
303         if self.shift is not None:
304             kwargs.setdefault("initial", self.configuration.__dict__)
305         form = ConfigurationForm(*args, **kwargs)
306         for name, config in self.get_configuration_fields().items():
307             form.fields[name] = config["formfield"]
308         return form
309 
310     def render_configuration_form(self, form=None, *args, **kwargs):
311         form = form or self.get_configuration_form(*args, **kwargs)
312         template = Template(
313             template_string="{% load bootstrap4 %}{% bootstrap_form form %}"
314         ).render(Context({"form": form}))
315         return template
316 
317 
318 class BaseSignupView(View):
319     shift: Shift = ...
320     method: BaseSignupMethod = ...
321 
322     def dispatch(self, request, *args, **kwargs):
323         if (choice := request.POST.get("signup_choice")) is not None:
324             if choice == "sign_up":
325                 return self.signup_pressed(request, *args, **kwargs)
326             elif choice == "decline":
327                 return self.decline_pressed(request, *args, **kwargs)
328             else:
329                 raise ValueError(
330                     _("'{choice}' is not a valid signup action.").format(choice=choice)
331                 )
332         return super().dispatch(request, *args, **kwargs)
333 
334     def signup_pressed(self, request, *args, **kwargs):
335         try:
336             with transaction.atomic():
337                 self.method.perform_signup(request.user.as_participant())
338                 messages.success(
339                     request,
340                     self.method.signup_success_message.format(shift=self.shift),
341                 )
342         except ParticipationError as errors:
343             for error in errors:
344                 messages.error(request, self.method.signup_error_message.format(error=error))
345         finally:
346             return redirect(self.shift.event.get_absolute_url())
347 
348     def decline_pressed(self, request, *args, **kwargs):
349         try:
350             with transaction.atomic():
351                 self.method.perform_decline(request.user.as_participant())
352                 messages.info(request, self.method.decline_success_message.format(shift=self.shift))
353         except ParticipationError as errors:
354             for error in errors:
355                 messages.error(request, self.method.decline_error_message.format(error=error))
356         finally:
357             return redirect(self.shift.event.get_absolute_url())
358
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/ephios/event_management/signup.py b/ephios/event_management/signup.py
--- a/ephios/event_management/signup.py
+++ b/ephios/event_management/signup.py
@@ -47,9 +47,8 @@
     date_of_birth: date
     email: Optional[str]  # if set to None, no notifications are sent
 
-    @property
-    def age(self):
-        today, born = date.today(), self.date_of_birth
+    def get_age(self, today: date = None):
+        today, born = today or date.today(), self.date_of_birth
         return today.year - born.year - ((today.month, today.day) < (born.month, born.day))
 
     def __str__(self):
@@ -156,7 +155,8 @@
 
 def check_participant_age(method, participant):
     minimum_age = method.configuration.minimum_age
-    if minimum_age is not None and participant.age < minimum_age:
+    day = method.shift.start_time.date()
+    if minimum_age is not None and participant.get_age(day) < minimum_age:
         return ParticipationError(
             _("You are too young. The minimum age is {age}.").format(age=minimum_age)
         )

{"golden_diff": "diff --git a/ephios/event_management/signup.py b/ephios/event_management/signup.py\n--- a/ephios/event_management/signup.py\n+++ b/ephios/event_management/signup.py\n@@ -47,9 +47,8 @@\n     date_of_birth: date\n     email: Optional[str]  # if set to None, no notifications are sent\n \n-    @property\n-    def age(self):\n-        today, born = date.today(), self.date_of_birth\n+    def get_age(self, today: date = None):\n+        today, born = today or date.today(), self.date_of_birth\n         return today.year - born.year - ((today.month, today.day) < (born.month, born.day))\n \n     def __str__(self):\n@@ -156,7 +155,8 @@\n \n def check_participant_age(method, participant):\n     minimum_age = method.configuration.minimum_age\n-    if minimum_age is not None and participant.age < minimum_age:\n+    day = method.shift.start_time.date()\n+    if minimum_age is not None and participant.get_age(day) < minimum_age:\n         return ParticipationError(\n             _(\"You are too young. The minimum age is {age}.\").format(age=minimum_age)\n         )\n", "issue": "Age checking for signup consideres wrong date\nWhen checking the participant age, the age at the current date is evaluated instead of the age at the event date\n", "before_files": [{"content": "import functools\nimport json\nfrom argparse import Namespace\nfrom dataclasses import dataclass, field\nfrom datetime import date\nfrom typing import List, Optional\n\nimport django.dispatch\nfrom django import forms\nfrom django.contrib import messages\nfrom django.contrib.auth import get_user_model\nfrom django.core.exceptions import ValidationError\nfrom django.db import transaction\nfrom django.db.models import QuerySet\nfrom django.shortcuts import redirect\nfrom django.template import Context, Template\nfrom django.template.defaultfilters import yesno\nfrom django.utils import formats, timezone\nfrom django.utils.functional import cached_property\nfrom django.utils.translation import gettext_lazy as _\nfrom django.views import View\n\nfrom ephios.event_management.models import AbstractParticipation, LocalParticipation, Shift\nfrom ephios.extra.widgets import CustomSplitDateTimeWidget\nfrom ephios.user_management.models import Qualification\n\nregister_signup_methods = django.dispatch.Signal(providing_args=[])\n\n\ndef all_signup_methods():\n    for _, methods in register_signup_methods.send(None):\n        yield from methods\n\n\ndef signup_method_from_slug(slug, shift=None):\n    for method in all_signup_methods():\n        if method.slug == slug:\n            return method(shift)\n    raise ValueError(_(\"Signup Method '{slug}' was not found.\").format(slug=slug))\n\n\n@dataclass(frozen=True)\nclass AbstractParticipant:\n    first_name: str\n    last_name: str\n    qualifications: QuerySet = field(hash=False)\n    date_of_birth: date\n    email: Optional[str]  # if set to None, no notifications are sent\n\n    @property\n    def age(self):\n        today, born = date.today(), self.date_of_birth\n        return today.year - born.year - ((today.month, today.day) < (born.month, born.day))\n\n    def __str__(self):\n        return f\"{self.first_name} {self.last_name}\"\n\n    def new_participation(self, shift):\n        raise NotImplementedError\n\n    def participation_for(self, shift):\n        \"\"\"Return the participation object for a shift. Return None if it does not exist.\"\"\"\n        raise NotImplementedError\n\n    def collect_all_qualifications(self):\n        \"\"\"We collect using breath first search with one query for every layer of inclusion.\"\"\"\n        all_qualifications = set(self.qualifications)\n        current = self.qualifications\n        while current:\n            next = (\n                Qualification.objects.filter(included_by__in=current)\n                .exclude(id__in=(q.id for q in all_qualifications))\n                .distinct()\n            )\n            all_qualifications |= set(next)\n            current = next\n        return all_qualifications\n\n    def has_qualifications(self, qualifications):\n        return set(qualifications) <= self.collect_all_qualifications()\n\n\n@dataclass(frozen=True)\nclass LocalUserParticipant(AbstractParticipant):\n    user: get_user_model()\n\n    def new_participation(self, shift):\n        return LocalParticipation(shift=shift, user=self.user)\n\n    def participation_for(self, shift):\n        try:\n            return LocalParticipation.objects.get(shift=shift, user=self.user)\n        except LocalParticipation.DoesNotExist:\n            return None\n\n\nclass ParticipationError(ValidationError):\n    pass\n\n\nclass ConfigurationForm(forms.Form):\n    pass\n\n\ndef check_event_is_active(method, participant):\n    if not method.shift.event.active:\n        return ParticipationError(_(\"The event is not active.\"))\n\n\ndef check_participation_state_for_signup(method, participant):\n    participation = participant.participation_for(method.shift)\n    if participation is not None:\n        if participation.state == AbstractParticipation.States.REQUESTED:\n            return ParticipationError(\n                _(\"You have already requested your participation for {shift}\").format(\n                    shift=method.shift\n                )\n            )\n        elif participation.state == AbstractParticipation.States.CONFIRMED:\n            return ParticipationError(\n                _(\"You are already signed up for {shift}.\").format(shift=method.shift)\n            )\n        elif participation.state == AbstractParticipation.States.RESPONSIBLE_REJECTED:\n            return ParticipationError(\n                _(\"You are rejected from {shift}.\").format(shift=method.shift)\n            )\n\n\ndef check_participation_state_for_decline(method, participant):\n    participation = participant.participation_for(method.shift)\n    if participation is not None:\n        if (\n            participation.state == AbstractParticipation.States.CONFIRMED\n            and not method.configuration.user_can_decline_confirmed\n        ):\n            return ParticipationError(\n                _(\"You are bindingly signed up for {shift}.\").format(shift=method.shift)\n            )\n        elif participation.state == AbstractParticipation.States.RESPONSIBLE_REJECTED:\n            return ParticipationError(\n                _(\"You are rejected from {shift}.\").format(shift=method.shift)\n            )\n        elif participation.state == AbstractParticipation.States.USER_DECLINED:\n            return ParticipationError(\n                _(\"You have already declined participating in {shift}.\").format(shift=method.shift)\n            )\n\n\ndef check_inside_signup_timeframe(method, participant):\n    last_time = method.shift.end_time\n    if method.configuration.signup_until is not None:\n        last_time = min(last_time, method.configuration.signup_until)\n    if timezone.now() > last_time:\n        return ParticipationError(_(\"The signup period is over.\"))\n\n\ndef check_participant_age(method, participant):\n    minimum_age = method.configuration.minimum_age\n    if minimum_age is not None and participant.age < minimum_age:\n        return ParticipationError(\n            _(\"You are too young. The minimum age is {age}.\").format(age=minimum_age)\n        )\n\n\nclass BaseSignupMethod:\n    slug = \"abstract\"\n    verbose_name = \"abstract\"\n    description = \"\"\"\"\"\"\n\n    # use _ == gettext_lazy!\n    registration_button_text = _(\"Sign up\")\n    signup_success_message = _(\"You have successfully signed up for {shift}.\")\n    signup_error_message = _(\"Signing up failed: {error}\")\n    decline_success_message = _(\"You have successfully declined {shift}.\")\n    decline_error_message = _(\"Declining failed: {error}\")\n\n    def __init__(self, shift):\n        self.shift = shift\n        self.configuration = Namespace(\n            **{name: config[\"default\"] for name, config in self.get_configuration_fields().items()}\n        )\n        if shift is not None:\n            for key, value in shift.signup_configuration.items():\n                setattr(self.configuration, key, value)\n\n    @property\n    def signup_view_class(self):\n        return BaseSignupView\n\n    @cached_property\n    def signup_view(self):\n        return self.signup_view_class.as_view(method=self, shift=self.shift)\n\n    @property\n    def signup_checkers(self):\n        return [\n            check_event_is_active,\n            check_participation_state_for_signup,\n            check_inside_signup_timeframe,\n            check_participant_age,\n        ]\n\n    @property\n    def decline_checkers(self):\n        return [\n            check_event_is_active,\n            check_participation_state_for_decline,\n            check_inside_signup_timeframe,\n        ]\n\n    @functools.lru_cache()\n    def get_signup_errors(self, participant) -> List[ParticipationError]:\n        return [\n            error\n            for checker in self.signup_checkers\n            if (error := checker(self, participant)) is not None\n        ]\n\n    @functools.lru_cache()\n    def get_decline_errors(self, participant):\n        return [\n            error\n            for checker in self.decline_checkers\n            if (error := checker(self, participant)) is not None\n        ]\n\n    def can_decline(self, participant):\n        return not self.get_decline_errors(participant)\n\n    def can_sign_up(self, participant):\n        return not self.get_signup_errors(participant)\n\n    def get_participation_for(self, participant):\n        return participant.participation_for(self.shift) or participant.new_participation(\n            self.shift\n        )\n\n    def perform_signup(self, participant: AbstractParticipant, **kwargs):\n        \"\"\"\n        Configure a participation object for the given participant according to the method's configuration.\n        `kwargs` may contain further instructions from a e.g. a form.\n        \"\"\"\n        if errors := self.get_signup_errors(participant):\n            raise ParticipationError(errors)\n        return self.get_participation_for(participant)\n\n    def perform_decline(self, participant, **kwargs):\n        \"\"\"Create and configure a declining participation object for the given participant. `kwargs` may contain further instructions from a e.g. a form.\"\"\"\n        if errors := self.get_decline_errors(participant):\n            raise ParticipationError(errors)\n        participation = self.get_participation_for(participant)\n        participation.state = AbstractParticipation.States.USER_DECLINED\n        participation.save()\n        return participation\n\n    def get_configuration_fields(self):\n        return {\n            \"minimum_age\": {\n                \"formfield\": forms.IntegerField(required=False),\n                \"default\": 16,\n                \"publish_with_label\": _(\"Minimum age\"),\n            },\n            \"signup_until\": {\n                \"formfield\": forms.SplitDateTimeField(\n                    required=False, widget=CustomSplitDateTimeWidget\n                ),\n                \"default\": None,\n                \"publish_with_label\": _(\"Signup until\"),\n                \"format\": functools.partial(formats.date_format, format=\"SHORT_DATETIME_FORMAT\"),\n            },\n            \"user_can_decline_confirmed\": {\n                \"formfield\": forms.BooleanField(\n                    label=_(\"Confirmed users can decline by themselves\"),\n                    required=False,\n                    help_text=_(\"only if the signup timeframe has not ended\"),\n                ),\n                \"default\": False,\n                \"publish_with_label\": _(\"Can decline after confirmation\"),\n                \"format\": yesno,\n            },\n        }\n\n    def get_signup_info(self):\n        \"\"\"\n        Return key/value pairs about the configuration to show in the shift info box.\n        \"\"\"\n        fields = self.get_configuration_fields()\n        return {\n            label: field.get(\"format\", str)(value)\n            for key, field in fields.items()\n            if (label := field.get(\"publish_with_label\", False))\n            and (value := getattr(self.configuration, key))\n        }\n\n    def render_shift_state(self, request):\n        \"\"\"\n        Render html that will be shown in the shift info box.\n        Use it to inform about the current state of the shift and participations.\n        \"\"\"\n        return \"\"\n\n    def get_configuration_form(self, *args, **kwargs):\n        if self.shift is not None:\n            kwargs.setdefault(\"initial\", self.configuration.__dict__)\n        form = ConfigurationForm(*args, **kwargs)\n        for name, config in self.get_configuration_fields().items():\n            form.fields[name] = config[\"formfield\"]\n        return form\n\n    def render_configuration_form(self, form=None, *args, **kwargs):\n        form = form or self.get_configuration_form(*args, **kwargs)\n        template = Template(\n            template_string=\"{% load bootstrap4 %}{% bootstrap_form form %}\"\n        ).render(Context({\"form\": form}))\n        return template\n\n\nclass BaseSignupView(View):\n    shift: Shift = ...\n    method: BaseSignupMethod = ...\n\n    def dispatch(self, request, *args, **kwargs):\n        if (choice := request.POST.get(\"signup_choice\")) is not None:\n            if choice == \"sign_up\":\n                return self.signup_pressed(request, *args, **kwargs)\n            elif choice == \"decline\":\n                return self.decline_pressed(request, *args, **kwargs)\n            else:\n                raise ValueError(\n                    _(\"'{choice}' is not a valid signup action.\").format(choice=choice)\n                )\n        return super().dispatch(request, *args, **kwargs)\n\n    def signup_pressed(self, request, *args, **kwargs):\n        try:\n            with transaction.atomic():\n                self.method.perform_signup(request.user.as_participant())\n                messages.success(\n                    request,\n                    self.method.signup_success_message.format(shift=self.shift),\n                )\n        except ParticipationError as errors:\n            for error in errors:\n                messages.error(request, self.method.signup_error_message.format(error=error))\n        finally:\n            return redirect(self.shift.event.get_absolute_url())\n\n    def decline_pressed(self, request, *args, **kwargs):\n        try:\n            with transaction.atomic():\n                self.method.perform_decline(request.user.as_participant())\n                messages.info(request, self.method.decline_success_message.format(shift=self.shift))\n        except ParticipationError as errors:\n            for error in errors:\n                messages.error(request, self.method.decline_error_message.format(error=error))\n        finally:\n            return redirect(self.shift.event.get_absolute_url())\n", "path": "ephios/event_management/signup.py"}], "after_files": [{"content": "import functools\nimport json\nfrom argparse import Namespace\nfrom dataclasses import dataclass, field\nfrom datetime import date\nfrom typing import List, Optional\n\nimport django.dispatch\nfrom django import forms\nfrom django.contrib import messages\nfrom django.contrib.auth import get_user_model\nfrom django.core.exceptions import ValidationError\nfrom django.db import transaction\nfrom django.db.models import QuerySet\nfrom django.shortcuts import redirect\nfrom django.template import Context, Template\nfrom django.template.defaultfilters import yesno\nfrom django.utils import formats, timezone\nfrom django.utils.functional import cached_property\nfrom django.utils.translation import gettext_lazy as _\nfrom django.views import View\n\nfrom ephios.event_management.models import AbstractParticipation, LocalParticipation, Shift\nfrom ephios.extra.widgets import CustomSplitDateTimeWidget\nfrom ephios.user_management.models import Qualification\n\nregister_signup_methods = django.dispatch.Signal(providing_args=[])\n\n\ndef all_signup_methods():\n    for _, methods in register_signup_methods.send(None):\n        yield from methods\n\n\ndef signup_method_from_slug(slug, shift=None):\n    for method in all_signup_methods():\n        if method.slug == slug:\n            return method(shift)\n    raise ValueError(_(\"Signup Method '{slug}' was not found.\").format(slug=slug))\n\n\n@dataclass(frozen=True)\nclass AbstractParticipant:\n    first_name: str\n    last_name: str\n    qualifications: QuerySet = field(hash=False)\n    date_of_birth: date\n    email: Optional[str]  # if set to None, no notifications are sent\n\n    def get_age(self, today: date = None):\n        today, born = today or date.today(), self.date_of_birth\n        return today.year - born.year - ((today.month, today.day) < (born.month, born.day))\n\n    def __str__(self):\n        return f\"{self.first_name} {self.last_name}\"\n\n    def new_participation(self, shift):\n        raise NotImplementedError\n\n    def participation_for(self, shift):\n        \"\"\"Return the participation object for a shift. Return None if it does not exist.\"\"\"\n        raise NotImplementedError\n\n    def collect_all_qualifications(self):\n        \"\"\"We collect using breath first search with one query for every layer of inclusion.\"\"\"\n        all_qualifications = set(self.qualifications)\n        current = self.qualifications\n        while current:\n            next = (\n                Qualification.objects.filter(included_by__in=current)\n                .exclude(id__in=(q.id for q in all_qualifications))\n                .distinct()\n            )\n            all_qualifications |= set(next)\n            current = next\n        return all_qualifications\n\n    def has_qualifications(self, qualifications):\n        return set(qualifications) <= self.collect_all_qualifications()\n\n\n@dataclass(frozen=True)\nclass LocalUserParticipant(AbstractParticipant):\n    user: get_user_model()\n\n    def new_participation(self, shift):\n        return LocalParticipation(shift=shift, user=self.user)\n\n    def participation_for(self, shift):\n        try:\n            return LocalParticipation.objects.get(shift=shift, user=self.user)\n        except LocalParticipation.DoesNotExist:\n            return None\n\n\nclass ParticipationError(ValidationError):\n    pass\n\n\nclass ConfigurationForm(forms.Form):\n    pass\n\n\ndef check_event_is_active(method, participant):\n    if not method.shift.event.active:\n        return ParticipationError(_(\"The event is not active.\"))\n\n\ndef check_participation_state_for_signup(method, participant):\n    participation = participant.participation_for(method.shift)\n    if participation is not None:\n        if participation.state == AbstractParticipation.States.REQUESTED:\n            return ParticipationError(\n                _(\"You have already requested your participation for {shift}\").format(\n                    shift=method.shift\n                )\n            )\n        elif participation.state == AbstractParticipation.States.CONFIRMED:\n            return ParticipationError(\n                _(\"You are already signed up for {shift}.\").format(shift=method.shift)\n            )\n        elif participation.state == AbstractParticipation.States.RESPONSIBLE_REJECTED:\n            return ParticipationError(\n                _(\"You are rejected from {shift}.\").format(shift=method.shift)\n            )\n\n\ndef check_participation_state_for_decline(method, participant):\n    participation = participant.participation_for(method.shift)\n    if participation is not None:\n        if (\n            participation.state == AbstractParticipation.States.CONFIRMED\n            and not method.configuration.user_can_decline_confirmed\n        ):\n            return ParticipationError(\n                _(\"You are bindingly signed up for {shift}.\").format(shift=method.shift)\n            )\n        elif participation.state == AbstractParticipation.States.RESPONSIBLE_REJECTED:\n            return ParticipationError(\n                _(\"You are rejected from {shift}.\").format(shift=method.shift)\n            )\n        elif participation.state == AbstractParticipation.States.USER_DECLINED:\n            return ParticipationError(\n                _(\"You have already declined participating in {shift}.\").format(shift=method.shift)\n            )\n\n\ndef check_inside_signup_timeframe(method, participant):\n    last_time = method.shift.end_time\n    if method.configuration.signup_until is not None:\n        last_time = min(last_time, method.configuration.signup_until)\n    if timezone.now() > last_time:\n        return ParticipationError(_(\"The signup period is over.\"))\n\n\ndef check_participant_age(method, participant):\n    minimum_age = method.configuration.minimum_age\n    day = method.shift.start_time.date()\n    if minimum_age is not None and participant.get_age(day) < minimum_age:\n        return ParticipationError(\n            _(\"You are too young. The minimum age is {age}.\").format(age=minimum_age)\n        )\n\n\nclass BaseSignupMethod:\n    slug = \"abstract\"\n    verbose_name = \"abstract\"\n    description = \"\"\"\"\"\"\n\n    # use _ == gettext_lazy!\n    registration_button_text = _(\"Sign up\")\n    signup_success_message = _(\"You have successfully signed up for {shift}.\")\n    signup_error_message = _(\"Signing up failed: {error}\")\n    decline_success_message = _(\"You have successfully declined {shift}.\")\n    decline_error_message = _(\"Declining failed: {error}\")\n\n    def __init__(self, shift):\n        self.shift = shift\n        self.configuration = Namespace(\n            **{name: config[\"default\"] for name, config in self.get_configuration_fields().items()}\n        )\n        if shift is not None:\n            for key, value in shift.signup_configuration.items():\n                setattr(self.configuration, key, value)\n\n    @property\n    def signup_view_class(self):\n        return BaseSignupView\n\n    @cached_property\n    def signup_view(self):\n        return self.signup_view_class.as_view(method=self, shift=self.shift)\n\n    @property\n    def signup_checkers(self):\n        return [\n            check_event_is_active,\n            check_participation_state_for_signup,\n            check_inside_signup_timeframe,\n            check_participant_age,\n        ]\n\n    @property\n    def decline_checkers(self):\n        return [\n            check_event_is_active,\n            check_participation_state_for_decline,\n            check_inside_signup_timeframe,\n        ]\n\n    @functools.lru_cache()\n    def get_signup_errors(self, participant) -> List[ParticipationError]:\n        return [\n            error\n            for checker in self.signup_checkers\n            if (error := checker(self, participant)) is not None\n        ]\n\n    @functools.lru_cache()\n    def get_decline_errors(self, participant):\n        return [\n            error\n            for checker in self.decline_checkers\n            if (error := checker(self, participant)) is not None\n        ]\n\n    def can_decline(self, participant):\n        return not self.get_decline_errors(participant)\n\n    def can_sign_up(self, participant):\n        return not self.get_signup_errors(participant)\n\n    def get_participation_for(self, participant):\n        return participant.participation_for(self.shift) or participant.new_participation(\n            self.shift\n        )\n\n    def perform_signup(self, participant: AbstractParticipant, **kwargs):\n        \"\"\"\n        Configure a participation object for the given participant according to the method's configuration.\n        `kwargs` may contain further instructions from a e.g. a form.\n        \"\"\"\n        if errors := self.get_signup_errors(participant):\n            raise ParticipationError(errors)\n        return self.get_participation_for(participant)\n\n    def perform_decline(self, participant, **kwargs):\n        \"\"\"Create and configure a declining participation object for the given participant. `kwargs` may contain further instructions from a e.g. a form.\"\"\"\n        if errors := self.get_decline_errors(participant):\n            raise ParticipationError(errors)\n        participation = self.get_participation_for(participant)\n        participation.state = AbstractParticipation.States.USER_DECLINED\n        participation.save()\n        return participation\n\n    def get_configuration_fields(self):\n        return {\n            \"minimum_age\": {\n                \"formfield\": forms.IntegerField(required=False),\n                \"default\": 16,\n                \"publish_with_label\": _(\"Minimum age\"),\n            },\n            \"signup_until\": {\n                \"formfield\": forms.SplitDateTimeField(\n                    required=False, widget=CustomSplitDateTimeWidget\n                ),\n                \"default\": None,\n                \"publish_with_label\": _(\"Signup until\"),\n                \"format\": functools.partial(formats.date_format, format=\"SHORT_DATETIME_FORMAT\"),\n            },\n            \"user_can_decline_confirmed\": {\n                \"formfield\": forms.BooleanField(\n                    label=_(\"Confirmed users can decline by themselves\"),\n                    required=False,\n                    help_text=_(\"only if the signup timeframe has not ended\"),\n                ),\n                \"default\": False,\n                \"publish_with_label\": _(\"Can decline after confirmation\"),\n                \"format\": yesno,\n            },\n        }\n\n    def get_signup_info(self):\n        \"\"\"\n        Return key/value pairs about the configuration to show in the shift info box.\n        \"\"\"\n        fields = self.get_configuration_fields()\n        return {\n            label: field.get(\"format\", str)(value)\n            for key, field in fields.items()\n            if (label := field.get(\"publish_with_label\", False))\n            and (value := getattr(self.configuration, key))\n        }\n\n    def render_shift_state(self, request):\n        \"\"\"\n        Render html that will be shown in the shift info box.\n        Use it to inform about the current state of the shift and participations.\n        \"\"\"\n        return \"\"\n\n    def get_configuration_form(self, *args, **kwargs):\n        if self.shift is not None:\n            kwargs.setdefault(\"initial\", self.configuration.__dict__)\n        form = ConfigurationForm(*args, **kwargs)\n        for name, config in self.get_configuration_fields().items():\n            form.fields[name] = config[\"formfield\"]\n        return form\n\n    def render_configuration_form(self, form=None, *args, **kwargs):\n        form = form or self.get_configuration_form(*args, **kwargs)\n        template = Template(\n            template_string=\"{% load bootstrap4 %}{% bootstrap_form form %}\"\n        ).render(Context({\"form\": form}))\n        return template\n\n\nclass BaseSignupView(View):\n    shift: Shift = ...\n    method: BaseSignupMethod = ...\n\n    def dispatch(self, request, *args, **kwargs):\n        if (choice := request.POST.get(\"signup_choice\")) is not None:\n            if choice == \"sign_up\":\n                return self.signup_pressed(request, *args, **kwargs)\n            elif choice == \"decline\":\n                return self.decline_pressed(request, *args, **kwargs)\n            else:\n                raise ValueError(\n                    _(\"'{choice}' is not a valid signup action.\").format(choice=choice)\n                )\n        return super().dispatch(request, *args, **kwargs)\n\n    def signup_pressed(self, request, *args, **kwargs):\n        try:\n            with transaction.atomic():\n                self.method.perform_signup(request.user.as_participant())\n                messages.success(\n                    request,\n                    self.method.signup_success_message.format(shift=self.shift),\n                )\n        except ParticipationError as errors:\n            for error in errors:\n                messages.error(request, self.method.signup_error_message.format(error=error))\n        finally:\n            return redirect(self.shift.event.get_absolute_url())\n\n    def decline_pressed(self, request, *args, **kwargs):\n        try:\n            with transaction.atomic():\n                self.method.perform_decline(request.user.as_participant())\n                messages.info(request, self.method.decline_success_message.format(shift=self.shift))\n        except ParticipationError as errors:\n            for error in errors:\n                messages.error(request, self.method.decline_error_message.format(error=error))\n        finally:\n            return redirect(self.shift.event.get_absolute_url())\n", "path": "ephios/event_management/signup.py"}]}