diff --git a/ephios/event_management/models.py b/ephios/event_management/models.py
index f420d47c7..5070b30aa 100644
--- a/ephios/event_management/models.py
+++ b/ephios/event_management/models.py
@@ -17,6 +17,7 @@
)
from django.utils import formats
from django.utils.translation import gettext_lazy as _
+from guardian.shortcuts import assign_perm
from polymorphic.models import PolymorphicModel
from ephios import settings
@@ -24,6 +25,7 @@
if TYPE_CHECKING:
from ephios.event_management.signup import AbstractParticipant
+ from ephios.user_management.models import UserProfile
class ActiveManager(Manager):
@@ -93,9 +95,10 @@ class States(models.IntegerChoices):
CONFIRMED = 1, _("confirmed")
USER_DECLINED = 2, _("declined by user")
RESPONSIBLE_REJECTED = 3, _("rejected by responsible")
+ RESPONSIBLE_ADDED = 4, _("added by responsible")
shift = ForeignKey("Shift", on_delete=models.CASCADE, verbose_name=_("shift"))
- state = IntegerField(_("state"), choices=States.choices, default=States.REQUESTED)
+ state = IntegerField(_("state"), choices=States.choices)
data = models.JSONField(default=dict)
@property
@@ -160,7 +163,20 @@ def __str__(self):
class LocalParticipation(AbstractParticipation):
- user = ForeignKey(get_user_model(), on_delete=models.CASCADE)
+ user: "UserProfile" = ForeignKey(get_user_model(), on_delete=models.CASCADE)
+
+ def save(self, *args, **kwargs):
+ super().save(*args, **kwargs)
+ if (
+ not self.user.has_perm("event_management.view_event", obj=self.shift.event)
+ and self.state != self.States.RESPONSIBLE_ADDED
+ ):
+ # If dispatched by a responsible, the user should be able to view
+ # the event, if not already permitted through its group.
+ # Currently, this permission does not get removed automatically.
+ assign_perm(
+ "event_management.view_event", user_or_group=self.user, obj=self.shift.event
+ )
@property
def participant(self):
diff --git a/ephios/event_management/signup.py b/ephios/event_management/signup.py
index 22e8e7103..13c9db756 100644
--- a/ephios/event_management/signup.py
+++ b/ephios/event_management/signup.py
@@ -21,6 +21,7 @@
from ephios.event_management.models import AbstractParticipation, LocalParticipation, Shift
from ephios.extra.widgets import CustomSplitDateTimeWidget
+from ephios.plugins.basesignup.signup.disposition import BaseDispositionParticipationForm
from ephios.user_management.models import Qualification
register_signup_methods = django.dispatch.Signal()
@@ -217,6 +218,13 @@ def verbose_name(self):
decline_success_message = _("You have successfully declined {shift}.")
decline_error_message = _("Declining failed: {error}")
+ """
+ This form will be used for participations in disposition.
+ Set to None if you don't want to support the default disposition.
+ """
+ disposition_participation_form_class = BaseDispositionParticipationForm
+ disposition_show_requested_state = True
+
def __init__(self, shift):
self.shift = shift
self.configuration = Namespace(
diff --git a/ephios/plugins/basesignup/signals.py b/ephios/plugins/basesignup/signals.py
index 238923ab5..c9e100d4f 100644
--- a/ephios/plugins/basesignup/signals.py
+++ b/ephios/plugins/basesignup/signals.py
@@ -1,11 +1,9 @@
from django.dispatch import receiver
from ephios.event_management.signup import register_signup_methods
+from ephios.plugins.basesignup.signup.instant import InstantConfirmationSignupMethod
+from ephios.plugins.basesignup.signup.request_confirm import RequestConfirmSignupMethod
from ephios.plugins.basesignup.signup.section_based import SectionBasedSignupMethod
-from ephios.plugins.basesignup.signup.simple import (
- InstantConfirmationSignupMethod,
- RequestConfirmSignupMethod,
-)
@receiver(
diff --git a/ephios/plugins/basesignup/signup/__init__.py b/ephios/plugins/basesignup/signup/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/ephios/plugins/basesignup/signup/common.py b/ephios/plugins/basesignup/signup/common.py
new file mode 100644
index 000000000..288052988
--- /dev/null
+++ b/ephios/plugins/basesignup/signup/common.py
@@ -0,0 +1,43 @@
+from django import forms
+from django.utils.translation import gettext_lazy as _
+from django_select2.forms import Select2MultipleWidget
+
+from ephios.event_management.signup import BaseSignupMethod, ParticipationError
+from ephios.user_management.models import Qualification
+
+
+class SimpleQualificationsRequiredSignupMethod(BaseSignupMethod):
+ # pylint: disable=abstract-method
+ def __init__(self, shift):
+ super().__init__(shift)
+ if shift is not None:
+ self.configuration.required_qualifications = Qualification.objects.filter(
+ pk__in=self.configuration.required_qualification_ids
+ )
+
+ @property
+ def signup_checkers(self):
+ return super().signup_checkers + [self.check_qualification]
+
+ @staticmethod
+ def check_qualification(method, participant):
+ if not participant.has_qualifications(method.configuration.required_qualifications):
+ return ParticipationError(_("You are not qualified."))
+
+ def get_configuration_fields(self):
+ return {
+ **super().get_configuration_fields(),
+ "required_qualification_ids": {
+ "formfield": forms.ModelMultipleChoiceField(
+ label=_("Required Qualifications"),
+ queryset=Qualification.objects.all(),
+ widget=Select2MultipleWidget,
+ required=False,
+ ),
+ "default": [],
+ "publish_with_label": _("Required Qualification"),
+ "format": lambda ids: ", ".join(
+ Qualification.objects.filter(id__in=ids).values_list("title", flat=True)
+ ),
+ },
+ }
diff --git a/ephios/plugins/basesignup/signup/disposition.py b/ephios/plugins/basesignup/signup/disposition.py
new file mode 100644
index 000000000..377cfe2fb
--- /dev/null
+++ b/ephios/plugins/basesignup/signup/disposition.py
@@ -0,0 +1,150 @@
+from django import forms
+from django.http import Http404
+from django.shortcuts import redirect
+from django.utils.translation import gettext_lazy as _
+from django.views import View
+from django.views.generic import TemplateView
+from django.views.generic.base import TemplateResponseMixin
+from django.views.generic.detail import SingleObjectMixin
+from django_select2.forms import ModelSelect2Widget
+
+from ephios.event_management.models import AbstractParticipation, Shift
+from ephios.extra.permissions import CustomPermissionRequiredMixin
+from ephios.user_management.models import UserProfile
+
+
+class BaseDispositionParticipationForm(forms.ModelForm):
+ disposition_participation_template = "basesignup/common/fragment_participant.html"
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ try:
+ self.shift = self.instance.shift
+ except AttributeError as e:
+ raise ValueError(f"{type(self)} must be initialized with an instance.") from e
+
+ class Meta:
+ model = AbstractParticipation
+ fields = ["state"]
+ widgets = dict(state=forms.HiddenInput(attrs={"class": "state-input"}))
+
+
+def get_disposition_formset(form):
+ return forms.modelformset_factory(
+ model=AbstractParticipation,
+ form=form,
+ extra=0,
+ can_order=False,
+ can_delete=True,
+ )
+
+
+def addable_users(shift):
+ """
+ Return queryset of user objects that can be added to the shift.
+ This also includes users that already have a participation, as that might have gotten removed in JS.
+
+ This also includes users that can normally not see the event. The permission will be added accordingly.
+ If needed, this method could be moved to signup methods.
+ """
+ return UserProfile.objects.all()
+
+
+class AddUserForm(forms.Form):
+ user = forms.ModelChoiceField(
+ widget=ModelSelect2Widget(
+ model=UserProfile,
+ search_fields=["first_name__icontains", "last_name__icontains"],
+ attrs={"form": "add-user-form"},
+ ),
+ queryset=UserProfile.objects.none(), # set using __init__
+ )
+
+ def __init__(self, user_queryset, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields["user"].queryset = user_queryset
+
+
+class DispositionBaseViewMixin(CustomPermissionRequiredMixin, SingleObjectMixin):
+ permission_required = "event_management.change_event"
+ model = Shift
+
+ def setup(self, request, *args, **kwargs):
+ super().setup(request, *args, **kwargs)
+ self.object: Shift = self.get_object()
+
+ def dispatch(self, request, *args, **kwargs):
+ if self.object.signup_method.disposition_participation_form_class is None:
+ raise Http404(_("This signup method does not support disposition."))
+ return super().dispatch(request, *args, **kwargs)
+
+ def get_permission_object(self):
+ return self.object.event
+
+
+class AddUserView(DispositionBaseViewMixin, TemplateResponseMixin, View):
+ def get_template_names(self):
+ return [
+ self.object.signup_method.disposition_participation_form_class.disposition_participation_template
+ ]
+
+ def post(self, request, *args, **kwargs):
+ shift = self.object
+ form = AddUserForm(
+ user_queryset=addable_users(shift),
+ data=request.POST,
+ )
+ if form.is_valid():
+ user: UserProfile = form.cleaned_data["user"]
+ instance = shift.signup_method.get_participation_for(user.as_participant())
+ instance.state = AbstractParticipation.States.RESPONSIBLE_ADDED
+ instance.save()
+
+ DispositionParticipationFormset = get_disposition_formset(
+ self.object.signup_method.disposition_participation_form_class
+ )
+ formset = DispositionParticipationFormset(
+ queryset=self.object.participations,
+ prefix="participations",
+ )
+ form = next(filter(lambda form: form.instance.id == instance.id, formset))
+ return self.render_to_response({"form": form})
+ raise Http404("User does not exist")
+
+
+class DispositionView(DispositionBaseViewMixin, TemplateView):
+ template_name = "basesignup/common/disposition.html"
+
+ def get_formset(self):
+ DispositionParticipationFormset = get_disposition_formset(
+ self.object.signup_method.disposition_participation_form_class
+ )
+ formset = DispositionParticipationFormset(
+ self.request.POST or None,
+ queryset=self.object.participations,
+ prefix="participations",
+ )
+ return formset
+
+ def post(self, request, *args, **kwargs):
+ formset = self.get_formset()
+ if formset.is_valid():
+ formset.save()
+ self.object.participations.filter(
+ state=AbstractParticipation.States.RESPONSIBLE_ADDED
+ ).delete()
+ return redirect(self.object.event.get_absolute_url())
+ return self.get(request, *args, **kwargs, formset=formset)
+
+ def get_context_data(self, **kwargs):
+ kwargs.setdefault("formset", self.get_formset())
+ kwargs.setdefault("states", AbstractParticipation.States)
+ kwargs.setdefault(
+ "participant_template",
+ self.object.signup_method.disposition_participation_form_class.disposition_participation_template,
+ )
+ kwargs.setdefault(
+ "add_user_form",
+ AddUserForm(user_queryset=addable_users(self.object)),
+ )
+ return super().get_context_data(**kwargs)
diff --git a/ephios/plugins/basesignup/signup/instant.py b/ephios/plugins/basesignup/signup/instant.py
new file mode 100644
index 000000000..609974071
--- /dev/null
+++ b/ephios/plugins/basesignup/signup/instant.py
@@ -0,0 +1,56 @@
+from django import forms
+from django.template.loader import get_template
+from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
+
+from ephios.event_management.models import AbstractParticipation
+from ephios.event_management.signup import ParticipationError
+from ephios.plugins.basesignup.signup.common import SimpleQualificationsRequiredSignupMethod
+
+
+class InstantConfirmationSignupMethod(SimpleQualificationsRequiredSignupMethod):
+ slug = "instant_confirmation"
+ verbose_name = _("Instant Confirmation")
+ description = _("""This method instantly confirms every signup after it was requested.""")
+ disposition_show_requested_state = False
+
+ @property
+ def signup_checkers(self):
+ return super().signup_checkers + [self.check_maximum_number_of_participants]
+
+ @staticmethod
+ def check_maximum_number_of_participants(method, participant):
+ if method.configuration.maximum_number_of_participants is not None:
+ current_count = AbstractParticipation.objects.filter(
+ shift=method.shift, state=AbstractParticipation.States.CONFIRMED
+ ).count()
+ if current_count >= method.configuration.maximum_number_of_participants:
+ return ParticipationError(_("The maximum number of participants is reached."))
+
+ def get_configuration_fields(self):
+ return {
+ **super().get_configuration_fields(),
+ "maximum_number_of_participants": {
+ "formfield": forms.IntegerField(min_value=1, required=False),
+ "default": None,
+ "publish_with_label": _("Maximum number of participants"),
+ },
+ }
+
+ def render_shift_state(self, request):
+ return get_template("basesignup/instant/fragment_state.html").render(
+ {
+ "shift": self.shift,
+ "disposition_url": (
+ reverse("basesignup:shift_disposition", kwargs=dict(pk=self.shift.pk))
+ if request.user.has_perm("event_management.change_event", obj=self.shift.event)
+ else None
+ ),
+ }
+ )
+
+ def perform_signup(self, participant, **kwargs):
+ participation = super().perform_signup(participant, **kwargs)
+ participation.state = AbstractParticipation.States.CONFIRMED
+ participation.save()
+ return participation
diff --git a/ephios/plugins/basesignup/signup/request_confirm.py b/ephios/plugins/basesignup/signup/request_confirm.py
new file mode 100644
index 000000000..3e2859b19
--- /dev/null
+++ b/ephios/plugins/basesignup/signup/request_confirm.py
@@ -0,0 +1,100 @@
+from django import forms
+from django.shortcuts import redirect
+from django.template.loader import get_template
+from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
+from django.views.generic import TemplateView
+from django.views.generic.detail import SingleObjectMixin
+
+from ephios.event_management.models import AbstractParticipation, Shift
+from ephios.extra.permissions import CustomPermissionRequiredMixin
+from ephios.plugins.basesignup.signup.common import SimpleQualificationsRequiredSignupMethod
+
+DispositionParticipationFormset = forms.modelformset_factory(
+ model=AbstractParticipation,
+ fields=["state"],
+ extra=0,
+ can_order=False,
+ can_delete=False,
+ widgets={
+ "state": forms.HiddenInput(attrs={"class": "state-input"}),
+ },
+)
+
+
+class RequestConfirmDispositionView(CustomPermissionRequiredMixin, SingleObjectMixin, TemplateView):
+ model = Shift
+ permission_required = "event_management.change_event"
+ template_name = "basesignup/disposition.html"
+
+ def get_permission_object(self):
+ self.object: Shift = self.get_object()
+ return self.object.event
+
+ def get_formset(self):
+ return DispositionParticipationFormset(
+ self.request.POST or None, queryset=self.object.participations
+ )
+
+ def get(self, request, *args, **kwargs):
+ self.object: Shift = self.get_object()
+ return super().get(request, *args, **kwargs)
+
+ def post(self, request, *args, **kwargs):
+ self.object: Shift = self.get_object()
+ formset = self.get_formset()
+ if formset.is_valid():
+ formset.save()
+ return redirect(self.object.event.get_absolute_url())
+ return self.get(request, *args, **kwargs, formset=formset)
+
+ def get_context_data(self, **kwargs):
+ kwargs.setdefault("formset", self.get_formset())
+ kwargs.setdefault("states", AbstractParticipation.States)
+ kwargs.setdefault(
+ "participant_template", "basesignup/request_confirm/fragment_participant.html"
+ )
+ return super().get_context_data(**kwargs)
+
+
+class RequestConfirmSignupMethod(SimpleQualificationsRequiredSignupMethod):
+ slug = "request_confirm"
+ verbose_name = _("Request and confirm")
+ description = _(
+ """This method lets people request participation. Responsibles can then confirm the participation."""
+ )
+ registration_button_text = _("Request")
+ signup_success_message = _("You have successfully requested a participation for {shift}.")
+ signup_error_message = _("Requesting a participation failed: {error}")
+
+ def render_shift_state(self, request):
+ participations = self.shift.participations.filter(
+ state__in={
+ AbstractParticipation.States.REQUESTED,
+ AbstractParticipation.States.CONFIRMED,
+ }
+ )
+ return get_template("basesignup/request_confirm/fragment_state.html").render(
+ {
+ "shift": self.shift,
+ "requested_participants": (
+ p.participant
+ for p in participations.filter(state=AbstractParticipation.States.REQUESTED)
+ ),
+ "confirmed_participants": (
+ p.participant
+ for p in participations.filter(state=AbstractParticipation.States.CONFIRMED)
+ ),
+ "disposition_url": (
+ reverse("basesignup:shift_disposition", kwargs=dict(pk=self.shift.pk))
+ if request.user.has_perm("event_management.change_event", obj=self.shift.event)
+ else None
+ ),
+ }
+ )
+
+ def perform_signup(self, participant, **kwargs):
+ participation = super().perform_signup(participant, **kwargs)
+ participation.state = AbstractParticipation.States.REQUESTED
+ participation.save()
+ return participation
diff --git a/ephios/plugins/basesignup/signup/section_based.py b/ephios/plugins/basesignup/signup/section_based.py
index ce269bb4b..54d191414 100644
--- a/ephios/plugins/basesignup/signup/section_based.py
+++ b/ephios/plugins/basesignup/signup/section_based.py
@@ -10,18 +10,17 @@
from django.template.loader import get_template
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
-from django.views.generic import FormView, TemplateView
-from django.views.generic.detail import SingleObjectMixin
+from django.views.generic import FormView
from django_select2.forms import Select2MultipleWidget
-from ephios.event_management.models import AbstractParticipation, Shift
+from ephios.event_management.models import AbstractParticipation
from ephios.event_management.signup import (
AbstractParticipant,
BaseSignupMethod,
BaseSignupView,
ParticipationError,
)
-from ephios.extra.permissions import CustomPermissionRequiredMixin
+from ephios.plugins.basesignup.signup.disposition import BaseDispositionParticipationForm
from ephios.user_management.models import Qualification
@@ -34,25 +33,44 @@ def sections_participant_qualifies_for(sections, participant: AbstractParticipan
]
-class DispositionParticipationForm(forms.ModelForm):
+class SectionBasedDispositionParticipationForm(BaseDispositionParticipationForm):
+ disposition_participation_template = "basesignup/section_based/fragment_participant.html"
+
section = forms.ChoiceField(
- label=_("Section"), required=False # only required if participation is confirmed
+ label=_("Section"),
+ required=False, # only required if participation is confirmed
+ widget=forms.Select(
+ attrs={"data-show-for-state": str(AbstractParticipation.States.CONFIRMED)}
+ ),
)
- class Meta:
- model = AbstractParticipation
- fields = ["state"]
-
def __init__(self, **kwargs):
super().__init__(**kwargs)
- sections = self.instance.shift.signup_method.configuration.sections
- self.fields["section"].choices = [("", "---")] + [
- (section["uuid"], section["title"])
- for section in sections_participant_qualifies_for(
+ sections = self.shift.signup_method.configuration.sections
+ qualified_sections = list(
+ sections_participant_qualifies_for(
sections,
self.instance.participant,
)
+ )
+ unqualified_sections = [
+ section for section in sections if section not in qualified_sections
]
+ self.fields["section"].choices = [("", "---")]
+ if qualified_sections:
+ self.fields["section"].choices += [
+ (
+ _("qualified"),
+ [(section["uuid"], section["title"]) for section in qualified_sections],
+ )
+ ]
+ if unqualified_sections:
+ self.fields["section"].choices += [
+ (
+ _("unqualified"),
+ [(section["uuid"], section["title"]) for section in unqualified_sections],
+ )
+ ]
if preferred_section_uuid := self.instance.data.get("preferred_section_uuid"):
self.fields["section"].initial = preferred_section_uuid
self.preferred_section = next(
@@ -77,53 +95,6 @@ def save(self, commit=True):
super().save(commit)
-DispositionParticipationFormset = forms.modelformset_factory(
- model=AbstractParticipation,
- form=DispositionParticipationForm,
- extra=0,
- can_order=False,
- can_delete=False,
- widgets={
- "state": forms.HiddenInput(attrs={"class": "state-input"}),
- },
-)
-
-
-class SectionBasedDispositionView(CustomPermissionRequiredMixin, SingleObjectMixin, TemplateView):
- model = Shift
- permission_required = "event_management.change_event"
- template_name = "basesignup/disposition.html"
-
- def setup(self, request, *args, **kwargs):
- super().setup(request, *args, **kwargs)
- self.object: Shift = self.get_object()
-
- def get_permission_object(self):
- return self.object.event
-
- def get_formset(self):
- formset = DispositionParticipationFormset(
- self.request.POST or None, queryset=self.object.participations, prefix="participations"
- )
- return formset
-
- def post(self, request, *args, **kwargs):
- formset = self.get_formset()
- if formset.is_valid():
- formset.save()
- return redirect(self.object.event.get_absolute_url())
- return self.get(request, *args, **kwargs, formset=formset)
-
- def get_context_data(self, **kwargs):
- kwargs.setdefault("formset", self.get_formset())
- kwargs.setdefault("states", AbstractParticipation.States)
- kwargs.setdefault("sections", self.object.signup_method.configuration.sections)
- kwargs.setdefault(
- "participant_template", "basesignup/section_based/fragment_participant.html"
- )
- return super().get_context_data(**kwargs)
-
-
class SectionForm(forms.Form):
title = forms.CharField(label=_("Title"), required=True)
qualifications = forms.ModelMultipleChoiceField(
@@ -208,10 +179,10 @@ def get_context_data(self, **kwargs):
def form_valid(self, form):
return super().signup_pressed(preferred_section_uuid=form.cleaned_data.get("section"))
- def signup_pressed(self):
+ def signup_pressed(self, **kwargs):
if not self.method.configuration.choose_preferred_section:
# do straight signup if choosing is not enabled
- return super().signup_pressed()
+ return super().signup_pressed(**kwargs)
if not self.method.can_sign_up(self.request.user.as_participant()):
# redirect a misled request
@@ -234,9 +205,12 @@ class SectionBasedSignupMethod(BaseSignupMethod):
registration_button_text = _("Request")
signup_success_message = _("You have successfully requested a participation for {shift}.")
signup_error_message = _("Requesting a participation failed: {error}")
+
configuration_form_class = SectionBasedConfigurationForm
signup_view_class = SectionBasedSignupView
+ disposition_participation_form_class = SectionBasedDispositionParticipationForm
+
def get_configuration_fields(self):
return {
**super().get_configuration_fields(),
@@ -268,7 +242,10 @@ def check_qualification(method, participant):
def signup_checkers(self):
return super().signup_checkers + [self.check_qualification]
- def perform_signup(self, participant: AbstractParticipant, preferred_section_uuid=None):
+ # pylint: disable=arguments-differ
+ def perform_signup(
+ self, participant: AbstractParticipant, preferred_section_uuid=None, **kwargs
+ ):
participation = super().perform_signup(participant)
participation.data["preferred_section_uuid"] = preferred_section_uuid
if preferred_section_uuid:
@@ -316,7 +293,7 @@ def render_shift_state(self, request):
"confirmed_sections_with_users": confirmed_sections_with_users,
"disposition_url": (
reverse(
- "basesignup:shift_disposition_section_based",
+ "basesignup:shift_disposition",
kwargs=dict(pk=self.shift.pk),
)
if request.user.has_perm("event_management.change_event", obj=self.shift.event)
diff --git a/ephios/plugins/basesignup/signup/simple.py b/ephios/plugins/basesignup/signup/simple.py
deleted file mode 100644
index 51638bbdf..000000000
--- a/ephios/plugins/basesignup/signup/simple.py
+++ /dev/null
@@ -1,180 +0,0 @@
-from django import forms
-from django.shortcuts import redirect
-from django.template.loader import get_template
-from django.urls import reverse
-from django.utils.translation import gettext_lazy as _
-from django.views.generic import TemplateView
-from django.views.generic.detail import SingleObjectMixin
-from django_select2.forms import Select2MultipleWidget
-
-from ephios.event_management.models import AbstractParticipation, Shift
-from ephios.event_management.signup import BaseSignupMethod, ParticipationError
-from ephios.extra.permissions import CustomPermissionRequiredMixin
-from ephios.user_management.models import Qualification
-
-
-class SimpleQualificationsRequiredSignupMethod(BaseSignupMethod):
- # pylint: disable=abstract-method
- def __init__(self, shift):
- super().__init__(shift)
- if shift is not None:
- self.configuration.required_qualifications = Qualification.objects.filter(
- pk__in=self.configuration.required_qualification_ids
- )
-
- @property
- def signup_checkers(self):
- return super().signup_checkers + [self.check_qualification]
-
- @staticmethod
- def check_qualification(method, participant):
- if not participant.has_qualifications(method.configuration.required_qualifications):
- return ParticipationError(_("You are not qualified."))
-
- def get_configuration_fields(self):
- return {
- **super().get_configuration_fields(),
- "required_qualification_ids": {
- "formfield": forms.ModelMultipleChoiceField(
- label=_("Required Qualifications"),
- queryset=Qualification.objects.all(),
- widget=Select2MultipleWidget,
- required=False,
- ),
- "default": [],
- "publish_with_label": _("Required Qualification"),
- "format": lambda ids: ", ".join(
- Qualification.objects.filter(id__in=ids).values_list("title", flat=True)
- ),
- },
- }
-
-
-class InstantConfirmationSignupMethod(SimpleQualificationsRequiredSignupMethod):
- slug = "instant_confirmation"
- verbose_name = _("Instant Confirmation")
- description = _("""This method instantly confirms every signup after it was requested.""")
-
- @property
- def signup_checkers(self):
- return super().signup_checkers + [self.check_maximum_number_of_participants]
-
- @staticmethod
- def check_maximum_number_of_participants(method, participant):
- if method.configuration.maximum_number_of_participants is not None:
- current_count = AbstractParticipation.objects.filter(
- shift=method.shift, state=AbstractParticipation.States.CONFIRMED
- ).count()
- if current_count >= method.configuration.maximum_number_of_participants:
- return ParticipationError(_("The maximum number of participants is reached."))
-
- def get_configuration_fields(self):
- return {
- **super().get_configuration_fields(),
- "maximum_number_of_participants": {
- "formfield": forms.IntegerField(min_value=1, required=False),
- "default": None,
- "publish_with_label": _("Maximum number of participants"),
- },
- }
-
- def render_shift_state(self, request):
- return get_template("basesignup/signup_instant_state.html").render({"shift": self.shift})
-
- def perform_signup(self, participant, **kwargs):
- participation = super().perform_signup(participant, **kwargs)
- participation.state = AbstractParticipation.States.CONFIRMED
- participation.save()
- return participation
-
-
-DispositionParticipationFormset = forms.modelformset_factory(
- model=AbstractParticipation,
- fields=["state"],
- extra=0,
- can_order=False,
- can_delete=False,
- widgets={
- "state": forms.HiddenInput(attrs={"class": "state-input"}),
- },
-)
-
-
-class RequestConfirmDispositionView(CustomPermissionRequiredMixin, SingleObjectMixin, TemplateView):
- model = Shift
- permission_required = "event_management.change_event"
- template_name = "basesignup/disposition.html"
-
- def get_permission_object(self):
- self.object: Shift = self.get_object()
- return self.object.event
-
- def get_formset(self):
- return DispositionParticipationFormset(
- self.request.POST or None, queryset=self.object.participations
- )
-
- def get(self, request, *args, **kwargs):
- self.object: Shift = self.get_object()
- return super().get(request, *args, **kwargs)
-
- def post(self, request, *args, **kwargs):
- self.object: Shift = self.get_object()
- formset = self.get_formset()
- if formset.is_valid():
- formset.save()
- return redirect(self.object.event.get_absolute_url())
- return self.get(request, *args, **kwargs, formset=formset)
-
- def get_context_data(self, **kwargs):
- kwargs.setdefault("formset", self.get_formset())
- kwargs.setdefault("states", AbstractParticipation.States)
- kwargs.setdefault(
- "participant_template", "basesignup/requestconfirm/fragment_participant.html"
- )
- return super().get_context_data(**kwargs)
-
-
-class RequestConfirmSignupMethod(SimpleQualificationsRequiredSignupMethod):
- slug = "request_confirm"
- verbose_name = _("Request and confirm")
- description = _(
- """This method lets people request participation. Responsibles can then confirm the participation."""
- )
- registration_button_text = _("Request")
- signup_success_message = _("You have successfully requested a participation for {shift}.")
- signup_error_message = _("Requesting a participation failed: {error}")
-
- def render_shift_state(self, request):
- participations = self.shift.participations.filter(
- state__in={
- AbstractParticipation.States.REQUESTED,
- AbstractParticipation.States.CONFIRMED,
- }
- )
- return get_template("basesignup/requestconfirm/fragment_state.html").render(
- {
- "shift": self.shift,
- "requested_participants": (
- p.participant
- for p in participations.filter(state=AbstractParticipation.States.REQUESTED)
- ),
- "confirmed_participants": (
- p.participant
- for p in participations.filter(state=AbstractParticipation.States.CONFIRMED)
- ),
- "disposition_url": (
- reverse(
- "basesignup:shift_disposition_requestconfirm", kwargs=dict(pk=self.shift.pk)
- )
- if request.user.has_perm("event_management.change_event", obj=self.shift.event)
- else None
- ),
- }
- )
-
- def perform_signup(self, participant, **kwargs):
- participation = super().perform_signup(participant, **kwargs)
- participation.state = AbstractParticipation.States.REQUESTED
- participation.save()
- return participation
diff --git a/ephios/plugins/basesignup/templates/basesignup/common/disposition.html b/ephios/plugins/basesignup/templates/basesignup/common/disposition.html
new file mode 100644
index 000000000..df7b3fba2
--- /dev/null
+++ b/ephios/plugins/basesignup/templates/basesignup/common/disposition.html
@@ -0,0 +1,115 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% load bootstrap4 %}
+{% load static %}
+{% load formset_tags %}
+
+{% block title %}
+ {% translate "Disposition" %}
+{% endblock %}
+
+{% block javascript %}
+
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+{% endblock %}
diff --git a/ephios/plugins/basesignup/templates/basesignup/common/fragment_participant.html b/ephios/plugins/basesignup/templates/basesignup/common/fragment_participant.html
new file mode 100644
index 000000000..1930e827f
--- /dev/null
+++ b/ephios/plugins/basesignup/templates/basesignup/common/fragment_participant.html
@@ -0,0 +1,20 @@
+{% load bootstrap4 %}
+{% load i18n %}
+
+
+
+
+
+ {{ form.instance.participant }}
+
+
+
+
+
+
+
+ {% bootstrap_form form layout="inline" %}
+
+
\ No newline at end of file
diff --git a/ephios/plugins/basesignup/templates/basesignup/disposition.html b/ephios/plugins/basesignup/templates/basesignup/disposition.html
deleted file mode 100644
index 0e748341d..000000000
--- a/ephios/plugins/basesignup/templates/basesignup/disposition.html
+++ /dev/null
@@ -1,86 +0,0 @@
-{% extends "base.html" %}
-{% load i18n %}
-{% load bootstrap4 %}
-{% load static %}
-
-{% block title %}
- {% translate "Disposition" %}
-{% endblock %}
-
-{% block javascript %}
-
-{% endblock %}
-
-{% block content %}
-
-
-
-
-{% endblock %}
diff --git a/ephios/plugins/basesignup/templates/basesignup/signup_instant_state.html b/ephios/plugins/basesignup/templates/basesignup/instant/fragment_state.html
similarity index 53%
rename from ephios/plugins/basesignup/templates/basesignup/signup_instant_state.html
rename to ephios/plugins/basesignup/templates/basesignup/instant/fragment_state.html
index fea9c3b3c..0f0feec92 100644
--- a/ephios/plugins/basesignup/templates/basesignup/signup_instant_state.html
+++ b/ephios/plugins/basesignup/templates/basesignup/instant/fragment_state.html
@@ -11,3 +11,9 @@
+
+{% if disposition_url %}
+
+{% endif %}
\ No newline at end of file
diff --git a/ephios/plugins/basesignup/templates/basesignup/requestconfirm/fragment_state.html b/ephios/plugins/basesignup/templates/basesignup/request_confirm/fragment_state.html
similarity index 100%
rename from ephios/plugins/basesignup/templates/basesignup/requestconfirm/fragment_state.html
rename to ephios/plugins/basesignup/templates/basesignup/request_confirm/fragment_state.html
diff --git a/ephios/plugins/basesignup/templates/basesignup/requestconfirm/fragment_participant.html b/ephios/plugins/basesignup/templates/basesignup/requestconfirm/fragment_participant.html
deleted file mode 100644
index 085af516f..000000000
--- a/ephios/plugins/basesignup/templates/basesignup/requestconfirm/fragment_participant.html
+++ /dev/null
@@ -1,10 +0,0 @@
-{% load bootstrap4 %}
-
-
-
- {{ form.instance.participant }}
-
-
- {{ form }}
-
-
\ No newline at end of file
diff --git a/ephios/plugins/basesignup/templates/basesignup/section_based/fragment_participant.html b/ephios/plugins/basesignup/templates/basesignup/section_based/fragment_participant.html
index 770d6cfd6..cabb59360 100644
--- a/ephios/plugins/basesignup/templates/basesignup/section_based/fragment_participant.html
+++ b/ephios/plugins/basesignup/templates/basesignup/section_based/fragment_participant.html
@@ -1,9 +1,9 @@
{% load bootstrap4 %}
{% load i18n %}
-
-
-
-
+
+
+
+
{{ form.instance.participant }}
{% if form.preferred_section %}
@@ -13,13 +13,16 @@
{% endif %}
-
- {% bootstrap_field form.section show_label='sr-only' form_group_class="form-group mb-0" %}
+
+ {% bootstrap_field form.section show_label='sr-only' form_group_class="form-group d-inline-block mb-0" %}
+
+
- {{ form.state }}
- {{ form.id }}
+ {% bootstrap_form form exclude="section" layout="inline" %}
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/ephios/plugins/basesignup/urls.py b/ephios/plugins/basesignup/urls.py
index 9153ba1d0..53a58e1a6 100644
--- a/ephios/plugins/basesignup/urls.py
+++ b/ephios/plugins/basesignup/urls.py
@@ -1,18 +1,17 @@
from django.urls import path
-from ephios.plugins.basesignup.signup.section_based import SectionBasedDispositionView
-from ephios.plugins.basesignup.signup.simple import RequestConfirmDispositionView
+from ephios.plugins.basesignup.signup.disposition import AddUserView, DispositionView
app_name = "basesignup"
urlpatterns = [
path(
- "shifts//disposition/requestconfirm",
- RequestConfirmDispositionView.as_view(),
- name="shift_disposition_requestconfirm",
+ "shifts//disposition/",
+ DispositionView.as_view(),
+ name="shift_disposition",
),
path(
- "shifts//disposition/section-based",
- SectionBasedDispositionView.as_view(),
- name="shift_disposition_section_based",
+ "shifts//disposition/add-user/",
+ AddUserView.as_view(),
+ name="shift_disposition_add_user",
),
]
diff --git a/ephios/static/ephios/js/formset/LICENSE b/ephios/static/ephios/js/formset/LICENSE
new file mode 100644
index 000000000..a26b87db9
--- /dev/null
+++ b/ephios/static/ephios/js/formset/LICENSE
@@ -0,0 +1,23 @@
+Copyright (c) 2013, Ionata Web Solutions
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/ephios/static/ephios/js/formset/formset.js b/ephios/static/ephios/js/formset/formset.js
new file mode 100644
index 000000000..e5f582413
--- /dev/null
+++ b/ephios/static/ephios/js/formset/formset.js
@@ -0,0 +1,379 @@
+/**
+ * Django formset helper, adapted from django-formset-js-improved
+ */
+(function ($) {
+ "use strict";
+
+ var pluginName = 'formset';
+
+ /**
+ * Wraps up a formset, allowing adding, and removing forms
+ */
+ var Formset = function (el, options) {
+ var _this = this;
+
+ //Defaults:
+ this.opts = $.extend({}, Formset.defaults, options);
+
+ this.$formset = $(el);
+ this.$emptyForm = this.$formset.find(this.opts.emptyForm);
+ this.$body = this.$formset.find(this.opts.body);
+ this.$add = this.$formset.find(this.opts.add);
+
+ this.formsetPrefix = $(el).data('formset-prefix');
+
+ // Bind to the `Add form` button
+ this.addForm = $.proxy(this, 'addForm');
+ this.$add.click(this.addForm);
+
+ // Bind receiver to `formAdded` and `formDeleted` events
+ this.$formset.on('formAdded formDeleted', this.opts.form, $.proxy(this, 'checkMaxForms'));
+
+ // Set up the existing forms
+ this.$forms().each(function (i, form) {
+ var $form = $(form);
+ _this.bindForm($(this), i);
+ });
+
+ // Fill "ORDER" fields with the current order
+ this.prefillOrder();
+
+ // Store a reference to this in the formset element
+ this.$formset.data(pluginName, this);
+
+ var extras = ['animateForms'];
+ $.each(extras, function (i, extra) {
+ if ((extra in _this.opts) && (_this.opts[extra])) {
+ _this[extra]();
+ }
+ });
+ };
+
+ Formset.defaults = {
+ form: '[data-formset-form]',
+ emptyForm: 'script[type=form-template][data-formset-empty-form]',
+ body: '[data-formset-body]',
+ add: '[data-formset-add]',
+ deleteButton: '[data-formset-delete-button]',
+ moveUpButton: '[data-formset-move-up-button]',
+ moveDownButton: '[data-formset-move-down-button]',
+ hasMaxFormsClass: 'has-max-forms',
+ animateForms: false,
+ reorderMode: 'none',
+ empty_prefix: '__prefix__'
+ };
+
+ Formset.prototype.addForm = function () {
+ // Don't proceed if the number of maximum forms has been reached
+ if (this.hasMaxForms()) {
+ throw new Error("MAX_NUM_FORMS reached");
+ }
+
+ var newIndex = this.totalFormCount();
+ this.$managementForm('TOTAL_FORMS').val(newIndex + 1);
+
+ var newFormHtml = this.$emptyForm.html()
+ .replace(new RegExp(this.opts.empty_prefix, 'g'), newIndex)
+ .replace(new RegExp('<\\\\/script>', 'g'), '');
+
+ var $newFormFragment = $($.parseHTML(newFormHtml, this.$body.document, true));
+ this.$body.append($newFormFragment);
+
+ var $newForm = $newFormFragment.filter(this.opts.form);
+ this.bindForm($newForm, newIndex);
+
+ var prefix = this.formsetPrefix + '-' + newIndex;
+ $newForm.find('[name=' + prefix + '-ORDER]').val(newIndex);
+ $newForm.attr("data-formset-created-at-runtime", "true");
+
+ return $newForm;
+ };
+
+ Formset.prototype.extractPrefix = function ($form) {
+ const indexRegex = new RegExp("(" + this.formsetPrefix + "-\\d+)-id");
+ let toReturn = null;
+ $form.find(":input").each((idx, element) => {
+ const name = $(element).attr("name");
+ if (name && name.match(indexRegex)) {
+ toReturn = name.replace(indexRegex, "$1");
+ return false;
+ }
+ });
+ return toReturn;
+ }
+
+ /**
+ * Attach any events needed to a new form
+ */
+ Formset.prototype.bindForm = function ($form, index) {
+ var _this = this;
+
+ // try to find prefix, otherwise guess it from the index
+ const extractedPrefix = this.extractPrefix($form);
+ var prefix = extractedPrefix || (this.formsetPrefix + '-' + index);
+ $form.data(pluginName + '__formPrefix', prefix);
+
+ var $delete = $form.find('[name=' + prefix + '-DELETE]');
+ var $order = $form.find('[name=' + prefix + '-ORDER]');
+
+ var onChangeDelete = function () {
+ if ($delete.is(':checked')) {
+ $form.attr('data-formset-form-deleted', '');
+ // Remove required property and pattern attribute to allow submit, back it up to data field
+ $form.find(':required').data(pluginName + '-required-field', true).prop('required', false);
+ $form.find('input[pattern]').each(function () {
+ var pattern = $(this).attr('pattern');
+ $(this).data(pluginName + '-field-pattern', pattern).removeAttr('pattern');
+ });
+ $form.trigger('formDeleted');
+ } else {
+ $form.removeAttr('data-formset-form-deleted');
+ // Restore required property and pattern attributes from data field
+ $form.find('*').filter(function () {
+ return $(this).data(pluginName + '-required-field') === true;
+ }).prop('required', true);
+ $form.find('input').each(function () {
+ var pattern = $(this).data(pluginName + '-field-pattern');
+ if (pattern) {
+ $(this).attr('pattern', pattern);
+ }
+ });
+ $form.trigger('formAdded');
+ }
+ }
+
+ // Trigger `formAdded` / `formDeleted` events when delete checkbox value changes
+ $delete.change(onChangeDelete);
+
+ // This will trigger `formAdded` for newly created forms.
+ // It will also trigger `formAdded` or `formDeleted` for all forms when
+ // the Formset is first created.
+ // setTimeout so the caller can register events before the events are
+ // triggered, during initialisation.
+ window.setTimeout(onChangeDelete);
+
+ // Delete the form if the delete button is pressed
+ var $deleteButton = $form.find(this.opts.deleteButton);
+ $deleteButton.bind('click', function () {
+ $delete.attr('checked', true).change();
+ });
+
+ $order.change(function (event) {
+ _this.reorderForms();
+ });
+
+ var $moveUpButton = $form.find(this.opts.moveUpButton);
+
+ $moveUpButton.bind('click', function () {
+ // Find the closest form with an ORDER value lower
+ // than ours
+ var current = $order.val();
+ var $nextOrder = null;
+ _this.$activeForms().each(function (i, form) {
+ var $o = $(form).find('[name*=ORDER]');
+ var order = parseInt($o.val());
+ if (order < current && ($nextOrder == null || order > parseInt($nextOrder.val()))) {
+ $nextOrder = $o;
+ }
+ });
+
+ // Swap the order values
+ if ($nextOrder != null) {
+ // Swap the order values
+ $order.val($nextOrder.val());
+ $nextOrder.val(current);
+ }
+
+ _this.reorderForms();
+ });
+
+ var $moveDownButton = $form.find(this.opts.moveDownButton);
+
+ $moveDownButton.bind('click', function () {
+ // Find the closest form with an ORDER value higher
+ // than ours
+ var current = $order.val();
+ var $nextOrder = null;
+ _this.$activeForms().each(function (i, form) {
+ var $o = $(form).find('[name*=ORDER]');
+ var order = parseInt($o.val());
+ if (order > current && ($nextOrder == null || order < parseInt($nextOrder.val()))) {
+ $nextOrder = $o;
+ }
+ });
+
+ // Swap the order values
+ if ($nextOrder != null) {
+ $order.val($nextOrder.val());
+ $nextOrder.val(current);
+ }
+
+ _this.reorderForms();
+ });
+ };
+
+ /**
+ * Enumerate the forms and fill numbers into their ORDER input
+ * fields, if present.
+ */
+ Formset.prototype.prefillOrder = function () {
+ var _this = this;
+ this.$forms().each(function (i, form) {
+ var prefix = _this.formsetPrefix + '-' + i;
+ var $order = $(form).find('[name=' + prefix + '-ORDER]');
+ $order.val(i);
+ });
+ }
+
+ /**
+ * Enumerate the forms and fill numbers into their ORDER input
+ * fields, if present.
+ */
+ Formset.prototype.reorderForms = function () {
+ var _this = this;
+
+ var compareForms = function (form_a, form_b) {
+ /**
+ * Compare two forms based on their ORDER input value.
+ */
+ var a = parseInt($(form_a).find('[name*=-ORDER]').val());
+ var b = parseInt($(form_b).find('[name*=-ORDER]').val());
+ return (a < b ? -1 : (a > b ? 1 : 0));
+ }
+ var $forms = this.$activeForms().sort(compareForms);
+
+ if (this.opts.reorderMode == 'dom') {
+ $forms.reverse().each(function (i, form) {
+ // Move the forms to the top of $body, one by one
+ _this.$body.prepend($(form));
+ });
+ } else if (this.opts.reorderMode == 'animate') {
+ var accumulatedHeight = 0;
+
+ // Setup the CSS
+ if (this.$body.css("position") != "relative") {
+ this.$body.css("height", this.$body.outerHeight(true) + "px");
+ this.$body.css("position", "relative");
+ this.$activeForms().each(function (i, form) {
+ $(form).css("position", "absolute");
+ $(form).css("top", accumulatedHeight + "px");
+ accumulatedHeight += $(form).outerHeight(true);
+ });
+ accumulatedHeight = 0;
+ }
+
+ // Do the animation
+ $forms.each(function (i, form) {
+ $(form).stop().animate({
+ "top": accumulatedHeight + "px"
+ }, 1000);
+ accumulatedHeight += $(form).outerHeight(true);
+ });
+ this.$body.css("height", accumulatedHeight + "px");
+
+ // Reset the CSS
+ window.setTimeout(function () {
+ $forms.reverse().each(function (i, form) {
+ $(form).css("position", "static");
+ // Move the forms to the top of $body, one by one
+ _this.$body.prepend($(form));
+ });
+ _this.$body.css("position", "static");
+ _this.$body.css("height", "auto");
+ }, 1000);
+ }
+ }
+
+ Formset.prototype.$forms = function () {
+ return this.$body.find(this.opts.form);
+ };
+
+ Formset.prototype.$activeForms = function () {
+ return this.$body.find(this.opts.form).not("[data-formset-form-deleted]");
+ };
+
+ Formset.prototype.$managementForm = function (name) {
+ return this.$formset.find('[name=' + this.formsetPrefix + '-' + name + ']');
+ };
+
+ Formset.prototype.totalFormCount = function () {
+ return this.$forms().length;
+ };
+
+ Formset.prototype.deletedFormCount = function () {
+ return this.$forms().filter('[data-formset-form-deleted]').length;
+ };
+
+ Formset.prototype.activeFormCount = function () {
+ return this.totalFormCount() - this.deletedFormCount();
+ };
+
+ Formset.prototype.hasMaxForms = function () {
+ var maxForms = parseInt(this.$managementForm('MAX_NUM_FORMS').val(), 10) || 1000;
+ return this.activeFormCount() >= maxForms;
+ };
+
+ Formset.prototype.checkMaxForms = function () {
+ if (this.hasMaxForms()) {
+ this.$formset.addClass(this.opts.hasMaxFormsClass);
+ this.$add.attr('disabled', 'disabled');
+ } else {
+ this.$formset.removeClass(this.opts.hasMaxFormsClass);
+ this.$add.removeAttr('disabled');
+ }
+ return false;
+ };
+
+ Formset.prototype.animateForms = function () {
+ this.$formset.on('formAdded', this.opts.form, function () {
+ var $form = $(this);
+ if ($form.attr("data-formset-created-at-runtime") == "true") {
+ $form.slideUp(0);
+ $form.slideDown();
+ }
+ return false;
+ }).on('formDeleted', this.opts.form, function () {
+ var $form = $(this);
+ $form.slideUp();
+ return false;
+ });
+ this.$forms().filter('[data-formset-form-deleted]').slideUp(0);
+ };
+
+ Formset.getOrCreate = function (el, options) {
+ var rev = $(el).data(pluginName);
+ if (!rev) {
+ rev = new Formset(el, options);
+ }
+
+ return rev;
+ };
+
+ $.fn[pluginName] = function () {
+ var options, fn, args;
+ // Create a new Formset for each element
+ if (arguments.length === 0 || (arguments.length === 1 && $.type(arguments[0]) != 'string')) {
+ options = arguments[0];
+ return this.each(function () {
+ return Formset.getOrCreate(this, options);
+ });
+ }
+
+ // Call a function on each Formset in the selector
+ fn = arguments[0];
+ args = $.makeArray(arguments).slice(1);
+
+ if (fn in Formset) {
+ // Call the Formset class method if it exists
+ args.unshift(this);
+ return Formset[fn].apply(Formset, args);
+ } else {
+ throw new Error("Unknown function call " + fn + " for $.fn.formset");
+ }
+ };
+
+ // Enable the array function 'reverse' for collections of jQuery
+ // elements by including the shortest jQuery plugin ever.
+ $.fn.reverse = [].reverse;
+
+})(jQuery);
diff --git a/ephios/static/ephios/js/main.js b/ephios/static/ephios/js/main.js
index 9a3e1cd3b..87c023aa5 100644
--- a/ephios/static/ephios/js/main.js
+++ b/ephios/static/ephios/js/main.js
@@ -15,23 +15,6 @@ $(document).ready(function () {
// Configure all prerendered Forms
handleForms($(document));
- // Used for disposition TODO: move to plugin specific JS!
- $("[data-drop-to-state]").each(function (index, elem) {
- Sortable.create(elem, {
- group: "participations",
- sort: true,
- draggable: ".draggable",
- emptyInsertThreshold: 50,
- fallbackTolerance: 5,
- animation: 150,
- easing: "cubic-bezier(1, 0, 0, 1)",
- onAdd: function (event) {
- const newState = $(event.target).data("drop-to-state");
- $(event.item).find(".state-input").val(newState);
- },
- });
- });
-
var recurrenceFields = document.querySelectorAll('.recurrence-widget');
Array.prototype.forEach.call(recurrenceFields, function (field, index) {
new recurrence.widget.Widget(field.id, {});
diff --git a/ephios/static/plugins/basesignup/js/disposition.js b/ephios/static/plugins/basesignup/js/disposition.js
new file mode 100644
index 000000000..d3a7da36a
--- /dev/null
+++ b/ephios/static/plugins/basesignup/js/disposition.js
@@ -0,0 +1,98 @@
+$(document).ready(function () {
+ function handleDispositionForm($form, state, instant) {
+ $form.find("[data-show-for-state]").each((index, el) => {
+ el = $(el);
+ if (el.attr("data-show-for-state").split(",").includes(state.toString())) {
+ el.slideDown();
+ } else if (!instant) {
+ el.slideUp();
+ } else {
+ el.fadeOut(0);
+ }
+ });
+ }
+
+ $("[data-drop-to-state]").each(function (index, elem) {
+ const newState = $(elem).data("drop-to-state");
+ Sortable.create(elem, {
+ group: "participations",
+ sort: true,
+ draggable: ".draggable",
+ emptyInsertThreshold: 20,
+ fallbackTolerance: 5,
+ animation: 150,
+ easing: "cubic-bezier(1, 0, 0, 1)",
+ // scrolling
+ scrollSensitivity: 150,
+ scrollSpeed: 15,
+ // set state
+ onAdd: (event) => {
+ $(event.item).find(".state-input").val(newState);
+ handleDispositionForm($(event.item), newState, false);
+ },
+ });
+ handleDispositionForm($(elem), newState, true);
+ });
+
+ $("select#id_user[form='add-user-form']").on('select2:close', function () {
+ // clear select2
+ const userSelect = $(this);
+ setTimeout(() => {
+ userSelect.val(null).change()
+ });
+
+ const spawn = $("[data-formset-spawn]");
+ const formset = $('#participations-form').formset('getOrCreate');
+ // look for existing form with that participation
+ const userId = $(this).val();
+ if (!userId) {
+ return;
+ }
+
+ const participation = $('[data-participant-id=' + userId + ']');
+ if (participation.length) {
+ // we already have that card
+ const prefix = formset.extractPrefix(participation);
+ const deleteCheckbox = participation.find('[name=' + prefix + '-DELETE]');
+ if (deleteCheckbox.attr("checked")) {
+ // was marked for deletion, so revert that.
+ participation.attr("data-formset-created-at-runtime", true); // so formset.js decides to slideDown() it
+ deleteCheckbox.attr("checked", false).change();
+ }
+ // now visible. Move it to here.
+ $([document.documentElement, document.body]).animate({
+ scrollTop: participation.offset().top - 200
+ }, 1000);
+ participation.focus();
+ participation.addClass("list-group-item-info");
+ setTimeout(() => {
+ participation.removeClass("list-group-item-info")
+ }, 2000);
+ } else {
+ // get the new form from the server
+ const addUserForm = $("#" + $(this)[0].form.id);
+ $.ajax({
+ url: addUserForm.attr("action"),
+ type: 'post',
+ dataType: 'html',
+ data: addUserForm.serialize(),
+ success: function (data) {
+ // adapted from addForm from formset.js
+ // update management form
+ const newIndex = formset.totalFormCount() + 1;
+ formset.$managementForm('TOTAL_FORMS').val(newIndex);
+ formset.$managementForm('INITIAL_FORMS').val(newIndex);
+
+ // insert html
+ const $newFormFragment = $($.parseHTML(data));
+ spawn.append($newFormFragment);
+
+ var $newForm = $newFormFragment.filter(formset.opts.form);
+ formset.bindForm($newForm, newIndex);
+ $newForm.attr("data-formset-created-at-runtime", "true");
+ }
+ });
+ }
+ });
+
+});
\ No newline at end of file
diff --git a/ephios/static/sortablejs/Sortable.min.js b/ephios/static/sortablejs/Sortable.min.js
index eba061497..4fe7f0c36 100644
--- a/ephios/static/sortablejs/Sortable.min.js
+++ b/ephios/static/sortablejs/Sortable.min.js
@@ -1,2 +1,2 @@
-/*! Sortable 1.10.2 - MIT | git://github.com/SortableJS/Sortable.git */
-!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function o(t){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function a(){return(a=Object.assign||function(t){for(var e=1;e"===e[0]&&(e=e.substring(1)),t)try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return!1}return!1}}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"===e[0]?t.parentNode===n&&h(t,e):h(t,e))||o&&t===n)return t;if(t===n)break}while(t=(i=t).host&&i!==document&&i.host.nodeType?i.host:i.parentNode)}var i;return null}var f,p=/\s+/g;function k(t,e,n){if(t&&e)if(t.classList)t.classList[n?"add":"remove"](e);else{var o=(" "+t.className+" ").replace(p," ").replace(" "+e+" "," ");t.className=(o+(n?" "+e:"")).replace(p," ")}}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];e in o||-1!==e.indexOf("webkit")||(e="-webkit-"+e),o[e]=n+("string"==typeof n?"":"px")}}function v(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform");o&&"none"!==o&&(n=o+" "+n)}while(!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function g(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=e.left-n&&r<=e.right+n,i=a>=e.top-n&&a<=e.bottom+n;return n&&o&&i?l=t:void 0}}),l}((t=t.touches?t.touches[0]:t).clientX,t.clientY);if(e){var n={};for(var o in t)t.hasOwnProperty(o)&&(n[o]=t[o]);n.target=n.rootEl=e,n.preventDefault=void 0,n.stopPropagation=void 0,e[j]._onDragOver(n)}}}function kt(t){z&&z.parentNode[j]._isOutsideThisEl(t.target)}function Rt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[j]=this;var n={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Ot(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Rt.supportPointer&&"PointerEvent"in window,emptyInsertThreshold:5};for(var o in O.initializePlugins(this,t,n),n)o in e||(e[o]=n[o]);for(var i in At(e),this)"_"===i.charAt(0)&&"function"==typeof this[i]&&(this[i]=this[i].bind(this));this.nativeDraggable=!e.forceFallback&&xt,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?u(t,"pointerdown",this._onTapStart):(u(t,"mousedown",this._onTapStart),u(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(u(t,"dragover",this),u(t,"dragenter",this)),bt.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,T())}function Xt(t,e,n,o,i,r,a,l){var s,c,u=t[j],d=u.options.onMove;return!window.CustomEvent||w||E?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),d&&(c=d.call(u,s,a)),c}function Yt(t){t.draggable=!1}function Bt(){Dt=!1}function Ft(t){for(var e=t.tagName+t.className+t.src+t.href+t.textContent,n=e.length,o=0;n--;)o+=e.charCodeAt(n);return o.toString(36)}function Ht(t){return setTimeout(t,0)}function Lt(t){return clearTimeout(t)}Rt.prototype={constructor:Rt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(ht=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,z):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(function(t){St.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&St.push(o)}}(o),!z&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled||s.isContentEditable||(l=P(l,t.draggable,o,!1))&&l.animated||Z===l)){if(J=F(l),et=F(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return W({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),K("filter",n,{evt:e}),void(i&&e.cancelable&&e.preventDefault())}else if(c&&(c=c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return W({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),K("filter",n,{evt:e}),!0})))return void(i&&e.cancelable&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;if(n&&!z&&n.parentNode===r){var s=X(n);if(q=r,G=(z=n).parentNode,V=z.nextSibling,Z=n,ot=a.group,rt={target:Rt.dragged=z,clientX:(e||t).clientX,clientY:(e||t).clientY},ct=rt.clientX-s.left,ut=rt.clientY-s.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,z.style["will-change"]="all",o=function(){K("delayEnded",i,{evt:t}),Rt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!c&&i.nativeDraggable&&(z.draggable=!0),i._triggerDragStart(t,e),W({sortable:i,name:"choose",originalEvent:t}),k(z,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){g(z,t.trim(),Yt)}),u(l,"dragover",Pt),u(l,"mousemove",Pt),u(l,"touchmove",Pt),u(l,"mouseup",i._onDrop),u(l,"touchend",i._onDrop),u(l,"touchcancel",i._onDrop),c&&this.nativeDraggable&&(this.options.touchStartThreshold=4,z.draggable=!0),K("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(E||w))o();else{if(Rt.eventCanceled)return void this._onDrop();u(l,"mouseup",i._disableDelayedDrag),u(l,"touchend",i._disableDelayedDrag),u(l,"touchcancel",i._disableDelayedDrag),u(l,"mousemove",i._delayedDragTouchMoveHandler),u(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&u(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)}}},_delayedDragTouchMoveHandler:function(t){var e=t.touches?t.touches[0]:t;Math.max(Math.abs(e.clientX-this._lastX),Math.abs(e.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){z&&Yt(z),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;d(t,"mouseup",this._disableDelayedDrag),d(t,"touchend",this._disableDelayedDrag),d(t,"touchcancel",this._disableDelayedDrag),d(t,"mousemove",this._delayedDragTouchMoveHandler),d(t,"touchmove",this._delayedDragTouchMoveHandler),d(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?u(document,"pointermove",this._onTouchMove):u(document,e?"touchmove":"mousemove",this._onTouchMove):(u(z,"dragend",this),u(q,"dragstart",this._onDragStart));try{document.selection?Ht(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){if(vt=!1,q&&z){K("dragStarted",this,{evt:e}),this.nativeDraggable&&u(document,"dragover",kt);var n=this.options;t||k(z,n.dragClass,!1),k(z,n.ghostClass,!0),Rt.active=this,t&&this._appendGhost(),W({sortable:this,name:"start",originalEvent:e})}else this._nulling()},_emulateDragOver:function(){if(at){this._lastX=at.clientX,this._lastY=at.clientY,Nt();for(var t=document.elementFromPoint(at.clientX,at.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(at.clientX,at.clientY))!==e;)e=t;if(z.parentNode[j]._isOutsideThisEl(t),e)do{if(e[j]){if(e[j]._onDragOver({clientX:at.clientX,clientY:at.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}t=e}while(e=e.parentNode);It()}},_onTouchMove:function(t){if(rt){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=U&&v(U,!0),a=U&&r&&r.a,l=U&&r&&r.d,s=Ct&>&&b(gt),c=(i.clientX-rt.clientX+o.x)/(a||1)+(s?s[0]-Et[0]:0)/(a||1),u=(i.clientY-rt.clientY+o.y)/(l||1)+(s?s[1]-Et[1]:0)/(l||1);if(!Rt.active&&!vt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))o.right+10||t.clientX<=o.right&&t.clientY>o.bottom&&t.clientX>=o.left:t.clientX>o.right&&t.clientY>o.top||t.clientX<=o.right&&t.clientY>o.bottom+10}(n,a,this)&&!g.animated){if(g===z)return A(!1);if(g&&l===n.target&&(s=g),s&&(i=X(s)),!1!==Xt(q,l,z,o,s,i,n,!!s))return O(),l.appendChild(z),G=l,N(),A(!0)}else if(s.parentNode===l){i=X(s);var v,m,b,y=z.parentNode!==l,w=!function(t,e,n){var o=n?t.left:t.top,i=n?t.right:t.bottom,r=n?t.width:t.height,a=n?e.left:e.top,l=n?e.right:e.bottom,s=n?e.width:e.height;return o===a||i===l||o+r/2===a+s/2}(z.animated&&z.toRect||o,s.animated&&s.toRect||i,a),E=a?"top":"left",D=Y(s,"top","top")||Y(z,"top","top"),S=D?D.scrollTop:void 0;if(ht!==s&&(m=i[E],yt=!1,wt=!w&&e.invertSwap||y),0!==(v=function(t,e,n,o,i,r,a,l){var s=o?t.clientY:t.clientX,c=o?n.height:n.width,u=o?n.top:n.left,d=o?n.bottom:n.right,h=!1;if(!a)if(l&&pt"===e[0]&&(e=e.substring(1)),t)try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return!1}return!1}}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"===e[0]?t.parentNode===n&&h(t,e):h(t,e))||o&&t===n)return t;if(t===n)break}while(t=(i=t).host&&i!==document&&i.host.nodeType?i.host:i.parentNode)}var i;return null}var f,p=/\s+/g;function k(t,e,n){if(t&&e)if(t.classList)t.classList[n?"add":"remove"](e);else{var o=(" "+t.className+" ").replace(p," ").replace(" "+e+" "," ");t.className=(o+(n?" "+e:"")).replace(p," ")}}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];e in o||-1!==e.indexOf("webkit")||(e="-webkit-"+e),o[e]=n+("string"==typeof n?"":"px")}}function v(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform");o&&"none"!==o&&(n=o+" "+n)}while(!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function g(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=e.left-n&&r<=e.right+n,i=a>=e.top-n&&a<=e.bottom+n;return n&&o&&i?l=t:void 0}}),l}((t=t.touches?t.touches[0]:t).clientX,t.clientY);if(e){var n={};for(var o in t)t.hasOwnProperty(o)&&(n[o]=t[o]);n.target=n.rootEl=e,n.preventDefault=void 0,n.stopPropagation=void 0,e[j]._onDragOver(n)}}}function kt(t){z&&z.parentNode[j]._isOutsideThisEl(t.target)}function Rt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[j]=this;var n={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Ot(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Rt.supportPointer&&"PointerEvent"in window&&!u,emptyInsertThreshold:5};for(var o in O.initializePlugins(this,t,n),n)o in e||(e[o]=n[o]);for(var i in Nt(e),this)"_"===i.charAt(0)&&"function"==typeof this[i]&&(this[i]=this[i].bind(this));this.nativeDraggable=!e.forceFallback&&xt,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?d(t,"pointerdown",this._onTapStart):(d(t,"mousedown",this._onTapStart),d(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(d(t,"dragover",this),d(t,"dragenter",this)),bt.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,T())}function Xt(t,e,n,o,i,r,a,l){var s,c,u=t[j],d=u.options.onMove;return!window.CustomEvent||w||E?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),d&&(c=d.call(u,s,a)),c}function Yt(t){t.draggable=!1}function Bt(){Dt=!1}function Ft(t){for(var e=t.tagName+t.className+t.src+t.href+t.textContent,n=e.length,o=0;n--;)o+=e.charCodeAt(n);return o.toString(36)}function Ht(t){return setTimeout(t,0)}function Lt(t){return clearTimeout(t)}Rt.prototype={constructor:Rt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(ht=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,z):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(function(t){St.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&St.push(o)}}(o),!z&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||Z===l)){if(J=F(l),et=F(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return W({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),K("filter",n,{evt:e}),void(i&&e.cancelable&&e.preventDefault())}else if(c&&(c=c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return W({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),K("filter",n,{evt:e}),!0})))return void(i&&e.cancelable&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;if(n&&!z&&n.parentNode===r){var s=X(n);if(q=r,G=(z=n).parentNode,V=z.nextSibling,Z=n,ot=a.group,rt={target:Rt.dragged=z,clientX:(e||t).clientX,clientY:(e||t).clientY},ct=rt.clientX-s.left,ut=rt.clientY-s.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,z.style["will-change"]="all",o=function(){K("delayEnded",i,{evt:t}),Rt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!c&&i.nativeDraggable&&(z.draggable=!0),i._triggerDragStart(t,e),W({sortable:i,name:"choose",originalEvent:t}),k(z,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){g(z,t.trim(),Yt)}),d(l,"dragover",Pt),d(l,"mousemove",Pt),d(l,"touchmove",Pt),d(l,"mouseup",i._onDrop),d(l,"touchend",i._onDrop),d(l,"touchcancel",i._onDrop),c&&this.nativeDraggable&&(this.options.touchStartThreshold=4,z.draggable=!0),K("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(E||w))o();else{if(Rt.eventCanceled)return void this._onDrop();d(l,"mouseup",i._disableDelayedDrag),d(l,"touchend",i._disableDelayedDrag),d(l,"touchcancel",i._disableDelayedDrag),d(l,"mousemove",i._delayedDragTouchMoveHandler),d(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&d(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)}}},_delayedDragTouchMoveHandler:function(t){var e=t.touches?t.touches[0]:t;Math.max(Math.abs(e.clientX-this._lastX),Math.abs(e.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){z&&Yt(z),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;s(t,"mouseup",this._disableDelayedDrag),s(t,"touchend",this._disableDelayedDrag),s(t,"touchcancel",this._disableDelayedDrag),s(t,"mousemove",this._delayedDragTouchMoveHandler),s(t,"touchmove",this._delayedDragTouchMoveHandler),s(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?d(document,"pointermove",this._onTouchMove):d(document,e?"touchmove":"mousemove",this._onTouchMove):(d(z,"dragend",this),d(q,"dragstart",this._onDragStart));try{document.selection?Ht(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){if(vt=!1,q&&z){K("dragStarted",this,{evt:e}),this.nativeDraggable&&d(document,"dragover",kt);var n=this.options;t||k(z,n.dragClass,!1),k(z,n.ghostClass,!0),Rt.active=this,t&&this._appendGhost(),W({sortable:this,name:"start",originalEvent:e})}else this._nulling()},_emulateDragOver:function(){if(at){this._lastX=at.clientX,this._lastY=at.clientY,At();for(var t=document.elementFromPoint(at.clientX,at.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(at.clientX,at.clientY))!==e;)e=t;if(z.parentNode[j]._isOutsideThisEl(t),e)do{if(e[j]){if(e[j]._onDragOver({clientX:at.clientX,clientY:at.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}t=e}while(e=e.parentNode);It()}},_onTouchMove:function(t){if(rt){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=U&&v(U,!0),a=U&&r&&r.a,l=U&&r&&r.d,s=Ct&>&&b(gt),c=(i.clientX-rt.clientX+o.x)/(a||1)+(s?s[0]-Et[0]:0)/(a||1),u=(i.clientY-rt.clientY+o.y)/(l||1)+(s?s[1]-Et[1]:0)/(l||1);if(!Rt.active&&!vt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))o.right+10||t.clientX<=o.right&&t.clientY>o.bottom&&t.clientX>=o.left:t.clientX>o.right&&t.clientY>o.top||t.clientX<=o.right&&t.clientY>o.bottom+10}(n,a,this)&&!g.animated){if(g===z)return N(!1);if(g&&l===n.target&&(s=g),s&&(i=X(s)),!1!==Xt(q,l,z,o,s,i,n,!!s))return O(),l.appendChild(z),G=l,A(),N(!0)}else if(s.parentNode===l){i=X(s);var v,m,b,y=z.parentNode!==l,w=!function(t,e,n){var o=n?t.left:t.top,i=n?t.right:t.bottom,r=n?t.width:t.height,a=n?e.left:e.top,l=n?e.right:e.bottom,s=n?e.width:e.height;return o===a||i===l||o+r/2===a+s/2}(z.animated&&z.toRect||o,s.animated&&s.toRect||i,a),E=a?"top":"left",D=Y(s,"top","top")||Y(z,"top","top"),S=D?D.scrollTop:void 0;if(ht!==s&&(m=i[E],yt=!1,wt=!w&&e.invertSwap||y),0!==(v=function(t,e,n,o,i,r,a,l){var s=o?t.clientY:t.clientX,c=o?n.height:n.width,u=o?n.top:n.left,d=o?n.bottom:n.right,h=!1;if(!a)if(l&&pt
-
+
{% block javascript %}{% endblock %}
diff --git a/ephios/user_management/models.py b/ephios/user_management/models.py
index b30a2aa21..1a0d77961 100644
--- a/ephios/user_management/models.py
+++ b/ephios/user_management/models.py
@@ -95,6 +95,9 @@ class Meta:
def get_full_name(self):
return self.first_name + " " + self.last_name
+ def __str__(self):
+ return self.get_full_name()
+
def get_short_name(self):
return self.first_name
diff --git a/ephios/user_management/views/accounts.py b/ephios/user_management/views/accounts.py
index b168eff3a..9b1cf6849 100644
--- a/ephios/user_management/views/accounts.py
+++ b/ephios/user_management/views/accounts.py
@@ -54,8 +54,8 @@ def post(self, request, *args, **kwargs):
qualification_formset.save()
messages.success(
self.request,
- _("User {name} ({user}) added successfully.").format(
- name=userprofile.get_full_name(), user=userprofile
+ _("User {name} ({email}) added successfully.").format(
+ name=userprofile.get_full_name(), email=userprofile.email
),
)
if userprofile.is_active:
diff --git a/tests/conftest.py b/tests/conftest.py
index 79411a729..ecbe6744d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -8,7 +8,7 @@
from guardian.shortcuts import assign_perm
from ephios.event_management.models import Event, EventType, Shift
-from ephios.plugins.basesignup.signup.simple import RequestConfirmSignupMethod
+from ephios.plugins.basesignup.signup.request_confirm import RequestConfirmSignupMethod
from ephios.user_management.consequences import (
QualificationConsequenceHandler,
WorkingHoursConsequenceHandler,
diff --git a/tests/plugins/basesignup/test_requestconfirm_signup.py b/tests/plugins/basesignup/test_requestconfirm_signup.py
index 5e187d7d6..c9b42fc3a 100644
--- a/tests/plugins/basesignup/test_requestconfirm_signup.py
+++ b/tests/plugins/basesignup/test_requestconfirm_signup.py
@@ -26,11 +26,12 @@ def test_request_confirm_signup_flow(django_app, volunteer, planner, event):
# confirm the participation as planner
response = django_app.get(
- reverse("basesignup:shift_disposition_requestconfirm", kwargs=dict(pk=shift.pk)),
+ reverse("basesignup:shift_disposition", kwargs=dict(pk=shift.pk)),
user=planner,
)
- response.form["form-0-state"] = AbstractParticipation.States.CONFIRMED.value
- response.form.submit()
+ form = response.forms["participations-form"]
+ form["participations-0-state"] = AbstractParticipation.States.CONFIRMED.value
+ form.submit()
assert (
LocalParticipation.objects.get(user=volunteer, shift=shift).state
== AbstractParticipation.States.CONFIRMED
diff --git a/tests/plugins/basesignup/test_sectionbased_signup.py b/tests/plugins/basesignup/test_sectionbased_signup.py
index 7f03474c9..149b44b8d 100644
--- a/tests/plugins/basesignup/test_sectionbased_signup.py
+++ b/tests/plugins/basesignup/test_sectionbased_signup.py
@@ -119,11 +119,12 @@ def test_signup_flow(django_app, qualified_volunteer, planner, event, sectioned_
# confirm the participation as planner
response = django_app.get(
- reverse("basesignup:shift_disposition_section_based", kwargs=dict(pk=sectioned_shift.pk)),
+ reverse("basesignup:shift_disposition", kwargs=dict(pk=sectioned_shift.pk)),
user=planner,
)
- response.form["participations-0-state"] = AbstractParticipation.States.CONFIRMED.value
- response.form.submit()
+ form = response.forms["participations-form"]
+ form["participations-0-state"] = AbstractParticipation.States.CONFIRMED.value
+ form.submit()
participation = LocalParticipation.objects.get(user=qualified_volunteer, shift=sectioned_shift)
assert participation.state == AbstractParticipation.States.CONFIRMED
assert participation.data.get("preferred_section_uuid") == KTW_UUID