From e99ee915739c8e962aaeb876a1b5d3e7dc4852ff Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Mon, 2 Dec 2024 15:58:50 +0100 Subject: [PATCH] Allow to use custom domains for some but not all events (Z#23153875) (#4627) * Allow to use custom domains for some but not all events * Update src/pretix/multidomain/urlreverse.py * Apply suggestions from code review Co-authored-by: Mira * Logging for domain config changes --------- Co-authored-by: Mira --- src/pretix/base/models/event.py | 3 + src/pretix/control/forms/event.py | 79 +++++------ src/pretix/control/forms/organizer.py | 129 ++++++++++++------ .../pretixcontrol/event/settings.html | 2 +- .../pretixcontrol/organizers/edit.html | 65 +++++++++ src/pretix/control/views/event.py | 1 - src/pretix/control/views/organizer.py | 39 +++++- src/pretix/multidomain/middlewares.py | 23 +++- ...ainassignment_knowndomain_mode_and_more.py | 71 ++++++++++ src/pretix/multidomain/models.py | 112 +++++++++++++-- .../organizer_alternative_domain_urlconf.py | 63 +++++++++ src/pretix/multidomain/urlreverse.py | 45 +++--- src/pretix/presale/utils.py | 37 +++-- src/pretix/presale/views/customer.py | 6 +- src/pretix/presale/views/organizer.py | 7 +- src/tests/api/test_orders.py | 4 +- src/tests/multidomain/test_middlewares.py | 75 +++++++++- src/tests/multidomain/test_templatetag.py | 30 ++++ src/tests/multidomain/test_urlreverse.py | 68 +++++++-- src/tests/presale/test_customer.py | 41 +++++- 20 files changed, 747 insertions(+), 153 deletions(-) create mode 100644 src/pretix/multidomain/migrations/0003_alternativedomainassignment_knowndomain_mode_and_more.py create mode 100644 src/pretix/multidomain/organizer_alternative_domain_urlconf.py diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index caacf81922..3de4f68bc5 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -823,6 +823,9 @@ class Event(EventMixin, LoggedModel): self.save() self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk}) + if hasattr(other, 'alternative_domain_assignment'): + other.alternative_domain_assignment.domain.event_assignments.create(event=self) + if not self.all_sales_channels: self.limit_sales_channels.set( self.organizer.sales_channels.filter( diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index d640e026e3..3be1ea7054 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -35,7 +35,7 @@ # License for the specific language governing permissions and limitations under the License. from decimal import Decimal -from urllib.parse import urlencode, urlparse +from urllib.parse import urlencode from zoneinfo import ZoneInfo import pycountry @@ -76,8 +76,10 @@ from pretix.control.forms import ( ) from pretix.control.forms.widgets import Select2 from pretix.helpers.countries import CachedCountries -from pretix.multidomain.models import KnownDomain -from pretix.multidomain.urlreverse import build_absolute_uri +from pretix.multidomain.models import AlternativeDomainAssignment, KnownDomain +from pretix.multidomain.urlreverse import ( + build_absolute_uri, get_organizer_domain, +) from pretix.plugins.banktransfer.payment import BankTransfer from pretix.presale.style import get_fonts @@ -363,14 +365,9 @@ class EventUpdateForm(I18nModelForm): def __init__(self, *args, **kwargs): self.change_slug = kwargs.pop('change_slug', False) - self.domain = kwargs.pop('domain', False) kwargs.setdefault('initial', {}) self.instance = kwargs['instance'] - if self.domain and self.instance: - initial_domain = self.instance.domains.first() - if initial_domain: - kwargs['initial'].setdefault('domain', initial_domain.domainname) super().__init__(*args, **kwargs) if not self.change_slug: @@ -379,48 +376,54 @@ class EventUpdateForm(I18nModelForm): self.fields['location'].widget.attrs['placeholder'] = _( 'Sample Conference Center\nHeidelberg, Germany' ) - if self.domain: + + try: self.fields['domain'] = forms.CharField( max_length=255, - label=_('Custom domain'), + label=_('Domain'), + initial=self.instance.domain.domainname, + required=False, + disabled=True, + help_text=_('You can configure this in your organizer settings.') + ) + except KnownDomain.DoesNotExist: + domain = get_organizer_domain(self.instance.organizer) + try: + current_domain_assignment = self.instance.alternative_domain_assignment + except AlternativeDomainAssignment.DoesNotExist: + current_domain_assignment = None + self.fields['domain'] = forms.ChoiceField( + label=_('Domain'), + help_text=_('You can add more domains in your organizer account.'), + choices=[('', _('Same as organizer account') + (f" ({domain})" if domain else ""))] + [ + (d.domainname, d.domainname) for d in self.instance.organizer.domains.filter(mode=KnownDomain.MODE_ORG_ALT_DOMAIN) + ], + initial=current_domain_assignment.domain_id if current_domain_assignment else "", required=False, - help_text=_('You need to configure the custom domain in the webserver beforehand.') ) self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all() self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={ 'data-inverse-dependency': '<[name$=all_sales_channels]', }, choices=self.fields['limit_sales_channels'].widget.choices) - def clean_domain(self): - d = self.cleaned_data['domain'] - if d: - if d == urlparse(settings.SITE_URL).hostname: - raise ValidationError( - _('You cannot choose the base domain of this installation.') - ) - if KnownDomain.objects.filter(domainname=d).exclude(event=self.instance.pk).exists(): - raise ValidationError( - _('This domain is already in use for a different event or organizer.') - ) - return d - def save(self, commit=True): instance = super().save(commit) - if self.domain: - current_domain = instance.domains.first() - if self.cleaned_data['domain']: - if current_domain and current_domain.domainname != self.cleaned_data['domain']: - current_domain.delete() - KnownDomain.objects.create( - organizer=instance.organizer, event=instance, domainname=self.cleaned_data['domain'] - ) - elif not current_domain: - KnownDomain.objects.create( - organizer=instance.organizer, event=instance, domainname=self.cleaned_data['domain'] - ) - elif current_domain: - current_domain.delete() + try: + current_domain_assignment = instance.alternative_domain_assignment + except AlternativeDomainAssignment.DoesNotExist: + current_domain_assignment = None + if self.cleaned_data['domain'] and not hasattr(instance, 'domain'): + domain = self.instance.organizer.domains.get(mode=KnownDomain.MODE_ORG_ALT_DOMAIN, domainname=self.cleaned_data["domain"]) + AlternativeDomainAssignment.objects.update_or_create( + event=instance, + defaults={ + "domain": domain, + } + ) + instance.cache.clear() + elif current_domain_assignment: + current_domain_assignment.delete() instance.cache.clear() return instance diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index fb5f44ef10..8a4082e86a 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -133,63 +133,108 @@ class OrganizerDeleteForm(forms.Form): class OrganizerUpdateForm(OrganizerForm): def __init__(self, *args, **kwargs): - self.domain = kwargs.pop('domain', False) self.change_slug = kwargs.pop('change_slug', False) kwargs.setdefault('initial', {}) self.instance = kwargs['instance'] - if self.domain and self.instance: - initial_domain = self.instance.domains.filter(event__isnull=True).first() - if initial_domain: - kwargs['initial'].setdefault('domain', initial_domain.domainname) super().__init__(*args, **kwargs) if not self.change_slug: self.fields['slug'].widget.attrs['readonly'] = 'readonly' - if self.domain: - self.fields['domain'] = forms.CharField( - max_length=255, - label=_('Custom domain'), - required=False, - help_text=_('You need to configure the custom domain in the webserver beforehand.') - ) - - def clean_domain(self): - d = self.cleaned_data['domain'] - if d: - if d == urlparse(settings.SITE_URL).hostname: - raise ValidationError( - _('You cannot choose the base domain of this installation.') - ) - if KnownDomain.objects.filter(domainname=d).exclude(organizer=self.instance.pk, - event__isnull=True).exists(): - raise ValidationError( - _('This domain is already in use for a different event or organizer.') - ) - return d def clean_slug(self): if self.change_slug: return self.cleaned_data['slug'] return self.instance.slug - def save(self, commit=True): - instance = super().save(commit) - if self.domain: - current_domain = instance.domains.filter(event__isnull=True).first() - if self.cleaned_data['domain']: - if current_domain and current_domain.domainname != self.cleaned_data['domain']: - current_domain.delete() - KnownDomain.objects.create(organizer=instance, domainname=self.cleaned_data['domain']) - elif not current_domain: - KnownDomain.objects.create(organizer=instance, domainname=self.cleaned_data['domain']) - elif current_domain: - current_domain.delete() - instance.cache.clear() - for ev in instance.events.all(): - ev.cache.clear() +class KnownDomainForm(forms.ModelForm): + class Meta: + model = KnownDomain + fields = ["domainname", "mode", "event"] + field_classes = { + "event": SafeModelChoiceField, + } - return instance + def __init__(self, *args, **kwargs): + self.organizer = kwargs.pop('organizer') + super().__init__(*args, **kwargs) + self.fields["event"].queryset = self.organizer.events.all() + if self.instance and self.instance.pk: + self.fields["domainname"].widget.attrs['readonly'] = 'readonly' + + def clean_domainname(self): + if self.instance and self.instance.pk: + return self.instance.domainname + d = self.cleaned_data['domainname'] + if d: + if d == urlparse(settings.SITE_URL).hostname: + raise ValidationError( + _('You cannot choose the base domain of this installation.') + ) + if KnownDomain.objects.filter(domainname=d).exclude(organizer=self.instance.organizer).exists(): + raise ValidationError( + _('This domain is already in use for a different event or organizer.') + ) + return d + + def clean(self): + d = super().clean() + + if d["mode"] == KnownDomain.MODE_ORG_DOMAIN and d["event"]: + raise ValidationError( + _("Do not choose an event for this mode.") + ) + + if d["mode"] == KnownDomain.MODE_ORG_ALT_DOMAIN and d["event"]: + raise ValidationError( + _("Do not choose an event for this mode. You can assign events to this domain in event settings.") + ) + + if d["mode"] == KnownDomain.MODE_EVENT_DOMAIN and not d["event"]: + raise ValidationError( + _("You need to choose an event.") + ) + + return d + + +class BaseKnownDomainFormSet(forms.BaseInlineFormSet): + def __init__(self, *args, **kwargs): + self.organizer = kwargs.pop('organizer') + super().__init__(*args, **kwargs) + + def _construct_form(self, i, **kwargs): + kwargs['organizer'] = self.organizer + return super()._construct_form(i, **kwargs) + + @property + def empty_form(self): + form = self.form( + auto_id=self.auto_id, + prefix=self.add_prefix('__prefix__'), + empty_permitted=True, + use_required_attribute=False, + organizer=self.organizer, + ) + self.add_fields(form, None) + return form + + def clean(self): + super().clean() + data = [f.cleaned_data for f in self.forms] + + if len([d for d in data if d.get("mode") == KnownDomain.MODE_ORG_DOMAIN and not d.get("DELETE")]) > 1: + raise ValidationError(_("You may set only one organizer domain.")) + + return data + + +KnownDomainFormset = inlineformset_factory( + Organizer, KnownDomain, + KnownDomainForm, + formset=BaseKnownDomainFormSet, + can_order=False, can_delete=True, extra=0 +) class SafeOrderPositionChoiceField(forms.ModelChoiceField): diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html index f858f3eab6..f701872c05 100644 --- a/src/pretix/control/templates/pretixcontrol/event/settings.html +++ b/src/pretix/control/templates/pretixcontrol/event/settings.html @@ -22,7 +22,7 @@ {% bootstrap_field form.name layout="control" %} {% bootstrap_field form.slug layout="control" %} {% if form.domain %} - {% bootstrap_field form.domain layout="control" %} + {% bootstrap_field form.domain layout="horizontal" %} {% endif %} {% bootstrap_field form.date_from layout="control" %} {% bootstrap_field form.date_to layout="control" %} diff --git a/src/pretix/control/templates/pretixcontrol/organizers/edit.html b/src/pretix/control/templates/pretixcontrol/organizers/edit.html index ce39171d1c..f9d000a1f2 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/edit.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/edit.html @@ -294,6 +294,71 @@ {% trans "Invoices" %} {% bootstrap_field sform.invoice_regenerate_allowed layout="control" %} + {% if domain_formset %} +
+ {% trans "Domains" %} +
+ {% trans "This dialog is intended for advanced users." %} + {% trans "The domain needs to be configured on your webserver before it can be used here." %} +
+
+ {{ domain_formset.management_form }} + {% bootstrap_formset_errors domain_formset %} +
+ {% for form in domain_formset %} +
+
+ {{ form.id }} + {% bootstrap_field form.DELETE form_group_class="" layout="inline" %} +
+
+ {% bootstrap_field form.domainname layout='' form_group_class="" %} + {% bootstrap_form_errors form %} +
+
+ {% bootstrap_field form.mode layout='' form_group_class="" %} +
+
+ {% bootstrap_field form.event layout='' form_group_class="" %} +
+
+
+ +
+
+ {% endfor %} +
+ +

+ +

+
+ {% endif %}