Skip to content

Commit 170e708

Browse files
lukasrad02felixrindtjeriox
authored
Add questionnaires (#1559)
Co-authored-by: Felix Rindt <[email protected]> Co-authored-by: Julian Baumann <[email protected]>
1 parent 6d4b3a2 commit 170e708

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+3083
-471
lines changed

ephios/core/signals.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
- ``HTML_SHIFT_INFO``: Add HTML to the event detail page shift box. Comes with a ``shift`` kwarg.
2727
- ``HTML_HOMEPAGE_INFO``: Add HTML to the homepage content area.
2828
- ``HTML_PERSONAL_DATA_PAGE``: Add HTML to the settings "personal data" page of the logged-in user.
29+
- ``HTML_DISPOSITION_PARTICIPATION``: Add HTML to the expandable details view of a participation form in the disposition view. Comes with a ``participation`` kwarg.
2930
"""
3031

3132
HTML_HEAD = sys.intern("head")
@@ -34,6 +35,7 @@
3435
HTML_SHIFT_INFO = sys.intern("shift_info")
3536
HTML_HOMEPAGE_INFO = sys.intern("homepage_info")
3637
HTML_PERSONAL_DATA_PAGE = sys.intern("personal_data_page")
38+
HTML_DISPOSITION_PARTICIPATION = sys.intern("disposition_participation")
3739

3840

3941
register_consequence_handlers = PluginSignal()
@@ -103,6 +105,21 @@
103105
"""
104106

105107

108+
shift_action = PluginSignal()
109+
"""
110+
This signal is sent out to collect additional actions that managers can perform on on a shift. For
111+
each action, a button will be displayed in the shift card next to the disposition button. Receivers
112+
of the signal will receive the ``shift`` and ``request`` and are expected to return an array of
113+
``{label: str, url: str}`` dicts, representing the available actions.
114+
The buttons will only be shown to responsibles of the respective shift.
115+
"""
116+
117+
shift_copy = PluginSignal()
118+
"""
119+
This signal is set out after a shift got copied to allow plugins to copy related data as well.
120+
Receivers will receive the original ``shift`` and a list of the created ``copies``.
121+
"""
122+
106123
shift_forms = PluginSignal()
107124
"""
108125
This signal is sent out to get a list of form instances to show on the shift create and update views.
@@ -111,6 +128,59 @@
111128
If all forms are valid, `save` will be called on your form.
112129
"""
113130

131+
signup_form_fields = PluginSignal()
132+
"""
133+
This signal is sent out to get a list of form fields to show on the signup view, especially to collect
134+
user input for shift structures. Receivers will receive the ``shift``, ``participant``, ``participation``,
135+
and ``signup_choice`` and should return a dict in the form ``{ 'fieldname1': {
136+
'label':, ...,
137+
'help_text':, ...,
138+
'default': ...,
139+
'required': ..., # meaning a non-Falsey value must be provided
140+
'form_class': ...,
141+
'form_kwargs': ...,
142+
'serializer_class': ...,
143+
'serializer_kwargs': ...,
144+
}, 'fieldname2: { ... } }``.
145+
``label`` (only form), ``help_text`` (only form), ``default`` (only form, as ``initial``), and ``required``
146+
(form and serializer) will be applied to the kwargs dicts for convenience. Values specified directly
147+
as kwarg have precedence.
148+
"""
149+
150+
151+
def collect_signup_form_fields(shift, participant, participation, signup_choice):
152+
responses = signup_form_fields.send(
153+
sender=None,
154+
shift=shift,
155+
participant=participant,
156+
participation=participation,
157+
signup_choice=signup_choice,
158+
)
159+
for _, additional_fields in responses:
160+
for fieldname, field in additional_fields.items():
161+
yield fieldname, {
162+
**field,
163+
"form_kwargs": {
164+
"label": field["label"],
165+
"help_text": field.get("help_text", ""),
166+
"initial": field["default"],
167+
"required": field["required"],
168+
**field["form_kwargs"],
169+
},
170+
"serializer_kwargs": {
171+
"required": field["required"],
172+
**field["serializer_kwargs"],
173+
},
174+
}
175+
176+
177+
signup_save = PluginSignal()
178+
"""
179+
This signal is sent out to when a signup is created or modified to allow plugins to handle additional
180+
user input. Receivers will receive the ``shift``, ``participant``, ``participation``, ``signup_choice``,
181+
and ``cleaned_data``.
182+
"""
183+
114184
register_notification_types = PluginSignal()
115185
"""
116186
This signal is sent out to get all notification types that can be sent out to a user or participant.
@@ -150,7 +220,7 @@
150220
will contain a list of event ids on which the action should be performed.
151221
"""
152222

153-
event_action = PluginSignal()
223+
event_menu = PluginSignal()
154224
"""
155225
This signal is sent out to get a list of actions that a user can perform on a single event. The actions are
156226
displayed in the dropdown menu on the event detail view.
@@ -276,6 +346,20 @@ def update_last_run_periodic_call(sender, **kwargs):
276346
LastRunPeriodicCall.set_last_call(timezone.now())
277347

278348

349+
@receiver(signup_form_fields, dispatch_uid="ephios.core.signals.provide_structure_form_fields")
350+
def provide_structure_form_fields(
351+
sender, shift, participant, participation, signup_choice, **kwargs
352+
):
353+
return shift.structure.get_signup_form_fields(participant, participation, signup_choice)
354+
355+
356+
@receiver(signup_save, dispatch_uid="ephios.core.signals.structure_signup_save")
357+
def structure_signup_save(
358+
sender, shift, participant, participation, signup_choice, cleaned_data, **kwargs
359+
):
360+
shift.structure.save_signup(participant, participation, signup_choice, cleaned_data)
361+
362+
279363
periodic_signal.connect(
280364
send_participation_finished, dispatch_uid="ephios.core.signals.send_participation_finished"
281365
)

ephios/core/signup/disposition.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django import forms
2+
from django.db import transaction
23
from django.http import Http404
34
from django.shortcuts import redirect
45
from django.utils.functional import cached_property
@@ -23,6 +24,7 @@
2324
ResponsibleParticipationStateChangeNotification,
2425
)
2526
from ephios.core.signup.forms import BaseParticipationForm
27+
from ephios.extra.database import OF_SELF
2628
from ephios.extra.mixins import CustomPermissionRequiredMixin
2729

2830

@@ -166,9 +168,12 @@ def post(self, request, *args, **kwargs):
166168
)
167169
if form.is_valid():
168170
user: UserProfile = form.cleaned_data["user"]
169-
instance = shift.signup_flow.get_or_create_participation_for(user.as_participant())
170-
instance.state = AbstractParticipation.States.GETTING_DISPATCHED
171-
instance.save()
171+
with transaction.atomic():
172+
# lock user row in case to avoid duplicate participation objects
173+
UserProfile.objects.select_for_update(of=OF_SELF).get(pk=user.pk)
174+
instance = shift.signup_flow.get_or_create_participation_for(user.as_participant())
175+
instance.state = AbstractParticipation.States.GETTING_DISPATCHED
176+
instance.save()
172177

173178
DispositionParticipationFormset = get_disposition_formset(
174179
self.object.structure.disposition_participation_form_class

ephios/core/signup/forms.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
from crispy_forms.helper import FormHelper
33
from crispy_forms.layout import HTML, Field, Layout
44
from django import forms
5-
from django.db import transaction
5+
from django.db import models, transaction
66
from django.utils.formats import date_format
77
from django.utils.timezone import localtime
88
from django.utils.translation import gettext_lazy as _
99

1010
from ephios.core.models import AbstractParticipation, Shift
1111
from ephios.core.models.events import ParticipationComment
12+
from ephios.core.signals import collect_signup_form_fields
1213
from ephios.core.signup.flow.participant_validation import get_conflicting_participations
1314
from ephios.core.signup.participants import AbstractParticipant
1415
from ephios.core.widgets import PreviousCommentWidget
@@ -126,14 +127,15 @@ def get_customization_notification_info(self):
126127
return info
127128

128129

129-
class BaseSignupForm(BaseParticipationForm):
130+
class SignupForm(BaseParticipationForm):
131+
class SignupChoices(models.TextChoices):
132+
SIGNUP = "sign_up", _("Sign up")
133+
CUSTOMIZE = "customize", _("Customize")
134+
DECLINE = "decline", _("Decline")
135+
130136
signup_choice = forms.ChoiceField(
131137
label=_("Signup choice"),
132-
choices=[
133-
("sign_up", _("Sign up")),
134-
("customize", _("Customize")),
135-
("decline", _("Decline")),
136-
],
138+
choices=SignupChoices.choices,
137139
widget=forms.HiddenInput,
138140
required=True,
139141
)
@@ -164,7 +166,7 @@ def _get_buttons(self):
164166
if self.shift.signup_flow.get_validator(self.participant).can_decline():
165167
buttons.append(
166168
HTML(
167-
f'<button class="btn btn-secondary mt-1 ms-1 float-end" type="submit" name="signup_choice" value="decline">{_("Decline")}</button>'
169+
f'<button class="btn btn-secondary mt-1 ms-1 float-end" type="submit" name="signup_choice" value="decline" formnovalidate>{_("Decline")}</button>'
168170
)
169171
)
170172
return buttons
@@ -173,6 +175,7 @@ def __init__(self, *args, **kwargs):
173175
self.shift: Shift = kwargs.pop("shift")
174176
self.participant: AbstractParticipant = kwargs.pop("participant")
175177
super().__init__(*args, **kwargs)
178+
self._collect_fields()
176179
self.helper = FormHelper()
177180
self.helper.layout = Layout(
178181
self._get_field_layout(),
@@ -185,6 +188,12 @@ def __init__(self, *args, **kwargs):
185188
self.fields["individual_start_time"].disabled = True
186189
self.fields["individual_end_time"].disabled = True
187190

191+
def _collect_fields(self):
192+
for fieldname, field in collect_signup_form_fields(
193+
self.shift, self.participant, self.instance, self.data.get("signup_choice")
194+
):
195+
self.fields[fieldname] = field["form_class"](**field.get("form_kwargs", {}))
196+
188197
def clean(self):
189198
cleaned_data = super().clean()
190199
self._validate_conflicting_participations()

ephios/core/signup/structure/abstract.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import logging
22
from abc import ABC
3+
from typing import Any
34

5+
from ephios.core.models.events import AbstractParticipation
6+
from ephios.core.signup.participants import AbstractParticipant
47
from ephios.core.signup.stats import SignupStats
58

69
logger = logging.getLogger(__name__)
@@ -49,12 +52,22 @@ def disposition_participation_form_class(self):
4952
"""
5053
raise NotImplementedError()
5154

52-
@property
53-
def signup_form_class(self):
54-
"""
55-
This form will be used for participations in signup.
56-
"""
57-
raise NotImplementedError()
55+
def get_signup_form_fields(
56+
self,
57+
participant: AbstractParticipant,
58+
participation: AbstractParticipation,
59+
signup_choice,
60+
):
61+
pass
62+
63+
def save_signup(
64+
self,
65+
participant: AbstractParticipant,
66+
participation: AbstractParticipation,
67+
signup_choice,
68+
cleaned_data: dict[str, Any],
69+
):
70+
pass
5871

5972
def get_configuration_form(self, *args, **kwargs):
6073
"""

ephios/core/signup/structure/base.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from ephios.core.models import AbstractParticipation
99
from ephios.core.signup.disposition import BaseDispositionParticipationForm
10-
from ephios.core.signup.forms import BaseSignupForm, SignupConfigurationForm
10+
from ephios.core.signup.forms import SignupConfigurationForm
1111
from ephios.core.signup.stats import SignupStats
1212
from ephios.core.signup.structure.abstract import AbstractShiftStructure
1313
from ephios.extra.utils import format_anything
@@ -24,10 +24,6 @@ class BaseShiftStructure(AbstractShiftStructure):
2424
def disposition_participation_form_class(self):
2525
return BaseDispositionParticipationForm
2626

27-
@property
28-
def signup_form_class(self):
29-
return BaseSignupForm
30-
3127
@property
3228
def configuration_form_class(self):
3329
return SignupConfigurationForm
@@ -36,6 +32,9 @@ def configuration_form_class(self):
3632
def shift_state_template_name(self):
3733
raise NotImplementedError()
3834

35+
def get_signup_form_fields(self, participant, participation, signup_choice):
36+
return {}
37+
3938
def get_participant_count_bounds(self):
4039
return None, None
4140

0 commit comments

Comments
 (0)