From: Magnus Hagander Date: Mon, 15 Sep 2025 12:57:12 +0000 (+0200) Subject: Make it possible to restrict an additional option to a specific set of attendees X-Git-Url: http://git.postgresql.org/gitweb/?a=commitdiff_plain;h=ce5b8d1bed3fb497496ede60b6ec1961e1b63e8f;p=pgeu-system.git Make it possible to restrict an additional option to a specific set of attendees If specific attendees are invited to use an option, make it possible to specify them without creating a separate registration type for them. When specifying both regtype and attendee requirement, both must be satisfied. --- diff --git a/docs/confreg/registrations.md b/docs/confreg/registrations.md index 92119887..295b2bee 100644 --- a/docs/confreg/registrations.md +++ b/docs/confreg/registrations.md @@ -308,6 +308,13 @@ Requires regtype registration types must be picked. If the attendee does not have one of these registration types, they may be offered an upsell to a different registration type if that is [enabled](#regtypes). +Note that if both a required regtype and a required +attendee is specified, both requirements must be fulfilled. + +Requires attendee +: In order to add one of these options, the attendee must be one of +the specified ones. Note that if both a required regtype and a required +attendee is specified, both requirements must be fulfilled. Mutually exclusive : This option cannot be picked at the same time as the selected other diff --git a/postgresqleu/confreg/admin.py b/postgresqleu/confreg/admin.py index 1875c281..523c2737 100644 --- a/postgresqleu/confreg/admin.py +++ b/postgresqleu/confreg/admin.py @@ -250,6 +250,7 @@ class ConferenceAdditionalOptionAdminForm(ConcurrentProtectedModelForm): super(ConferenceAdditionalOptionAdminForm, self).__init__(*args, **kwargs) try: self.fields['requires_regtype'].queryset = RegistrationType.objects.filter(conference=self.instance.conference) + self.fields['requires_attendee'].queryset = ConferenceRegistration.objects.filter(conference=self.instance.conference) self.fields['mutually_exclusive'].queryset = ConferenceAdditionalOption.objects.filter(conference=self.instance.conference) self.fields['additionaldays'].queryset = RegistrationDay.objects.filter(conference=self.instance.conference) except Conference.DoesNotExist: @@ -263,7 +264,7 @@ class ConferenceAdditionalOptionAdmin(admin.ModelAdmin): list_filter = ['conference', ] ordering = ['conference', 'name', ] search_fields = ['name', ] - filter_horizontal = ('requires_regtype', 'mutually_exclusive', ) + filter_horizontal = ('requires_regtype', 'requires_attendee', 'mutually_exclusive', ) form = ConferenceAdditionalOptionAdminForm def get_queryset(self, request): diff --git a/postgresqleu/confreg/backendforms.py b/postgresqleu/confreg/backendforms.py index 90a8305e..2751f74d 100644 --- a/postgresqleu/confreg/backendforms.py +++ b/postgresqleu/confreg/backendforms.py @@ -597,7 +597,11 @@ class BackendAdditionalOptionForm(BackendForm): }) vat_fields = {'cost': 'reg'} auto_cascade_delete_to = ['registrationtype_requires_option', 'conferenceadditionaloption_requires_regtype', + 'conferenceadditionaloption_requires_attendee', 'conferenceadditionaloption_mutually_exclusive', ] + selectize_multiple_fields = { + 'requires_attendee': RegisteredUsersLookup(None), + } coltypes = { 'Maxcount': ['nosearch', ], } @@ -605,10 +609,12 @@ class BackendAdditionalOptionForm(BackendForm): class Meta: model = ConferenceAdditionalOption fields = ['name', 'cost', 'maxcount', 'sortkey', 'public', 'upsellable', 'invoice_autocancel_hours', - 'requires_regtype', 'mutually_exclusive', 'additionaldays'] + 'requires_regtype', 'requires_attendee', 'mutually_exclusive', 'additionaldays'] def fix_fields(self): self.fields['requires_regtype'].queryset = RegistrationType.objects.filter(conference=self.conference) + self.fields['requires_attendee'].queryset = ConferenceRegistration.objects.filter(conference=self.conference) + self.selectize_multiple_fields['requires_attendee'] = RegisteredUsersLookup(self.conference) self.fields['mutually_exclusive'].queryset = ConferenceAdditionalOption.objects.filter(conference=self.conference).exclude(pk=self.instance.pk) self.fields['additionaldays'].queryset = RegistrationDay.objects.filter(conference=self.conference) diff --git a/postgresqleu/confreg/forms.py b/postgresqleu/confreg/forms.py index 66b744b0..60e639d0 100644 --- a/postgresqleu/confreg/forms.py +++ b/postgresqleu/confreg/forms.py @@ -276,11 +276,8 @@ class ConferenceRegistrationForm(forms.ModelForm): regtype = cleaned_data['regtype'] errs = [] for ao in cleaned_data['additionaloptions']: - if ao.requires_regtype.exists(): - if regtype not in ao.requires_regtype.all(): - errs.append('Additional option "%s" requires one of the following registration types: %s.' % (ao.name, ", ".join(x.regtype for x in ao.requires_regtype.all()))) - if len(errs): - self._errors['additionaloptions'] = self.error_class(errs) + if msg := ao.verify_available_to(regtype, self.instance if self.instance.pk else None): + self.add_error('additionaloptions', msg) return cleaned_data diff --git a/postgresqleu/confreg/migrations/0119_conferenceadditionaloption_requires_attendee.py b/postgresqleu/confreg/migrations/0119_conferenceadditionaloption_requires_attendee.py new file mode 100644 index 00000000..c7f193c1 --- /dev/null +++ b/postgresqleu/confreg/migrations/0119_conferenceadditionaloption_requires_attendee.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.11 on 2025-09-15 12:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('confreg', '0118_registrationtype_checkinmessage'), + ] + + operations = [ + migrations.AddField( + model_name='conferenceadditionaloption', + name='requires_attendee', + field=models.ManyToManyField(blank=True, help_text='Can only be picked by specific attendees', to='confreg.conferenceregistration', verbose_name='Requires specific attendee'), + ), + ] diff --git a/postgresqleu/confreg/models.py b/postgresqleu/confreg/models.py index 2db0ef4a..bfe6acdc 100644 --- a/postgresqleu/confreg/models.py +++ b/postgresqleu/confreg/models.py @@ -540,6 +540,7 @@ class ConferenceAdditionalOption(models.Model): upsellable = models.BooleanField(null=False, blank=False, default=True, help_text='Can this option be purchased after the registration is completed') invoice_autocancel_hours = models.IntegerField(blank=True, null=True, validators=[MinValueValidator(1), ], verbose_name="Autocancel invoices", help_text="Automatically cancel invoices after this many hours") requires_regtype = models.ManyToManyField(RegistrationType, blank=True, verbose_name="Requires registration type", help_text='Can only be picked with selected registration types') + requires_attendee = models.ManyToManyField('ConferenceRegistration', blank=True, verbose_name="Requires specific attendee", help_text='Can only be picked by specific attendees') mutually_exclusive = models.ManyToManyField('self', blank=True, help_text='Mutually exlusive with these additional options', symmetrical=True) additionaldays = models.ManyToManyField(RegistrationDay, blank=True, verbose_name="Adds access to days", help_text='Adds access to additional conference day(s), even if the registration type does not') sortkey = models.IntegerField(default=100, null=False, blank=False, verbose_name="Sort key") @@ -566,6 +567,16 @@ class ConferenceAdditionalOption(models.Model): self.maxcount) return "%s%s" % (self.name, coststr) + def verify_available_to(self, regtype, reg): + if self.requires_regtype.exists() and not self.requires_regtype.filter(pk=regtype.pk).exists(): + return 'Option "{}" requires one of the registration types {}'.format(self.name, ", ".join(x.regtype for x in self.requires_regtype.all())) + if self.requires_attendee.exists(): + if reg and not self.requires_attendee.filter(pk=reg.pk).exists(): + return 'Option "{}" is only available to specific attendees'.format(self.name) + elif not reg: + return 'Option "{}" is only available to specific attendees'.format(self.name) + return None + class BulkPayment(models.Model): # User that owns this bulk payment diff --git a/postgresqleu/confreg/views.py b/postgresqleu/confreg/views.py index 4c7b7a9f..742db51a 100644 --- a/postgresqleu/confreg/views.py +++ b/postgresqleu/confreg/views.py @@ -1003,6 +1003,12 @@ def reg_add_options(request, confname, whatfor=None): else: upsell_cost = 0 + # Check that the options is available at all, given the combinations + for ao in options: + if msg := ao.verify_available_to(new_regtype if new_regtype else reg.regtype, reg): + messages.warning(request, msg) + return HttpResponseRedirect('../') + # Build our invoice rows invoicerows = [] autocancel_hours = [conference.invoice_autocancel_hours, ]