mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
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 <weller@rami.io> * Logging for domain config changes --------- Co-authored-by: Mira <weller@rami.io>
This commit is contained in:
@@ -80,28 +80,32 @@ class MultiDomainMiddleware(MiddlewareMixin):
|
||||
request.port = int(port) if port else None
|
||||
request.host = domain
|
||||
if domain == default_domain:
|
||||
request.domain_mode = "system"
|
||||
request.urlconf = "pretix.multidomain.maindomain_urlconf"
|
||||
elif domain:
|
||||
cached = cache.get('pretix_multidomain_instance_{}'.format(domain))
|
||||
cached = cache.get('pretix_multidomain_instances_{}'.format(domain))
|
||||
|
||||
if cached is None:
|
||||
try:
|
||||
kd = KnownDomain.objects.select_related('organizer', 'event').get(domainname=domain) # noqa
|
||||
orga = kd.organizer
|
||||
event = kd.event
|
||||
mode = kd.mode
|
||||
except KnownDomain.DoesNotExist:
|
||||
orga = False
|
||||
event = False
|
||||
mode = "system"
|
||||
cache.set(
|
||||
'pretix_multidomain_instance_{}'.format(domain),
|
||||
(orga.pk if orga else None, event.pk if event else None),
|
||||
'pretix_multidomain_instances_{}'.format(domain),
|
||||
(orga.pk if orga else None, event.pk if event else None, mode),
|
||||
3600
|
||||
)
|
||||
else:
|
||||
orga, event = cached
|
||||
orga, event, mode = cached
|
||||
|
||||
if event:
|
||||
if mode == KnownDomain.MODE_EVENT_DOMAIN:
|
||||
request.event_domain = True
|
||||
request.domain_mode = KnownDomain.MODE_EVENT_DOMAIN
|
||||
if isinstance(event, Event):
|
||||
request.organizer = orga
|
||||
request.event = event
|
||||
@@ -110,11 +114,18 @@ class MultiDomainMiddleware(MiddlewareMixin):
|
||||
request.event = Event.objects.select_related('organizer').get(pk=event)
|
||||
request.organizer = request.event.organizer
|
||||
request.urlconf = "pretix.multidomain.event_domain_urlconf"
|
||||
elif orga:
|
||||
elif mode == KnownDomain.MODE_ORG_ALT_DOMAIN:
|
||||
request.organizer_domain = True
|
||||
request.domain_mode = KnownDomain.MODE_ORG_ALT_DOMAIN
|
||||
request.organizer = orga if isinstance(orga, Organizer) else Organizer.objects.get(pk=orga)
|
||||
request.urlconf = "pretix.multidomain.organizer_alternative_domain_urlconf"
|
||||
elif mode == KnownDomain.MODE_ORG_DOMAIN:
|
||||
request.organizer_domain = True
|
||||
request.domain_mode = KnownDomain.MODE_ORG_DOMAIN
|
||||
request.organizer = orga if isinstance(orga, Organizer) else Organizer.objects.get(pk=orga)
|
||||
request.urlconf = "pretix.multidomain.organizer_domain_urlconf"
|
||||
elif settings.DEBUG or domain in LOCAL_HOST_NAMES:
|
||||
request.domain_mode = "system"
|
||||
request.urlconf = "pretix.multidomain.maindomain_urlconf"
|
||||
else:
|
||||
with scopes_disabled():
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# Generated by Django 4.2.16 on 2024-11-12 10:46
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0273_remove_checkinlist_auto_checkin_sales_channels"),
|
||||
("pretixmultidomain", "0002_knowndomain_event"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="AlternativeDomainAssignment",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="knowndomain",
|
||||
name="mode",
|
||||
field=models.CharField(default="organizer", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="knowndomain",
|
||||
name="event",
|
||||
field=models.OneToOneField(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="domain",
|
||||
to="pretixbase.event",
|
||||
),
|
||||
),
|
||||
migrations.RunSQL(
|
||||
sql="UPDATE pretixmultidomain_knowndomain SET mode = 'event' WHERE event_id IS NOT NULL",
|
||||
reverse_sql=migrations.RunSQL.noop,
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="knowndomain",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("mode", "organizer")),
|
||||
fields=("organizer",),
|
||||
name="unique_organizer_domain",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="alternativedomainassignment",
|
||||
name="domain",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="event_assignments",
|
||||
to="pretixmultidomain.knowndomain",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="alternativedomainassignment",
|
||||
name="event",
|
||||
field=models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="alternative_domain_assignment",
|
||||
to="pretixbase.event",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -21,6 +21,7 @@
|
||||
#
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
@@ -28,39 +29,134 @@ from pretix.base.models import Event, Organizer
|
||||
|
||||
|
||||
class KnownDomain(models.Model):
|
||||
domainname = models.CharField(max_length=255, primary_key=True)
|
||||
organizer = models.ForeignKey(Organizer, blank=True, null=True, related_name='domains', on_delete=models.CASCADE)
|
||||
event = models.ForeignKey(Event, blank=True, null=True, related_name='domains', on_delete=models.PROTECT)
|
||||
MODE_ORG_DOMAIN = "organizer"
|
||||
MODE_ORG_ALT_DOMAIN = "organizer_alternative"
|
||||
MODE_EVENT_DOMAIN = "event"
|
||||
MODES = (
|
||||
(MODE_ORG_DOMAIN, _("Organizer domain")),
|
||||
(MODE_ORG_ALT_DOMAIN, _("Alternative organizer domain for a set of events")),
|
||||
(MODE_EVENT_DOMAIN, _("Event domain")),
|
||||
)
|
||||
|
||||
domainname = models.CharField(
|
||||
max_length=255,
|
||||
primary_key=True,
|
||||
verbose_name=_("Domain name"),
|
||||
)
|
||||
mode = models.CharField(
|
||||
max_length=255,
|
||||
choices=MODES,
|
||||
default=MODE_ORG_DOMAIN,
|
||||
verbose_name=_("Mode"),
|
||||
)
|
||||
organizer = models.ForeignKey(
|
||||
Organizer,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name='domains',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
event = models.OneToOneField(
|
||||
Event,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name='domain',
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_("Event"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Known domain")
|
||||
verbose_name_plural = _("Known domains")
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("organizer",),
|
||||
name="unique_organizer_domain",
|
||||
condition=Q(mode="organizer"),
|
||||
),
|
||||
]
|
||||
ordering = ("-mode", "domainname")
|
||||
|
||||
def __str__(self):
|
||||
return self.domainname
|
||||
|
||||
@scopes_disabled()
|
||||
def save(self, *args, **kwargs):
|
||||
if self.event:
|
||||
self.mode = KnownDomain.MODE_EVENT_DOMAIN
|
||||
elif self.mode == KnownDomain.MODE_EVENT_DOMAIN:
|
||||
raise ValueError("Event domain needs event")
|
||||
super().save(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
try:
|
||||
self.event.alternative_domain_assignment.delete()
|
||||
except AlternativeDomainAssignment.DoesNotExist:
|
||||
pass
|
||||
elif self.organizer:
|
||||
self.organizer.get_cache().clear()
|
||||
for event in self.organizer.events.all():
|
||||
event.get_cache().clear()
|
||||
cache.delete('pretix_multidomain_organizer_{}'.format(self.domainname))
|
||||
cache.delete('pretix_multidomain_instance_{}'.format(self.domainname))
|
||||
cache.delete('pretix_multidomain_instances_{}'.format(self.domainname))
|
||||
cache.delete('pretix_multidomain_event_{}'.format(self.domainname))
|
||||
|
||||
@scopes_disabled()
|
||||
def delete(self, *args, **kwargs):
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
self.event.cache.clear()
|
||||
elif self.organizer:
|
||||
self.organizer.get_cache().clear()
|
||||
self.organizer.cache.clear()
|
||||
for event in self.organizer.events.all():
|
||||
event.get_cache().clear()
|
||||
event.cache.clear()
|
||||
cache.delete('pretix_multidomain_organizer_{}'.format(self.domainname))
|
||||
cache.delete('pretix_multidomain_instance_{}'.format(self.domainname))
|
||||
cache.delete('pretix_multidomain_instances_{}'.format(self.domainname))
|
||||
cache.delete('pretix_multidomain_event_{}'.format(self.domainname))
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
def _log_domain_action(self, user, data):
|
||||
if self.event:
|
||||
self.event.log_action(
|
||||
'pretix.event.settings',
|
||||
user=user,
|
||||
data=data
|
||||
)
|
||||
else:
|
||||
self.organizer.log_action(
|
||||
'pretix.organizer.settings',
|
||||
user=user,
|
||||
data=data
|
||||
)
|
||||
|
||||
def log_create(self, user):
|
||||
self._log_domain_action(user, {'add_alt_domain': self.domainname} if self.mode == KnownDomain.MODE_ORG_ALT_DOMAIN else {'domain': self.domainname})
|
||||
|
||||
def log_delete(self, user):
|
||||
self._log_domain_action(user, {'remove_alt_domain': self.domainname} if self.mode == KnownDomain.MODE_ORG_ALT_DOMAIN else {'domain': None})
|
||||
|
||||
|
||||
class AlternativeDomainAssignment(models.Model):
|
||||
domain = models.ForeignKey(
|
||||
KnownDomain,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="event_assignments",
|
||||
)
|
||||
event = models.OneToOneField(
|
||||
Event,
|
||||
related_name="alternative_domain_assignment",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
@scopes_disabled()
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
self.event.cache.clear()
|
||||
cache.delete('pretix_multidomain_instances_{}'.format(self.domain_id))
|
||||
cache.delete('pretix_multidomain_event_{}'.format(self.domain_id))
|
||||
|
||||
@scopes_disabled()
|
||||
def delete(self, *args, **kwargs):
|
||||
self.event.cache.clear()
|
||||
cache.delete('pretix_multidomain_instances_{}'.format(self.domain_id))
|
||||
cache.delete('pretix_multidomain_event_{}'.format(self.domain_id))
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import importlib.util
|
||||
|
||||
from django.apps import apps
|
||||
from django.urls import include, re_path
|
||||
|
||||
from pretix.multidomain.plugin_handler import plugin_event_urls
|
||||
from pretix.presale.urls import event_patterns, locale_patterns
|
||||
from pretix.presale.views import organizer
|
||||
from pretix.urls import common_patterns
|
||||
|
||||
presale_patterns = [
|
||||
re_path(r'', include((locale_patterns + [
|
||||
re_path(r'^$', organizer.RedirectToOrganizerIndex.as_view(), name='organizer.alt.index'),
|
||||
re_path(r'^(?P<event>[^/]+)/', include(event_patterns)),
|
||||
], 'presale')))
|
||||
]
|
||||
|
||||
raw_plugin_patterns = []
|
||||
for app in apps.get_app_configs():
|
||||
if hasattr(app, 'PretixPluginMeta'):
|
||||
if importlib.util.find_spec(app.name + '.urls'):
|
||||
urlmod = importlib.import_module(app.name + '.urls')
|
||||
if hasattr(urlmod, 'event_patterns'):
|
||||
patterns = plugin_event_urls(urlmod.event_patterns, plugin=app.name)
|
||||
raw_plugin_patterns.append(
|
||||
re_path(r'^(?P<event>[^/]+)/', include((patterns, app.label)))
|
||||
)
|
||||
if hasattr(urlmod, 'organizer_patterns'):
|
||||
patterns = plugin_event_urls(urlmod.organizer_patterns, plugin=app.name)
|
||||
raw_plugin_patterns.append(
|
||||
re_path(r'', include((patterns, app.label)))
|
||||
)
|
||||
|
||||
plugin_patterns = [
|
||||
re_path(r'', include((raw_plugin_patterns, 'plugins')))
|
||||
]
|
||||
|
||||
# The presale namespace comes last, because it contains a wildcard catch
|
||||
urlpatterns = common_patterns + plugin_patterns + presale_patterns
|
||||
|
||||
handler404 = 'pretix.base.views.errors.page_not_found'
|
||||
handler500 = 'pretix.base.views.errors.server_error'
|
||||
@@ -43,28 +43,33 @@ from pretix.base.models import Event, Organizer
|
||||
from .models import KnownDomain
|
||||
|
||||
|
||||
def get_event_domain(event, fallback=False, return_info=False):
|
||||
def get_event_domain(event, fallback=False, return_mode=False):
|
||||
assert isinstance(event, Event)
|
||||
if not event.pk:
|
||||
# Can happen on the "event deleted" response
|
||||
return (None, None) if return_info else None
|
||||
suffix = ('_fallback' if fallback else '') + ('_info' if return_info else '')
|
||||
return (None, None) if return_mode else None
|
||||
suffix = ('_fallback' if fallback else '') + ('_mode' if return_mode else '')
|
||||
domain = getattr(event, '_cached_domain' + suffix, None) or event.cache.get('domain' + suffix)
|
||||
if domain is None:
|
||||
domain = None, None
|
||||
if fallback:
|
||||
if hasattr(event, 'alternative_domain_assignment'):
|
||||
domain = event.alternative_domain_assignment.domain_id, KnownDomain.MODE_ORG_ALT_DOMAIN
|
||||
elif fallback:
|
||||
domains = KnownDomain.objects.filter(
|
||||
Q(event=event) | Q(organizer_id=event.organizer_id, event__isnull=True)
|
||||
Q(event=event, mode=KnownDomain.MODE_EVENT_DOMAIN) |
|
||||
Q(organizer_id=event.organizer_id, event__isnull=True, mode=KnownDomain.MODE_ORG_DOMAIN)
|
||||
)
|
||||
domains_event = [d for d in domains if d.event_id == event.pk]
|
||||
domains_org = [d for d in domains if not d.event_id]
|
||||
if domains_event:
|
||||
domain = domains_event[0].domainname, "event"
|
||||
domain = domains_event[0].domainname, KnownDomain.MODE_EVENT_DOMAIN
|
||||
elif domains_org:
|
||||
domain = domains_org[0].domainname, "organizer"
|
||||
domain = domains_org[0].domainname, KnownDomain.MODE_ORG_DOMAIN
|
||||
else:
|
||||
domains = event.domains.all()
|
||||
domain = domains[0].domainname if domains else None, "event"
|
||||
try:
|
||||
domain = event.domain.domainname, KnownDomain.MODE_EVENT_DOMAIN
|
||||
except KnownDomain.DoesNotExist:
|
||||
domain = None, None
|
||||
event.cache.set('domain' + suffix, domain or 'none')
|
||||
setattr(event, '_cached_domain' + suffix, domain or 'none')
|
||||
elif domain == 'none':
|
||||
@@ -72,7 +77,7 @@ def get_event_domain(event, fallback=False, return_info=False):
|
||||
domain = None, None
|
||||
else:
|
||||
setattr(event, '_cached_domain' + suffix, domain)
|
||||
return domain if return_info or not isinstance(domain, tuple) else domain[0]
|
||||
return domain if return_mode else domain[0]
|
||||
|
||||
|
||||
def get_organizer_domain(organizer):
|
||||
@@ -81,7 +86,7 @@ def get_organizer_domain(organizer):
|
||||
return None
|
||||
domain = getattr(organizer, '_cached_domain', None) or organizer.cache.get('domain')
|
||||
if domain is None:
|
||||
domains = organizer.domains.filter(event__isnull=True)
|
||||
domains = organizer.domains.filter(event__isnull=True, mode=KnownDomain.MODE_ORG_DOMAIN)
|
||||
domain = domains[0].domainname if domains else None
|
||||
organizer.cache.set('domain', domain or 'none')
|
||||
organizer._cached_domain = domain or 'none'
|
||||
@@ -131,7 +136,8 @@ def eventreverse(obj, name, kwargs=None):
|
||||
:returns: An absolute or relative URL as a string
|
||||
"""
|
||||
from pretix.multidomain import (
|
||||
event_domain_urlconf, maindomain_urlconf, organizer_domain_urlconf,
|
||||
event_domain_urlconf, maindomain_urlconf,
|
||||
organizer_alternative_domain_urlconf, organizer_domain_urlconf,
|
||||
)
|
||||
|
||||
c = None
|
||||
@@ -153,17 +159,24 @@ def eventreverse(obj, name, kwargs=None):
|
||||
raise TypeError('obj should be Event or Organizer')
|
||||
|
||||
if event:
|
||||
domain, domaintype = get_event_domain(obj, fallback=True, return_info=True)
|
||||
domain, domaintype = get_event_domain(obj, fallback=True, return_mode=True)
|
||||
else:
|
||||
domain, domaintype = get_organizer_domain(organizer), "organizer"
|
||||
domain, domaintype = get_organizer_domain(organizer), KnownDomain.MODE_ORG_DOMAIN
|
||||
|
||||
if domain:
|
||||
if domaintype == "event" and 'event' in kwargs:
|
||||
if domaintype == KnownDomain.MODE_EVENT_DOMAIN and 'event' in kwargs:
|
||||
del kwargs['event']
|
||||
if 'organizer' in kwargs:
|
||||
del kwargs['organizer']
|
||||
|
||||
path = reverse(name, kwargs=kwargs, urlconf=event_domain_urlconf if domaintype == "event" else organizer_domain_urlconf)
|
||||
if domaintype == KnownDomain.MODE_EVENT_DOMAIN:
|
||||
urlconf = event_domain_urlconf
|
||||
elif domaintype == KnownDomain.MODE_ORG_ALT_DOMAIN:
|
||||
urlconf = organizer_alternative_domain_urlconf
|
||||
else:
|
||||
urlconf = organizer_domain_urlconf
|
||||
|
||||
path = reverse(name, kwargs=kwargs, urlconf=urlconf)
|
||||
siteurlsplit = urlsplit(settings.SITE_URL)
|
||||
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
|
||||
domain = '%s:%d' % (domain, siteurlsplit.port)
|
||||
|
||||
Reference in New Issue
Block a user