From 3f8295c8cc6630504407231d6262405e2f13c8c2 Mon Sep 17 00:00:00 2001 From: odkhang Date: Wed, 9 Oct 2024 14:53:46 +0700 Subject: [PATCH 01/10] Implement the option to add a video when creating a ticket --- src/pretix/base/models/event.py | 4 + src/pretix/control/forms/event.py | 16 +++- .../eventyay_common/event/settings.html | 4 + .../events/create_foundation.html | 4 + src/pretix/eventyay_common/utils.py | 73 +++++++++++++++++++ src/pretix/eventyay_common/views/event.py | 25 +++++++ 6 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 src/pretix/eventyay_common/utils.py diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index eaec57309..12264fce0 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -433,6 +433,10 @@ class Event(EventMixin, LoggedModel): help_text=_('Only sell tickets for this event on the following sales channels.'), default=['web'], ) + add_video = models.BooleanField( + verbose_name=_('Add video call'), + default=False + ) objects = ScopedManager(organizer='organizer') class Meta: diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index e6bc3838e..5f05d934c 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -48,6 +48,10 @@ class EventWizardFoundationForm(forms.Form): label=_("This is an event series"), required=False, ) + add_video = forms.BooleanField( + label= _("Add video"), + required=False, + ) def __init__(self, *args, **kwargs): self.user = kwargs.pop('user') @@ -139,6 +143,7 @@ def __init__(self, *args, **kwargs): self.organizer = kwargs.pop('organizer') self.locales = kwargs.get('locales') self.has_subevents = kwargs.pop('has_subevents') + self.add_video = kwargs.pop('add_video') self.user = kwargs.pop('user') kwargs.pop('session') super().__init__(*args, **kwargs) @@ -301,7 +306,9 @@ class Meta: class EventUpdateForm(I18nModelForm): - + add_video = forms.BooleanField( + required=False, + ) def __init__(self, *args, **kwargs): self.change_slug = kwargs.pop('change_slug', False) self.domain = kwargs.pop('domain', False) @@ -338,6 +345,10 @@ def __init__(self, *args, **kwargs): widget=forms.CheckboxSelectMultiple ) + self.add_video = self.initial.get('add_video') + if self.add_video: + self.fields['add_video'].disabled = True + def clean_domain(self): d = self.cleaned_data['domain'] if d: @@ -393,7 +404,8 @@ class Meta: 'location', 'geo_lat', 'geo_lon', - 'sales_channels' + 'sales_channels', + 'add_video' ] field_classes = { 'date_from': SplitDateTimeField, diff --git a/src/pretix/eventyay_common/templates/eventyay_common/event/settings.html b/src/pretix/eventyay_common/templates/eventyay_common/event/settings.html index 7acfbcaac..895c007ed 100644 --- a/src/pretix/eventyay_common/templates/eventyay_common/event/settings.html +++ b/src/pretix/eventyay_common/templates/eventyay_common/event/settings.html @@ -23,6 +23,10 @@

{{ request.event.name }} {% trans "- Settings" %}

{% bootstrap_field form.date_to layout="control" %} {% bootstrap_field form.currency layout="control" %} {% bootstrap_field form.sales_channels layout="control" %} +
+ + {% bootstrap_field form.add_video %} +
{% trans "Localization" %} diff --git a/src/pretix/eventyay_common/templates/eventyay_common/events/create_foundation.html b/src/pretix/eventyay_common/templates/eventyay_common/events/create_foundation.html index e62219e65..8959764d2 100644 --- a/src/pretix/eventyay_common/templates/eventyay_common/events/create_foundation.html +++ b/src/pretix/eventyay_common/templates/eventyay_common/events/create_foundation.html @@ -30,6 +30,10 @@ +
+ + {% bootstrap_field form.add_video %} +
{% bootstrap_field form.organizer layout="horizontal" %}
diff --git a/src/pretix/eventyay_common/utils.py b/src/pretix/eventyay_common/utils.py new file mode 100644 index 000000000..0e62ed2d5 --- /dev/null +++ b/src/pretix/eventyay_common/utils.py @@ -0,0 +1,73 @@ +import hashlib +import random +import string +from datetime import datetime, timezone, timedelta +import jwt +import requests +from django.conf import settings +from django.contrib import messages +from django.utils.translation import gettext_lazy as _ +import logging + +logger = logging.getLogger(__name__) + +def generate_token(request): + uid_token = encode_email(request.user.email) + iat = datetime.now(timezone.utc) + exp = iat + timedelta(days=30) + + permissions_list = list(request.user.get_organizer_permission_set(request.organizer)) + is_active_staff_session = request.user.has_active_staff_session(request.session.session_key) + + payload = { + "exp": exp, + "iat": iat, + "uid": uid_token, + "permissions": permissions_list, + "is_active_staff_session": is_active_staff_session + } + token = jwt.encode( + payload, settings.SECRET_KEY, algorithm="HS256" + ) + return token + + +def encode_email(email): + hash_object = hashlib.sha256(email.encode()) + hash_hex = hash_object.hexdigest() + short_hash = hash_hex[:7] + characters = string.ascii_letters + string.digits + random_suffix = ''.join(random.choice(characters) for _ in range(7 - len(short_hash))) + final_result = short_hash + random_suffix + return final_result.upper() + + +def check_create_permission(request): + is_create_permission = request.user.get_organizer_permission_set(request.organizer) + is_active_staff_session= request.user.has_active_staff_session(request.session.session_key) + + if is_create_permission or is_active_staff_session: + return True + return False + +def create_world(request, is_add_video, data): + id = data.get('id') + title = data.get('title') + timezone = data.get('timezone') + locale = data.get('locale') + + if is_add_video and check_create_permission(request): + try: + requests.post("{}/api/v1/create-world/".format(settings.VIDEO_SERVER_HOSTNAME), json={ + "id": id, + "title": title, + "timezone": timezone, + "locale": locale, + }, headers={ + "Authorization": "Bearer " + generate_token(request) + }) + except requests.exceptions.RequestException as e: + logger.error('An error occurred while requesting to create a video: %s' % e) + messages.error(request, _('Cannot create video system for this event. Please try again later.')) + elif is_add_video and not check_create_permission(request): + messages.error(request, _('You do not have permission to create video system')) diff --git a/src/pretix/eventyay_common/views/event.py b/src/pretix/eventyay_common/views/event.py index fdd33ce49..6baf9cfc9 100644 --- a/src/pretix/eventyay_common/views/event.py +++ b/src/pretix/eventyay_common/views/event.py @@ -23,6 +23,7 @@ from pretix.control.views.item import MetaDataEditorMixin from pretix.eventyay_common.forms.event import EventCommonSettingsForm from pretix.eventyay_common.tasks import send_event_webhook +from pretix.eventyay_common.utils import check_create_permission, create_world class EventList(PaginationMixin, ListView): @@ -165,6 +166,8 @@ def done(self, form_list, form_dict, **kwargs): create_for = self.storage.extra_data.get('create_for') + self.request.organizer = foundation_data['organizer'] + if create_for == "talk": event_dict = { 'organiser_slug': foundation_data.get('organizer').slug if foundation_data.get('organizer') else None, @@ -185,6 +188,10 @@ def done(self, form_list, form_dict, **kwargs): event.organizer = foundation_data['organizer'] event.plugins = settings.PRETIX_PLUGINS_DEFAULT event.has_subevents = foundation_data['has_subevents'] + if check_create_permission(self.request): + event.add_video = foundation_data['add_video'] + else: + event.add_video = False event.testmode = True form_dict['basics'].save() @@ -210,6 +217,11 @@ def done(self, form_list, form_dict, **kwargs): } send_event_webhook.delay(user_id=self.request.user.id, event=event_dict, action='create') event.settings.set('create_for', create_for) + + data = dict(id=basics_data.get('slug'), title=basics_data['name'].data, timezone=basics_data['timezone'], + locale=basics_data['locale']) + create_world(self.request, foundation_data['add_video'], data) + return redirect(reverse('eventyay_common:events') + '?congratulations=1') @@ -248,6 +260,19 @@ def form_valid(self, form): self.sform.save() tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk}) + + if check_create_permission(self.request): + form.instance.add_video = form.cleaned_data.get('add_video') + elif not check_create_permission(self.request) and form.cleaned_data.get('add_video'): + form.instance.add_video = True + else: + form.instance.add_video = False + + data = dict(id=form.cleaned_data.get('slug'), title=form.cleaned_data.get('name').data, + timezone=self.sform.cleaned_data.get('timezone'), locale=self.sform.cleaned_data.get('locale')) + + create_world(self.request, form.cleaned_data.get('add_video'), data) + messages.success(self.request, _('Your changes have been saved.')) return super().form_valid(form) From f200e9f298f16f97c6a6a720332dd011d49d7839 Mon Sep 17 00:00:00 2001 From: odkhang Date: Wed, 9 Oct 2024 15:31:30 +0700 Subject: [PATCH 02/10] Optimize code --- src/pretix/eventyay_common/utils.py | 13 ++++++------- src/pretix/eventyay_common/views/event.py | 6 ++++-- src/pretix/settings.py | 3 ++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/pretix/eventyay_common/utils.py b/src/pretix/eventyay_common/utils.py index 0e62ed2d5..8e3abd518 100644 --- a/src/pretix/eventyay_common/utils.py +++ b/src/pretix/eventyay_common/utils.py @@ -11,20 +11,17 @@ logger = logging.getLogger(__name__) +## Generate token for video system def generate_token(request): uid_token = encode_email(request.user.email) iat = datetime.now(timezone.utc) exp = iat + timedelta(days=30) - permissions_list = list(request.user.get_organizer_permission_set(request.organizer)) - is_active_staff_session = request.user.has_active_staff_session(request.session.session_key) - payload = { "exp": exp, "iat": iat, "uid": uid_token, - "permissions": permissions_list, - "is_active_staff_session": is_active_staff_session + "has_permission": check_create_permission(request), } token = jwt.encode( payload, settings.SECRET_KEY, algorithm="HS256" @@ -41,21 +38,23 @@ def encode_email(email): final_result = short_hash + random_suffix return final_result.upper() - +## Check if the user has permission to create videos ('can_create_events' permission) and has admin session mode (admin session mode has full permissions) def check_create_permission(request): - is_create_permission = request.user.get_organizer_permission_set(request.organizer) + is_create_permission = 'can_create_events' in request.user.get_organizer_permission_set(request.organizer) is_active_staff_session= request.user.has_active_staff_session(request.session.session_key) if is_create_permission or is_active_staff_session: return True return False +## user create automatically world when choosing add video option in create ticket form def create_world(request, is_add_video, data): id = data.get('id') title = data.get('title') timezone = data.get('timezone') locale = data.get('locale') + ## check if user choose add video option and has permission to create video system ('can_create_events' permission) if is_add_video and check_create_permission(request): try: requests.post("{}/api/v1/create-world/".format(settings.VIDEO_SERVER_HOSTNAME), json={ diff --git a/src/pretix/eventyay_common/views/event.py b/src/pretix/eventyay_common/views/event.py index 6baf9cfc9..286188657 100644 --- a/src/pretix/eventyay_common/views/event.py +++ b/src/pretix/eventyay_common/views/event.py @@ -218,8 +218,9 @@ def done(self, form_list, form_dict, **kwargs): send_event_webhook.delay(user_id=self.request.user.id, event=event_dict, action='create') event.settings.set('create_for', create_for) - data = dict(id=basics_data.get('slug'), title=basics_data['name'].data, timezone=basics_data['timezone'], - locale=basics_data['locale']) + ## The user automatically creates a world when selecting the add video option in the create ticket form. + data = dict(id=basics_data.get('slug'), title=basics_data.get('name').data, timezone=basics_data.get('timezone'), + locale=basics_data.get('locale')) create_world(self.request, foundation_data['add_video'], data) return redirect(reverse('eventyay_common:events') + '?congratulations=1') @@ -268,6 +269,7 @@ def form_valid(self, form): else: form.instance.add_video = False + ## The user automatically creates a world when selecting the add video option in the update ticket form. data = dict(id=form.cleaned_data.get('slug'), title=form.cleaned_data.get('name').data, timezone=self.sform.cleaned_data.get('timezone'), locale=self.sform.cleaned_data.get('locale')) diff --git a/src/pretix/settings.py b/src/pretix/settings.py index 251e65fc2..b85a38c20 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -113,7 +113,7 @@ } DATABASE_ROUTERS = ['pretix.helpers.database.ReplicaRouter'] -BASE_PATH = config.get('pretix', 'base_path', fallback='/tickets') +BASE_PATH = config.get('pretix', 'base_path', fallback='') FORCE_SCRIPT_NAME = BASE_PATH @@ -132,6 +132,7 @@ PRETIX_SESSION_TIMEOUT_ABSOLUTE = 3600 * 12 PRETIX_PRIMARY_COLOR = '#2185d0' TALK_HOSTNAME = config.get('pretix', 'talk_hostname', fallback='https://wikimania-dev.eventyay.com/') +VIDEO_SERVER_HOSTNAME = config.get('pretix', 'video_server_hostname', fallback='') SITE_URL = config.get('pretix', 'url', fallback='http://localhost') if SITE_URL.endswith('/'): From 2e940cd27dd9c7077b783c1798b6b648777933b7 Mon Sep 17 00:00:00 2001 From: lcduong Date: Wed, 9 Oct 2024 18:36:10 +0700 Subject: [PATCH 03/10] refactor code --- .../base/migrations/0005_event_add_video.py | 18 ++++ src/pretix/base/models/event.py | 3 +- src/pretix/control/forms/event.py | 18 ++-- src/pretix/eventyay_common/forms/event.py | 9 -- .../eventyay_common/event/settings.html | 2 +- .../events/create_foundation.html | 5 +- src/pretix/eventyay_common/utils.py | 88 ++++++++++++------- src/pretix/eventyay_common/views/event.py | 21 ++--- 8 files changed, 101 insertions(+), 63 deletions(-) create mode 100644 src/pretix/base/migrations/0005_event_add_video.py diff --git a/src/pretix/base/migrations/0005_event_add_video.py b/src/pretix/base/migrations/0005_event_add_video.py new file mode 100644 index 000000000..f71b198b5 --- /dev/null +++ b/src/pretix/base/migrations/0005_event_add_video.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2024-10-09 09:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0004_create_customer_table'), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='is_video_create', + field=models.BooleanField(default=False), + ), + ] diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 12264fce0..392c19645 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -433,8 +433,9 @@ class Event(EventMixin, LoggedModel): help_text=_('Only sell tickets for this event on the following sales channels.'), default=['web'], ) - add_video = models.BooleanField( + is_video_create = models.BooleanField( verbose_name=_('Add video call'), + help_text=_('Create Video platform for Event.'), default=False ) objects = ScopedManager(organizer='organizer') diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 5f05d934c..dd13e527c 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -48,9 +48,11 @@ class EventWizardFoundationForm(forms.Form): label=_("This is an event series"), required=False, ) - add_video = forms.BooleanField( - label= _("Add video"), + is_video_create = forms.BooleanField( + label=_("Create Video platform for this Event."), + help_text=_("This will create a new Video platform for this event."), required=False, + widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}) ) def __init__(self, *args, **kwargs): @@ -143,7 +145,7 @@ def __init__(self, *args, **kwargs): self.organizer = kwargs.pop('organizer') self.locales = kwargs.get('locales') self.has_subevents = kwargs.pop('has_subevents') - self.add_video = kwargs.pop('add_video') + self.is_video_create = kwargs.pop('is_video_create') self.user = kwargs.pop('user') kwargs.pop('session') super().__init__(*args, **kwargs) @@ -306,7 +308,7 @@ class Meta: class EventUpdateForm(I18nModelForm): - add_video = forms.BooleanField( + is_video_create = forms.BooleanField( required=False, ) def __init__(self, *args, **kwargs): @@ -345,9 +347,9 @@ def __init__(self, *args, **kwargs): widget=forms.CheckboxSelectMultiple ) - self.add_video = self.initial.get('add_video') - if self.add_video: - self.fields['add_video'].disabled = True + self.is_video_create = self.initial.get('is_video_create') + if self.is_video_create: + self.fields['is_video_create'].disabled = True def clean_domain(self): d = self.cleaned_data['domain'] @@ -405,7 +407,7 @@ class Meta: 'geo_lat', 'geo_lon', 'sales_channels', - 'add_video' + 'is_video_create' ] field_classes = { 'date_from': SplitDateTimeField, diff --git a/src/pretix/eventyay_common/forms/event.py b/src/pretix/eventyay_common/forms/event.py index 9ffd445a5..13d692663 100644 --- a/src/pretix/eventyay_common/forms/event.py +++ b/src/pretix/eventyay_common/forms/event.py @@ -30,12 +30,3 @@ def clean(self): def __init__(self, *args, **kwargs): self.event = kwargs['obj'] super().__init__(*args, **kwargs) - - -class EventWizardCommonFoundationForm(EventWizardFoundationForm): - create_for = forms.MultipleChoiceField( - choices=settings.LANGUAGES, - label=_("Use languages"), - widget=MultipleLanguagesWidget, - help_text=_('Choose all languages that your event should be available in.') - ) diff --git a/src/pretix/eventyay_common/templates/eventyay_common/event/settings.html b/src/pretix/eventyay_common/templates/eventyay_common/event/settings.html index 895c007ed..6cb84bfdc 100644 --- a/src/pretix/eventyay_common/templates/eventyay_common/event/settings.html +++ b/src/pretix/eventyay_common/templates/eventyay_common/event/settings.html @@ -25,7 +25,7 @@

{{ request.event.name }} {% trans "- Settings" %}

{% bootstrap_field form.sales_channels layout="control" %}
- {% bootstrap_field form.add_video %} + {% bootstrap_field form.is_video_create %}
diff --git a/src/pretix/eventyay_common/templates/eventyay_common/events/create_foundation.html b/src/pretix/eventyay_common/templates/eventyay_common/events/create_foundation.html index 8959764d2..a62de93f1 100644 --- a/src/pretix/eventyay_common/templates/eventyay_common/events/create_foundation.html +++ b/src/pretix/eventyay_common/templates/eventyay_common/events/create_foundation.html @@ -30,10 +30,7 @@ -
- - {% bootstrap_field form.add_video %} -
+ {% bootstrap_field form.is_video_create layout="horizontal" %} {% bootstrap_field form.organizer layout="horizontal" %}
diff --git a/src/pretix/eventyay_common/utils.py b/src/pretix/eventyay_common/utils.py index 8e3abd518..ee1069185 100644 --- a/src/pretix/eventyay_common/utils.py +++ b/src/pretix/eventyay_common/utils.py @@ -1,18 +1,24 @@ import hashlib +import logging import random import string -from datetime import datetime, timezone, timedelta +from datetime import datetime, timedelta, timezone + import jwt import requests from django.conf import settings from django.contrib import messages from django.utils.translation import gettext_lazy as _ -import logging logger = logging.getLogger(__name__) -## Generate token for video system + def generate_token(request): + """ + Generate token for video system + @param request: user request + @return: jwt + """ uid_token = encode_email(request.user.email) iat = datetime.now(timezone.utc) exp = iat + timedelta(days=30) @@ -23,9 +29,7 @@ def generate_token(request): "uid": uid_token, "has_permission": check_create_permission(request), } - token = jwt.encode( - payload, settings.SECRET_KEY, algorithm="HS256" - ) + token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") return token @@ -34,39 +38,63 @@ def encode_email(email): hash_hex = hash_object.hexdigest() short_hash = hash_hex[:7] characters = string.ascii_letters + string.digits - random_suffix = ''.join(random.choice(characters) for _ in range(7 - len(short_hash))) + random_suffix = "".join( + random.choice(characters) for _ in range(7 - len(short_hash)) + ) final_result = short_hash + random_suffix return final_result.upper() -## Check if the user has permission to create videos ('can_create_events' permission) and has admin session mode (admin session mode has full permissions) + def check_create_permission(request): - is_create_permission = 'can_create_events' in request.user.get_organizer_permission_set(request.organizer) - is_active_staff_session= request.user.has_active_staff_session(request.session.session_key) + """ + Check if the user has permission to create videos ('can_create_events' permission) and + has admin session mode (admin session mode has full permissions) + @param request: user request + @return: True if user has permission, False otherwise + """ + is_create_permission = ( + "can_create_events" + in request.user.get_organizer_permission_set(request.organizer) + ) + is_active_staff_session = request.user.has_active_staff_session( + request.session.session_key + ) if is_create_permission or is_active_staff_session: return True return False -## user create automatically world when choosing add video option in create ticket form -def create_world(request, is_add_video, data): - id = data.get('id') - title = data.get('title') - timezone = data.get('timezone') - locale = data.get('locale') - ## check if user choose add video option and has permission to create video system ('can_create_events' permission) - if is_add_video and check_create_permission(request): +def create_world(request, is_video_create, data): + """ + Create video system for the event + @param request: user request + @param is_video_create: allow user to add video system + @param data: event's data + """ + event_slug = data.get("id") + title = data.get("title") + event_timezone = data.get("timezone") + locale = data.get("locale") + + # Check if user choose add video option and has permission to create video system ('can_create_events' permission) + if is_video_create and check_create_permission(request): try: - requests.post("{}/api/v1/create-world/".format(settings.VIDEO_SERVER_HOSTNAME), json={ - "id": id, - "title": title, - "timezone": timezone, - "locale": locale, - }, headers={ - "Authorization": "Bearer " + generate_token(request) - }) + requests.post( + "{}/api/v1/create-world/".format(settings.VIDEO_SERVER_HOSTNAME), + json={ + "id": event_slug, + "title": title, + "timezone": event_timezone, + "locale": locale, + }, + headers={"Authorization": "Bearer " + generate_token(request)}, + ) except requests.exceptions.RequestException as e: - logger.error('An error occurred while requesting to create a video: %s' % e) - messages.error(request, _('Cannot create video system for this event. Please try again later.')) - elif is_add_video and not check_create_permission(request): - messages.error(request, _('You do not have permission to create video system')) + logger.error("An error occurred while requesting to create a video: %s" % e) + messages.error( + request, + _("Cannot create video system for this event. Please try again later."), + ) + elif is_video_create and not check_create_permission(request): + messages.error(request, _("You do not have permission to create video system")) diff --git a/src/pretix/eventyay_common/views/event.py b/src/pretix/eventyay_common/views/event.py index 286188657..1d6b072ec 100644 --- a/src/pretix/eventyay_common/views/event.py +++ b/src/pretix/eventyay_common/views/event.py @@ -189,9 +189,9 @@ def done(self, form_list, form_dict, **kwargs): event.plugins = settings.PRETIX_PLUGINS_DEFAULT event.has_subevents = foundation_data['has_subevents'] if check_create_permission(self.request): - event.add_video = foundation_data['add_video'] + event.is_video_create = foundation_data['is_video_create'] else: - event.add_video = False + event.is_video_create = False event.testmode = True form_dict['basics'].save() @@ -218,10 +218,11 @@ def done(self, form_list, form_dict, **kwargs): send_event_webhook.delay(user_id=self.request.user.id, event=event_dict, action='create') event.settings.set('create_for', create_for) - ## The user automatically creates a world when selecting the add video option in the create ticket form. - data = dict(id=basics_data.get('slug'), title=basics_data.get('name').data, timezone=basics_data.get('timezone'), + # The user automatically creates a world when selecting the add video option in the create ticket form. + data = dict(id=basics_data.get('slug'), title=basics_data.get('name').data, + timezone=basics_data.get('timezone'), locale=basics_data.get('locale')) - create_world(self.request, foundation_data['add_video'], data) + create_world(self.request, foundation_data['is_video_create'], data) return redirect(reverse('eventyay_common:events') + '?congratulations=1') @@ -263,17 +264,17 @@ def form_valid(self, form): tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk}) if check_create_permission(self.request): - form.instance.add_video = form.cleaned_data.get('add_video') - elif not check_create_permission(self.request) and form.cleaned_data.get('add_video'): - form.instance.add_video = True + form.instance.is_video_create = form.cleaned_data.get('is_video_create') + elif not check_create_permission(self.request) and form.cleaned_data.get('is_video_create'): + form.instance.is_video_create = True else: - form.instance.add_video = False + form.instance.is_video_create = False ## The user automatically creates a world when selecting the add video option in the update ticket form. data = dict(id=form.cleaned_data.get('slug'), title=form.cleaned_data.get('name').data, timezone=self.sform.cleaned_data.get('timezone'), locale=self.sform.cleaned_data.get('locale')) - create_world(self.request, form.cleaned_data.get('add_video'), data) + create_world(self.request, form.cleaned_data.get('is_video_create'), data) messages.success(self.request, _('Your changes have been saved.')) return super().form_valid(form) From 6e1c32dd0409888faf6fd42b61948b93d70f1f83 Mon Sep 17 00:00:00 2001 From: lcduong Date: Wed, 9 Oct 2024 18:42:06 +0700 Subject: [PATCH 04/10] revert change relate to base path --- src/pretix/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pretix/settings.py b/src/pretix/settings.py index b85a38c20..e3290f1ce 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -113,7 +113,7 @@ } DATABASE_ROUTERS = ['pretix.helpers.database.ReplicaRouter'] -BASE_PATH = config.get('pretix', 'base_path', fallback='') +BASE_PATH = config.get('pretix', 'base_path', fallback='/tickets') FORCE_SCRIPT_NAME = BASE_PATH @@ -132,7 +132,7 @@ PRETIX_SESSION_TIMEOUT_ABSOLUTE = 3600 * 12 PRETIX_PRIMARY_COLOR = '#2185d0' TALK_HOSTNAME = config.get('pretix', 'talk_hostname', fallback='https://wikimania-dev.eventyay.com/') -VIDEO_SERVER_HOSTNAME = config.get('pretix', 'video_server_hostname', fallback='') +VIDEO_SERVER_HOSTNAME = config.get('pretix', 'video_server_hostname', fallback='https://app.eventyay.com/video') SITE_URL = config.get('pretix', 'url', fallback='http://localhost') if SITE_URL.endswith('/'): From 0532a4c6abf34adf07de0a5014a72fa79f72f4af Mon Sep 17 00:00:00 2001 From: odkhang Date: Thu, 10 Oct 2024 15:47:34 +0700 Subject: [PATCH 05/10] Implement Celery for the create_world API --- src/pretix/eventyay_common/tasks.py | 42 +++++++++++++++++++++++ src/pretix/eventyay_common/utils.py | 38 +------------------- src/pretix/eventyay_common/views/event.py | 14 ++++---- 3 files changed, 51 insertions(+), 43 deletions(-) diff --git a/src/pretix/eventyay_common/tasks.py b/src/pretix/eventyay_common/tasks.py index e5e15853e..27b7d9dd0 100644 --- a/src/pretix/eventyay_common/tasks.py +++ b/src/pretix/eventyay_common/tasks.py @@ -103,6 +103,48 @@ def send_event_webhook(self, user_id, event, action): except self.MaxRetriesExceededError: logger.error("Max retries exceeded for sending organizer webhook.") +@shared_task(bind=True, max_retries=5, default_retry_delay=60) # Retries up to 5 times with a 60-second delay +def create_world(self, is_video_create, data): + """ + Create video system for the event + @self: task instance + @param is_video_create: allow user to add video system + @param data: event's data + """ + event_slug = data.get("id") + title = data.get("title") + event_timezone = data.get("timezone") + locale = data.get("locale") + token = data.get("token") + has_permission = data.get("has_permission") + + payload = { + 'id': event_slug, + 'title': title, + 'timezone': event_timezone, + 'locale': locale, + } + + headers = { + "Authorization": "Bearer " + token + } + + # Check if user choose add video option and has permission to create video system ('can_create_events' permission) + if is_video_create and has_permission: + try: + requests.post( + "{}/api/v1/create-world/".format(settings.VIDEO_SERVER_HOSTNAME), + json= payload, + headers= headers, + ) + except requests.RequestException as e: + # Log any errors that occur + logger.error('An error occurred while requesting to create a video: %s', e) + try: + self.retry(exc=e) + except self.MaxRetriesExceededError: + logger.error("Max retries exceeded for sending organizer webhook.") + def get_header_token(user_id): # Fetch the user and organizer instances diff --git a/src/pretix/eventyay_common/utils.py b/src/pretix/eventyay_common/utils.py index ee1069185..8fa86fd98 100644 --- a/src/pretix/eventyay_common/utils.py +++ b/src/pretix/eventyay_common/utils.py @@ -5,10 +5,8 @@ from datetime import datetime, timedelta, timezone import jwt -import requests from django.conf import settings -from django.contrib import messages -from django.utils.translation import gettext_lazy as _ + logger = logging.getLogger(__name__) @@ -64,37 +62,3 @@ def check_create_permission(request): return True return False - -def create_world(request, is_video_create, data): - """ - Create video system for the event - @param request: user request - @param is_video_create: allow user to add video system - @param data: event's data - """ - event_slug = data.get("id") - title = data.get("title") - event_timezone = data.get("timezone") - locale = data.get("locale") - - # Check if user choose add video option and has permission to create video system ('can_create_events' permission) - if is_video_create and check_create_permission(request): - try: - requests.post( - "{}/api/v1/create-world/".format(settings.VIDEO_SERVER_HOSTNAME), - json={ - "id": event_slug, - "title": title, - "timezone": event_timezone, - "locale": locale, - }, - headers={"Authorization": "Bearer " + generate_token(request)}, - ) - except requests.exceptions.RequestException as e: - logger.error("An error occurred while requesting to create a video: %s" % e) - messages.error( - request, - _("Cannot create video system for this event. Please try again later."), - ) - elif is_video_create and not check_create_permission(request): - messages.error(request, _("You do not have permission to create video system")) diff --git a/src/pretix/eventyay_common/views/event.py b/src/pretix/eventyay_common/views/event.py index 1d6b072ec..2e61321a7 100644 --- a/src/pretix/eventyay_common/views/event.py +++ b/src/pretix/eventyay_common/views/event.py @@ -22,8 +22,8 @@ from pretix.control.views.event import DecoupleMixin, EventSettingsViewMixin from pretix.control.views.item import MetaDataEditorMixin from pretix.eventyay_common.forms.event import EventCommonSettingsForm -from pretix.eventyay_common.tasks import send_event_webhook -from pretix.eventyay_common.utils import check_create_permission, create_world +from pretix.eventyay_common.tasks import send_event_webhook, create_world +from pretix.eventyay_common.utils import check_create_permission, generate_token class EventList(PaginationMixin, ListView): @@ -221,8 +221,9 @@ def done(self, form_list, form_dict, **kwargs): # The user automatically creates a world when selecting the add video option in the create ticket form. data = dict(id=basics_data.get('slug'), title=basics_data.get('name').data, timezone=basics_data.get('timezone'), - locale=basics_data.get('locale')) - create_world(self.request, foundation_data['is_video_create'], data) + locale=basics_data.get('locale'), has_permission=check_create_permission(self.request), + token=generate_token(self.request)) + create_world.delay(is_video_create=foundation_data.get('is_video_create'), data=data) return redirect(reverse('eventyay_common:events') + '?congratulations=1') @@ -272,9 +273,10 @@ def form_valid(self, form): ## The user automatically creates a world when selecting the add video option in the update ticket form. data = dict(id=form.cleaned_data.get('slug'), title=form.cleaned_data.get('name').data, - timezone=self.sform.cleaned_data.get('timezone'), locale=self.sform.cleaned_data.get('locale')) + timezone=self.sform.cleaned_data.get('timezone'), locale=self.sform.cleaned_data.get('locale'), + has_permission=check_create_permission(self.request), token=generate_token(self.request)) - create_world(self.request, form.cleaned_data.get('is_video_create'), data) + create_world.delay(is_video_create=form.cleaned_data.get('is_video_create'), data=data) messages.success(self.request, _('Your changes have been saved.')) return super().form_valid(form) From c04ae9920db4791b76454c11738ed9630f6ba2a0 Mon Sep 17 00:00:00 2001 From: odkhang Date: Tue, 15 Oct 2024 16:37:51 +0700 Subject: [PATCH 06/10] Code refactoring --- ...create_event_is_video_creation_and_more.py | 18 + src/pretix/base/models/event.py | 876 +++++++---- src/pretix/control/forms/event.py | 1379 +++++++++-------- src/pretix/eventyay_common/tasks.py | 133 +- .../eventyay_common/event/settings.html | 2 +- .../events/create_foundation.html | 2 +- src/pretix/eventyay_common/utils.py | 14 +- src/pretix/eventyay_common/views/event.py | 350 +++-- 8 files changed, 1640 insertions(+), 1134 deletions(-) create mode 100644 src/pretix/base/migrations/0006_rename_is_video_create_event_is_video_creation_and_more.py diff --git a/src/pretix/base/migrations/0006_rename_is_video_create_event_is_video_creation_and_more.py b/src/pretix/base/migrations/0006_rename_is_video_create_event_is_video_creation_and_more.py new file mode 100644 index 000000000..4a3c107a4 --- /dev/null +++ b/src/pretix/base/migrations/0006_rename_is_video_create_event_is_video_creation_and_more.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2024-10-15 08:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("pretixbase", "0005_event_add_video"), + ] + + operations = [ + migrations.RenameField( + model_name="event", + old_name="is_video_create", + new_name="is_video_creation", + ) + ] diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 392c19645..66e1b72b7 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -42,10 +42,22 @@ class EventMixin: def clean(self): - if self.presale_start and self.presale_end and self.presale_start > self.presale_end: - raise ValidationError({'presale_end': _('The end of the presale period has to be later than its start.')}) + if ( + self.presale_start + and self.presale_end + and self.presale_start > self.presale_end + ): + raise ValidationError( + { + "presale_end": _( + "The end of the presale period has to be later than its start." + ) + } + ) if self.date_from and self.date_to and self.date_from > self.date_to: - raise ValidationError({'date_to': _('The end of the event has to be later than its start.')}) + raise ValidationError( + {"date_to": _("The end of the event has to be later than its start.")} + ) super().clean() def get_short_date_from_display(self, tz=None, show_times=True) -> str: @@ -56,7 +68,11 @@ def get_short_date_from_display(self, tz=None, show_times=True) -> str: tz = tz or self.timezone return _date( self.date_from.astimezone(tz), - "SHORT_DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT" + ( + "SHORT_DATETIME_FORMAT" + if self.settings.show_times and show_times + else "DATE_FORMAT" + ), ) def get_short_date_to_display(self, tz=None) -> str: @@ -70,7 +86,7 @@ def get_short_date_to_display(self, tz=None) -> str: return "" return _date( self.date_to.astimezone(tz), - "SHORT_DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT" + "SHORT_DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT", ) def get_date_from_display(self, tz=None, show_times=True, short=False) -> str: @@ -81,7 +97,12 @@ def get_date_from_display(self, tz=None, show_times=True, short=False) -> str: tz = tz or self.timezone return _date( self.date_from.astimezone(tz), - ("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT") + ("SHORT_" if short else "") + + ( + "DATETIME_FORMAT" + if self.settings.show_times and show_times + else "DATE_FORMAT" + ), ) def get_time_from_display(self, tz=None) -> str: @@ -90,9 +111,7 @@ def get_time_from_display(self, tz=None) -> str: the ``show_times`` setting. """ tz = tz or self.timezone - return _date( - self.date_from.astimezone(tz), "TIME_FORMAT" - ) + return _date(self.date_from.astimezone(tz), "TIME_FORMAT") def get_date_to_display(self, tz=None, show_times=True, short=False) -> str: """ @@ -105,7 +124,12 @@ def get_date_to_display(self, tz=None, show_times=True, short=False) -> str: return "" return _date( self.date_to.astimezone(tz), - ("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT") + ("SHORT_" if short else "") + + ( + "DATETIME_FORMAT" + if self.settings.show_times and show_times + else "DATE_FORMAT" + ), ) def get_date_range_display(self, tz=None, force_show_end=False) -> str: @@ -131,7 +155,11 @@ def effective_presale_end(self): """ if isinstance(self, SubEvent): presale_ends = [self.presale_end, self.event.presale_end] - return min(filter(lambda x: x is not None, presale_ends)) if any(presale_ends) else None + return ( + min(filter(lambda x: x is not None, presale_ends)) + if any(presale_ends) + else None + ) else: return self.presale_end @@ -145,7 +173,10 @@ def presale_has_ended(self): elif self.date_to: return now() > self.date_to else: - return now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date() + return ( + now().astimezone(self.timezone).date() + > self.date_from.astimezone(self.timezone).date() + ) @property def effective_presale_start(self): @@ -155,7 +186,11 @@ def effective_presale_start(self): """ if isinstance(self, SubEvent): presale_starts = [self.presale_start, self.event.presale_start] - return max(filter(lambda x: x is not None, presale_starts)) if any(presale_starts) else None + return ( + max(filter(lambda x: x is not None, presale_starts)) + if any(presale_starts) + else None + ) else: return self.presale_start @@ -175,15 +210,16 @@ def event_microdata(self): eventdict = { "@context": "http://schema.org", - "@type": "Event", "location": { + "@type": "Event", + "location": { "@type": "Place", "address": str(self.location), }, "name": str(self.name), } - img = getattr(self, 'event', self).social_image + img = getattr(self, "event", self).social_image if img: - eventdict['image'] = img + eventdict["image"] = img if self.settings.show_times: eventdict["startDate"] = self.date_from.isoformat() @@ -197,40 +233,62 @@ def event_microdata(self): return safe_string(json.dumps(eventdict)) @classmethod - def annotated(cls, qs, channel='web'): + def annotated(cls, qs, channel="web"): from pretix.base.models import Item, ItemVariation, Quota - sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel).filter( - Q(variations__isnull=True) - & Q(quotas__pk=OuterRef('pk')) - ).order_by().values_list('quotas__pk').annotate( - items=GroupConcat('pk', delimiter=',') - ).values('items') - sq_active_variation = ItemVariation.objects.filter( - Q(active=True) - & Q(item__active=True) - & Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=now())) - & Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=now())) - & Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False)) - & Q(item__sales_channels__contains=channel) - & Q(item__hide_without_voucher=False) - & Q(item__require_bundling=False) - & Q(quotas__pk=OuterRef('pk')) - ).order_by().values_list('quotas__pk').annotate( - items=GroupConcat('pk', delimiter=',') - ).values('items') + sq_active_item = ( + Item.objects.using(settings.DATABASE_REPLICA) + .filter_available(channel=channel) + .filter(Q(variations__isnull=True) & Q(quotas__pk=OuterRef("pk"))) + .order_by() + .values_list("quotas__pk") + .annotate(items=GroupConcat("pk", delimiter=",")) + .values("items") + ) + sq_active_variation = ( + ItemVariation.objects.filter( + Q(active=True) + & Q(item__active=True) + & Q( + Q(item__available_from__isnull=True) + | Q(item__available_from__lte=now()) + ) + & Q( + Q(item__available_until__isnull=True) + | Q(item__available_until__gte=now()) + ) + & Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False)) + & Q(item__sales_channels__contains=channel) + & Q(item__hide_without_voucher=False) + & Q(item__require_bundling=False) + & Q(quotas__pk=OuterRef("pk")) + ) + .order_by() + .values_list("quotas__pk") + .annotate(items=GroupConcat("pk", delimiter=",")) + .values("items") + ) return qs.annotate( - has_paid_item=Exists(Item.objects.filter(event_id=OuterRef(cls._event_id), default_price__gt=0)) + has_paid_item=Exists( + Item.objects.filter( + event_id=OuterRef(cls._event_id), default_price__gt=0 + ) + ) ).prefetch_related( Prefetch( - 'quotas', - to_attr='active_quotas', - queryset=Quota.objects.using(settings.DATABASE_REPLICA).annotate( - active_items=Subquery(sq_active_item, output_field=models.TextField()), - active_variations=Subquery(sq_active_variation, output_field=models.TextField()), - ).exclude( - Q(active_items="") & Q(active_variations="") - ).select_related('event', 'subevent') + "quotas", + to_attr="active_quotas", + queryset=Quota.objects.using(settings.DATABASE_REPLICA) + .annotate( + active_items=Subquery( + sq_active_item, output_field=models.TextField() + ), + active_variations=Subquery( + sq_active_variation, output_field=models.TextField() + ), + ) + .exclude(Q(active_items="") & Q(active_variations="")) + .select_related("event", "subevent"), ) ) @@ -238,8 +296,10 @@ def annotated(cls, qs, channel='web'): def best_availability_state(self): from .items import Quota - if not hasattr(self, 'active_quotas'): - raise TypeError("Call this only if you fetched the subevents via Event/SubEvent.annotated()") + if not hasattr(self, "active_quotas"): + raise TypeError( + "Call this only if you fetched the subevents via Event/SubEvent.annotated()" + ) items_available = set() vars_available = set() items_reserved = set() @@ -247,7 +307,7 @@ def best_availability_state(self): items_gone = set() vars_gone = set() - r = getattr(self, '_quota_cache', {}) + r = getattr(self, "_quota_cache", {}) for q in self.active_quotas: res = r[q] if q in r else q.availability(allow_cache=True) @@ -268,20 +328,28 @@ def best_availability_state(self): vars_gone.update(q.active_variations.split(",")) if not self.active_quotas: return None - if items_available - items_reserved - items_gone or vars_available - vars_reserved - vars_gone: + if ( + items_available - items_reserved - items_gone + or vars_available - vars_reserved - vars_gone + ): return Quota.AVAILABILITY_OK if items_reserved - items_gone or vars_reserved - vars_gone: return Quota.AVAILABILITY_RESERVED return Quota.AVAILABILITY_GONE - def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False): + def free_seats( + self, ignore_voucher=None, sales_channel="web", include_blocked=False + ): qs_annotated = self._seats(ignore_voucher=ignore_voucher) qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False) if self.settings.seating_minimal_distance > 0: qs = qs.filter(has_closeby_taken=False) - if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked): + if not ( + sales_channel in self.settings.seating_allow_blocked_seats_for_channel + or include_blocked + ): qs = qs.filter(blocked=False) return qs @@ -293,17 +361,13 @@ def taken_seats(self, ignore_voucher=None): def blocked_seats(self, ignore_voucher=None): qs = self._seats(ignore_voucher=ignore_voucher) - q = ( - Q(has_cart=True) - | Q(has_voucher=True) - | Q(blocked=True) - ) + q = Q(has_cart=True) | Q(has_voucher=True) | Q(blocked=True) if self.settings.seating_minimal_distance > 0: q |= Q(has_closeby_taken=True, has_order=False) return qs.filter(q) -@settings_hierarkey.add(parent_field='organizer', cache_namespace='event') +@settings_hierarkey.add(parent_field="organizer", cache_namespace="event") class Event(EventMixin, LoggedModel): """ This model represents an event. An event is anything you can buy @@ -341,110 +405,137 @@ class Event(EventMixin, LoggedModel): :type sales_channels: list """ - settings_namespace = 'event' - _event_id = 'pk' - CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES] - organizer = models.ForeignKey(Organizer, related_name="events", on_delete=models.PROTECT) + settings_namespace = "event" + _event_id = "pk" + CURRENCY_CHOICES = [ + (c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES + ] + organizer = models.ForeignKey( + Organizer, related_name="events", on_delete=models.PROTECT + ) testmode = models.BooleanField(default=False) name = I18nCharField( max_length=200, verbose_name=_("Event name"), ) slug = models.CharField( - max_length=50, db_index=True, + max_length=50, + db_index=True, help_text=_( "Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be unique among your " "events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily " "remembered, but you can also choose to use a random value. " - "This will be used in URLs, order codes, invoice numbers, and bank transfer references."), + "This will be used in URLs, order codes, invoice numbers, and bank transfer references." + ), validators=[ MinLengthValidator( limit_value=2, ), RegexValidator( regex="^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$", - message=_("The slug may only contain letters, numbers, dots and dashes."), + message=_( + "The slug may only contain letters, numbers, dots and dashes." + ), ), - EventSlugBanlistValidator() + EventSlugBanlistValidator(), ], verbose_name=_("Short form"), ) live = models.BooleanField(default=False, verbose_name=_("Shop is live")) - currency = models.CharField(max_length=10, - verbose_name=_("Event currency"), - choices=CURRENCY_CHOICES, - default=settings.DEFAULT_CURRENCY) + currency = models.CharField( + max_length=10, + verbose_name=_("Event currency"), + choices=CURRENCY_CHOICES, + default=settings.DEFAULT_CURRENCY, + ) date_from = models.DateTimeField(verbose_name=_("Event start time")) - date_to = models.DateTimeField(null=True, blank=True, - verbose_name=_("Event end time")) - date_admission = models.DateTimeField(null=True, blank=True, - verbose_name=_("Admission time")) - is_public = models.BooleanField(default=True, - verbose_name=_("Show in lists"), - help_text=_("If selected, this event will show up publicly on the list of events for your organizer account.")) + date_to = models.DateTimeField( + null=True, blank=True, verbose_name=_("Event end time") + ) + date_admission = models.DateTimeField( + null=True, blank=True, verbose_name=_("Admission time") + ) + is_public = models.BooleanField( + default=True, + verbose_name=_("Show in lists"), + help_text=_( + "If selected, this event will show up publicly on the list of events for your organizer account." + ), + ) presale_end = models.DateTimeField( - null=True, blank=True, + null=True, + blank=True, verbose_name=_("End of presale"), - help_text=_("Optional. No products will be sold after this date. If you do not set this value, the presale " - "will end after the end date of your event."), + help_text=_( + "Optional. No products will be sold after this date. If you do not set this value, the presale " + "will end after the end date of your event." + ), ) presale_start = models.DateTimeField( - null=True, blank=True, + null=True, + blank=True, verbose_name=_("Start of presale"), help_text=_("Optional. No products will be sold before this date."), ) location = I18nTextField( - null=True, blank=True, + null=True, + blank=True, max_length=200, verbose_name=_("Location"), ) geo_lat = models.FloatField( verbose_name=_("Latitude"), - null=True, blank=True, + null=True, + blank=True, validators=[ MinValueValidator(-90), MaxValueValidator(90), - ] + ], ) geo_lon = models.FloatField( verbose_name=_("Longitude"), - null=True, blank=True, + null=True, + blank=True, validators=[ MinValueValidator(-180), MaxValueValidator(180), - ] + ], ) plugins = models.TextField( - null=False, blank=True, + null=False, + blank=True, verbose_name=_("Plugins"), ) comment = models.TextField( - verbose_name=_("Internal comment"), - null=True, blank=True + verbose_name=_("Internal comment"), null=True, blank=True ) - has_subevents = models.BooleanField( - verbose_name=_('Event series'), - default=False + has_subevents = models.BooleanField(verbose_name=_("Event series"), default=False) + seating_plan = models.ForeignKey( + "SeatingPlan", + on_delete=models.PROTECT, + null=True, + blank=True, + related_name="events", ) - seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True, - related_name='events') sales_channels = MultiStringField( - verbose_name=_('Restrict to specific sales channels'), - help_text=_('Only sell tickets for this event on the following sales channels.'), - default=['web'], + verbose_name=_("Restrict to specific sales channels"), + help_text=_( + "Only sell tickets for this event on the following sales channels." + ), + default=["web"], ) - is_video_create = models.BooleanField( - verbose_name=_('Add video call'), - help_text=_('Create Video platform for Event.'), - default=False + is_video_creation = models.BooleanField( + verbose_name=_("Add video call"), + help_text=_("Create Video platform for Event."), + default=False, ) - objects = ScopedManager(organizer='organizer') + objects = ScopedManager(organizer="organizer") class Meta: verbose_name = _("Event") verbose_name_plural = _("Events") ordering = ("date_from", "name") - unique_together = (('organizer', 'slug'),) + unique_together = (("organizer", "slug"),) def __str__(self): return str(self.name) @@ -454,35 +545,39 @@ def set_defaults(self): This will be called after event creation, but only if the event was not created by copying an existing one. This way, we can use this to introduce new default settings to pretix that do not affect existing events. """ - self.settings.invoice_renderer = 'modern1' + self.settings.invoice_renderer = "modern1" self.settings.invoice_include_expire_date = True self.settings.ticketoutput_pdf__enabled = True self.settings.ticketoutput_passbook__enabled = True - self.settings.event_list_type = 'calendar' + self.settings.event_list_type = "calendar" self.settings.invoice_email_attachment = True - self.settings.name_scheme = 'given_family' + self.settings.name_scheme = "given_family" @property def social_image(self): from pretix.multidomain.urlreverse import build_absolute_uri img = None - logo_file = self.settings.get('logo_image', as_type=str, default='')[7:] - og_file = self.settings.get('og_image', as_type=str, default='')[7:] + logo_file = self.settings.get("logo_image", as_type=str, default="")[7:] + og_file = self.settings.get("og_image", as_type=str, default="")[7:] if og_file: - img = get_thumbnail(og_file, '1200').thumb.url + img = get_thumbnail(og_file, "1200").thumb.url elif logo_file: - img = get_thumbnail(logo_file, '5000x120').thumb.url + img = get_thumbnail(logo_file, "5000x120").thumb.url if img: - return urljoin(build_absolute_uri(self, 'presale:event.index'), img) + return urljoin(build_absolute_uri(self, "presale:event.index"), img) def _seats(self, ignore_voucher=None): from .seating import Seat - qs_annotated = Seat.annotated(self.seats, self.pk, None, - ignore_voucher_id=ignore_voucher.pk if ignore_voucher else None, - minimal_distance=self.settings.seating_minimal_distance, - distance_only_within_row=self.settings.seating_distance_within_row) + qs_annotated = Seat.annotated( + self.seats, + self.pk, + None, + ignore_voucher_id=ignore_voucher.pk if ignore_voucher else None, + minimal_distance=self.settings.seating_minimal_distance, + distance_only_within_row=self.settings.seating_distance_within_row, + ) return qs_annotated @@ -561,26 +656,32 @@ def get_mail_backend(self, timeout=None, force_custom=False): gs = GlobalSettingsObject() if self.settings.smtp_use_custom or force_custom: - if self.settings.email_vendor == 'sendgrid': + if self.settings.email_vendor == "sendgrid": return SendGridEmail(api_key=self.settings.send_grid_api_key) - return CustomSMTPBackend(host=self.settings.smtp_host, - port=self.settings.smtp_port, - username=self.settings.smtp_username, - password=self.settings.smtp_password, - use_tls=self.settings.smtp_use_tls, - use_ssl=self.settings.smtp_use_ssl, - fail_silently=False, timeout=timeout) + return CustomSMTPBackend( + host=self.settings.smtp_host, + port=self.settings.smtp_port, + username=self.settings.smtp_username, + password=self.settings.smtp_password, + use_tls=self.settings.smtp_use_tls, + use_ssl=self.settings.smtp_use_ssl, + fail_silently=False, + timeout=timeout, + ) elif gs.settings.email_vendor is not None: - if gs.settings.email_vendor == 'sendgrid': + if gs.settings.email_vendor == "sendgrid": return SendGridEmail(api_key=gs.settings.send_grid_api_key) else: - CustomSMTPBackend(host=gs.settings.smtp_host, - port=gs.settings.smtp_port, - username=gs.settings.smtp_username, - password=gs.settings.smtp_password, - use_tls=gs.settings.smtp_use_tls, - use_ssl=gs.settings.smtp_use_ssl, - fail_silently=False, timeout=timeout) + CustomSMTPBackend( + host=gs.settings.smtp_host, + port=gs.settings.smtp_port, + username=gs.settings.smtp_username, + password=gs.settings.smtp_password, + use_tls=gs.settings.smtp_use_tls, + use_ssl=gs.settings.smtp_use_ssl, + fail_silently=False, + timeout=timeout, + ) else: return get_connection(fail_silently=False) @@ -590,10 +691,15 @@ def payment_term_last(self): The last datetime of payments for this event. """ tz = pytz.timezone(self.settings.timezone) - return make_aware(datetime.combine( - self.settings.get('payment_term_last', as_type=RelativeDateWrapper).datetime(self).date(), - time(hour=23, minute=59, second=59) - ), tz) + return make_aware( + datetime.combine( + self.settings.get("payment_term_last", as_type=RelativeDateWrapper) + .datetime(self) + .date(), + time(hour=23, minute=59, second=59), + ), + tz, + ) def copy_data_from(self, other): from ..signals import event_copy_data @@ -605,10 +711,14 @@ def copy_data_from(self, other): self.plugins = other.plugins self.is_public = other.is_public if other.date_admission: - self.date_admission = self.date_from + (other.date_admission - other.date_from) + self.date_admission = self.date_from + ( + other.date_admission - other.date_from + ) self.testmode = other.testmode self.save() - self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk}) + self.log_action( + "pretix.object.cloned", data={"source": other.slug, "source_id": other.pk} + ) tax_map = {} for t in other.tax_rules.all(): @@ -616,7 +726,7 @@ def copy_data_from(self, other): t.pk = None t.event = self t.save() - t.log_action('pretix.object.cloned') + t.log_action("pretix.object.cloned") category_map = {} for c in ItemCategory.objects.filter(event=other): @@ -624,7 +734,7 @@ def copy_data_from(self, other): c.pk = None c.event = self c.save() - c.log_action('pretix.object.cloned') + c.log_action("pretix.object.cloned") item_meta_properties_map = {} for imp in other.item_meta_properties.all(): @@ -632,11 +742,11 @@ def copy_data_from(self, other): imp.pk = None imp.event = self imp.save() - imp.log_action('pretix.object.cloned') + imp.log_action("pretix.object.cloned") item_map = {} variation_map = {} - for i in Item.objects.filter(event=other).prefetch_related('variations'): + for i in Item.objects.filter(event=other).prefetch_related("variations"): vars = list(i.variations.all()) item_map[i.pk] = i i.pk = None @@ -648,26 +758,32 @@ def copy_data_from(self, other): if i.tax_rule_id: i.tax_rule = tax_map[i.tax_rule_id] i.save() - i.log_action('pretix.object.cloned') + i.log_action("pretix.object.cloned") for v in vars: variation_map[v.pk] = v v.pk = None v.item = i v.save() - for imv in ItemMetaValue.objects.filter(item__event=other).prefetch_related('item', 'property'): + for imv in ItemMetaValue.objects.filter(item__event=other).prefetch_related( + "item", "property" + ): imv.pk = None imv.property = item_meta_properties_map[imv.property.pk] imv.item = item_map[imv.item.pk] imv.save() - for ia in ItemAddOn.objects.filter(base_item__event=other).prefetch_related('base_item', 'addon_category'): + for ia in ItemAddOn.objects.filter(base_item__event=other).prefetch_related( + "base_item", "addon_category" + ): ia.pk = None ia.base_item = item_map[ia.base_item.pk] ia.addon_category = category_map[ia.addon_category.pk] ia.save() - for ia in ItemBundle.objects.filter(base_item__event=other).prefetch_related('base_item', 'bundled_item', 'bundled_variation'): + for ia in ItemBundle.objects.filter(base_item__event=other).prefetch_related( + "base_item", "bundled_item", "bundled_variation" + ): ia.pk = None ia.base_item = item_map[ia.base_item.pk] ia.bundled_item = item_map[ia.bundled_item.pk] @@ -675,7 +791,9 @@ def copy_data_from(self, other): ia.bundled_variation = variation_map[ia.bundled_variation.pk] ia.save() - for q in Quota.objects.filter(event=other, subevent__isnull=True).prefetch_related('items', 'variations'): + for q in Quota.objects.filter( + event=other, subevent__isnull=True + ).prefetch_related("items", "variations"): items = list(q.items.all()) vars = list(q.variations.all()) oldid = q.pk @@ -683,23 +801,27 @@ def copy_data_from(self, other): q.event = self q.closed = False q.save() - q.log_action('pretix.object.cloned') + q.log_action("pretix.object.cloned") for i in items: if i.pk in item_map: q.items.add(item_map[i.pk]) for v in vars: q.variations.add(variation_map[v.pk]) - self.items.filter(hidden_if_available_id=oldid).update(hidden_if_available=q) + self.items.filter(hidden_if_available_id=oldid).update( + hidden_if_available=q + ) question_map = {} - for q in Question.objects.filter(event=other).prefetch_related('items', 'options'): + for q in Question.objects.filter(event=other).prefetch_related( + "items", "options" + ): items = list(q.items.all()) opts = list(q.options.all()) question_map[q.pk] = q q.pk = None q.event = self q.save() - q.log_action('pretix.object.cloned') + q.log_action("pretix.object.cloned") for i in items: q.items.add(item_map[i.pk]) @@ -710,8 +832,8 @@ def copy_data_from(self, other): for q in self.questions.filter(dependency_question__isnull=False): q.dependency_question = question_map[q.dependency_question_id] - q.save(update_fields=['dependency_question']) - + q.save(update_fields=["dependency_question"]) + # Copy event footer link for footerLink in EventFooterLinkModel.objects.filter(event=other): footerLink.pk = None @@ -721,11 +843,19 @@ def copy_data_from(self, other): def _walk_rules(rules): if isinstance(rules, dict): for k, v in rules.items(): - if k == 'lookup': - if v[0] == 'product': - v[1] = str(item_map.get(int(v[1]), 0).pk) if int(v[1]) in item_map else "0" - elif v[0] == 'variation': - v[1] = str(variation_map.get(int(v[1]), 0).pk) if int(v[1]) in variation_map else "0" + if k == "lookup": + if v[0] == "product": + v[1] = ( + str(item_map.get(int(v[1]), 0).pk) + if int(v[1]) in item_map + else "0" + ) + elif v[0] == "variation": + v[1] = ( + str(variation_map.get(int(v[1]), 0).pk) + if int(v[1]) in variation_map + else "0" + ) else: _walk_rules(v) elif isinstance(rules, list): @@ -733,7 +863,9 @@ def _walk_rules(rules): _walk_rules(i) checkin_list_map = {} - for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'): + for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related( + "limit_products" + ): items = list(cl.limit_products.all()) checkin_list_map[cl.pk] = cl cl.pk = None @@ -742,7 +874,7 @@ def _walk_rules(rules): _walk_rules(rules) cl.rules = rules cl.save() - cl.log_action('pretix.object.cloned') + cl.log_action("pretix.object.cloned") for i in items: cl.limit_products.add(item_map[i.pk]) @@ -750,7 +882,9 @@ def _walk_rules(rules): if other.seating_plan.organizer_id == self.organizer_id: self.seating_plan = other.seating_plan else: - self.organizer.seating_plans.create(name=other.seating_plan.name, layout=other.seating_plan.layout) + self.organizer.seating_plans.create( + name=other.seating_plan.name, layout=other.seating_plan.layout + ) self.save() for m in other.seat_category_mappings.filter(subevent__isnull=True): @@ -767,8 +901,8 @@ def _walk_rules(rules): s.save() skip_settings = ( - 'ticket_secrets_pretix_sig1_pubkey', - 'ticket_secrets_pretix_sig1_privkey', + "ticket_secrets_pretix_sig1_pubkey", + "ticket_secrets_pretix_sig1_privkey", ) for s in other.settings._objects.all(): if s.key in skip_settings: @@ -776,17 +910,21 @@ def _walk_rules(rules): s.object = self s.pk = None - if s.value.startswith('file://'): - fi = default_storage.open(s.value[7:], 'rb') + if s.value.startswith("file://"): + fi = default_storage.open(s.value[7:], "rb") nonce = get_random_string(length=8) # TODO: make sure pub is always correct - fname = 'pub/%s/%s/%s.%s.%s' % ( - self.organizer.slug, self.slug, s.key, nonce, s.value.split('.')[-1] + fname = "pub/%s/%s/%s.%s.%s" % ( + self.organizer.slug, + self.slug, + s.key, + nonce, + s.value.split(".")[-1], ) newname = default_storage.save(fname, fi) - s.value = 'file://' + newname + s.value = "file://" + newname s.save() - elif s.key == 'tax_rate_default': + elif s.key == "tax_rate_default": try: if int(s.value) in tax_map: s.value = tax_map.get(int(s.value)).pk @@ -798,9 +936,14 @@ def _walk_rules(rules): self.settings.flush() event_copy_data.send( - sender=self, other=other, - tax_map=tax_map, category_map=category_map, item_map=item_map, variation_map=variation_map, - question_map=question_map, checkin_list_map=checkin_list_map + sender=self, + other=other, + tax_map=tax_map, + category_map=category_map, + item_map=item_map, + variation_map=variation_map, + question_map=question_map, + checkin_list_map=checkin_list_map, ) def get_payment_providers(self, cached=False) -> dict: @@ -809,7 +952,7 @@ def get_payment_providers(self, cached=False) -> dict: """ from ..signals import register_payment_providers - if not cached or not hasattr(self, '_cached_payment_providers'): + if not cached or not hasattr(self, "_cached_payment_providers"): responses = register_payment_providers.send(self) providers = {} for receiver, response in responses: @@ -819,18 +962,19 @@ def get_payment_providers(self, cached=False) -> dict: pp = p(self) providers[pp.identifier] = pp - self._cached_payment_providers = OrderedDict(sorted( - providers.items(), key=lambda v: (-v[1].priority, str(v[1].verbose_name)) - )) + self._cached_payment_providers = OrderedDict( + sorted( + providers.items(), + key=lambda v: (-v[1].priority, str(v[1].verbose_name)), + ) + ) return self._cached_payment_providers def get_html_mail_renderer(self): """ Returns the currently selected HTML email renderer """ - return self.get_html_mail_renderers()[ - self.settings.mail_html_renderer - ] + return self.get_html_mail_renderers()[self.settings.mail_html_renderer] def get_html_mail_renderers(self) -> dict: """ @@ -888,7 +1032,7 @@ def ticket_secret_generator(self): Returns the currently configured ticket secret generator. """ tsgs = self.ticket_secret_generators - return tsgs.get(self.settings.ticket_secret_generator, tsgs.get('random')) + return tsgs.get(self.settings.ticket_secret_generator, tsgs.get("random")) def get_data_shredders(self) -> dict: """ @@ -918,26 +1062,37 @@ def subevents_annotated(self, channel): return SubEvent.annotated(self.subevents, channel) def subevents_sorted(self, queryset): - ordering = self.settings.get('frontpage_subevent_ordering', default='date_ascending', as_type=str) + ordering = self.settings.get( + "frontpage_subevent_ordering", default="date_ascending", as_type=str + ) orderfields = { - 'date_ascending': ('date_from', 'name'), - 'date_descending': ('-date_from', 'name'), - 'name_ascending': ('name', 'date_from'), - 'name_descending': ('-name', 'date_from'), + "date_ascending": ("date_from", "name"), + "date_descending": ("-date_from", "name"), + "name_ascending": ("name", "date_from"), + "name_descending": ("-name", "date_from"), }[ordering] subevs = queryset.annotate( has_paid_item=Value( - self.cache.get_or_set('has_paid_item', lambda: self.items.filter(default_price__gt=0).exists(), 3600), - output_field=models.BooleanField() + self.cache.get_or_set( + "has_paid_item", + lambda: self.items.filter(default_price__gt=0).exists(), + 3600, + ), + output_field=models.BooleanField(), ) ).filter( - Q(active=True) & Q(is_public=True) & ( - Q(Q(date_to__isnull=True) & Q(date_from__gte=now() - timedelta(hours=24))) + Q(active=True) + & Q(is_public=True) + & ( + Q( + Q(date_to__isnull=True) + & Q(date_from__gte=now() - timedelta(hours=24)) + ) | Q(date_to__gte=now() - timedelta(hours=24)) ) ) # order_by doesn't make sense with I18nField for f in reversed(orderfields): - if f.startswith('-'): + if f.startswith("-"): subevs = sorted(subevs, key=attrgetter(f[1:]), reverse=True) else: subevs = sorted(subevs, key=attrgetter(f)) @@ -946,10 +1101,15 @@ def subevents_sorted(self, queryset): @property def meta_data(self): data = {p.name: p.default for p in self.organizer.meta_properties.all()} - if hasattr(self, 'meta_values_cached'): + if hasattr(self, "meta_values_cached"): data.update({v.property.name: v.value for v in self.meta_values_cached}) else: - data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()}) + data.update( + { + v.property.name: v.value + for v in self.meta_values.select_related("property").all() + } + ) return OrderedDict((k, v) for k, v in sorted(data.items(), key=lambda k: k[0])) @@ -957,7 +1117,12 @@ def meta_data(self): def has_payment_provider(self): result = False for provider in self.get_payment_providers().values(): - if provider.is_enabled and provider.identifier not in ('free', 'boxoffice', 'offsetting', 'giftcard'): + if provider.is_enabled and provider.identifier not in ( + "free", + "boxoffice", + "offsetting", + "giftcard", + ): result = True break return result @@ -966,29 +1131,51 @@ def has_payment_provider(self): def has_paid_things(self): from .items import Item, ItemVariation - return Item.objects.filter(event=self, default_price__gt=0).exists()\ - or ItemVariation.objects.filter(item__event=self, default_price__gt=0).exists() + return ( + Item.objects.filter(event=self, default_price__gt=0).exists() + or ItemVariation.objects.filter( + item__event=self, default_price__gt=0 + ).exists() + ) @cached_property def live_issues(self): from pretix.base.signals import event_live_issues + issues = [] if self.has_paid_things and not self.has_payment_provider: - issues.append(_('You have configured at least one paid product but have not enabled any payment methods.')) + issues.append( + _( + "You have configured at least one paid product but have not enabled any payment methods." + ) + ) if not self.quotas.exists(): - issues.append(_('You need to configure at least one quota to sell anything.')) + issues.append( + _("You need to configure at least one quota to sell anything.") + ) for mp in self.organizer.meta_properties.all(): if mp.required and not self.meta_data.get(mp.name): issues.append( - ('' + gettext('You need to fill the meta parameter "{property}".') + '').format( + ( + "" + + gettext('You need to fill the meta parameter "{property}".') + + "" + ).format( property=mp.name, - a_attr='href="%s#id_prop-%d-value"' % ( - reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}), - mp.pk - ) + a_attr='href="%s#id_prop-%d-value"' + % ( + reverse( + "control:event.settings", + kwargs={ + "organizer": self.organizer.slug, + "event": self.slug, + }, + ), + mp.pk, + ), ) ) @@ -1021,12 +1208,8 @@ def get_users_with_permission(self, permission): kwargs = {} team_with_perm = Team.objects.filter( - members__pk=OuterRef('pk'), - organizer=self.organizer, - **kwargs - ).filter( - Q(all_events=True) | Q(limit_events__pk=self.pk) - ) + members__pk=OuterRef("pk"), organizer=self.organizer, **kwargs + ).filter(Q(all_events=True) | Q(limit_events__pk=self.pk)) return User.objects.annotate(twp=Exists(team_with_perm)).filter(twp=True) @@ -1050,17 +1233,23 @@ def set_active_plugins(self, modules, allow_restricted=False): plugins_active = self.get_plugins() plugins_available = { - p.module: p for p in get_all_plugins(self) - if not p.name.startswith('.') and getattr(p, 'visible', True) + p.module: p + for p in get_all_plugins(self) + if not p.name.startswith(".") and getattr(p, "visible", True) } - enable = [m for m in modules if m not in plugins_active and m in plugins_available] + enable = [ + m for m in modules if m not in plugins_active and m in plugins_available + ] for module in enable: - if getattr(plugins_available[module].app, 'restricted', False) and not allow_restricted: + if ( + getattr(plugins_available[module].app, "restricted", False) + and not allow_restricted + ): modules.remove(module) - elif hasattr(plugins_available[module].app, 'installed'): - getattr(plugins_available[module].app, 'installed')(self) + elif hasattr(plugins_available[module].app, "installed"): + getattr(plugins_available[module].app, "installed")(self) self.plugins = ",".join(modules) @@ -1088,28 +1277,36 @@ def disable_plugin(self, module): def clean_has_subevents(event, has_subevents): if event is not None and event.has_subevents is not None: if event.has_subevents != has_subevents: - raise ValidationError(_('Once created an event cannot change between an series and a single event.')) + raise ValidationError( + _( + "Once created an event cannot change between an series and a single event." + ) + ) @staticmethod def clean_slug(organizer, event, slug): if event is not None and event.slug is not None: if event.slug != slug: - raise ValidationError(_('The event slug cannot be changed.')) + raise ValidationError(_("The event slug cannot be changed.")) else: if Event.objects.filter(slug=slug, organizer=organizer).exists(): - raise ValidationError(_('This slug has already been used for a different event.')) + raise ValidationError( + _("This slug has already been used for a different event.") + ) @staticmethod def clean_dates(date_from, date_to): if date_from is not None and date_to is not None: if date_from > date_to: - raise ValidationError(_('The event cannot end before it starts.')) + raise ValidationError(_("The event cannot end before it starts.")) @staticmethod def clean_presale(presale_start, presale_end): if presale_start is not None and presale_end is not None: if presale_start > presale_end: - raise ValidationError(_('The event\'s presale cannot end before it starts.')) + raise ValidationError( + _("The event's presale cannot end before it starts.") + ) class SubEvent(EventMixin, LoggedModel): @@ -1136,70 +1333,92 @@ class SubEvent(EventMixin, LoggedModel): :type location: str """ - _event_id = 'event_id' + _event_id = "event_id" event = models.ForeignKey(Event, related_name="subevents", on_delete=models.PROTECT) - active = models.BooleanField(default=False, verbose_name=_("Active"), - help_text=_("Only with this checkbox enabled, this date is visible in the " - "frontend to users.")) - is_public = models.BooleanField(default=True, - verbose_name=_("Show in lists"), - help_text=_("If selected, this event will show up publicly on the list of dates " - "for your event.")) + active = models.BooleanField( + default=False, + verbose_name=_("Active"), + help_text=_( + "Only with this checkbox enabled, this date is visible in the " + "frontend to users." + ), + ) + is_public = models.BooleanField( + default=True, + verbose_name=_("Show in lists"), + help_text=_( + "If selected, this event will show up publicly on the list of dates " + "for your event." + ), + ) name = I18nCharField( max_length=200, verbose_name=_("Name"), ) date_from = models.DateTimeField(verbose_name=_("Event start time")) - date_to = models.DateTimeField(null=True, blank=True, - verbose_name=_("Event end time")) - date_admission = models.DateTimeField(null=True, blank=True, - verbose_name=_("Admission time")) + date_to = models.DateTimeField( + null=True, blank=True, verbose_name=_("Event end time") + ) + date_admission = models.DateTimeField( + null=True, blank=True, verbose_name=_("Admission time") + ) presale_end = models.DateTimeField( - null=True, blank=True, + null=True, + blank=True, verbose_name=_("End of presale"), - help_text=_("Optional. No products will be sold after this date. If you do not set this value, the presale " - "will end after the end date of your event."), + help_text=_( + "Optional. No products will be sold after this date. If you do not set this value, the presale " + "will end after the end date of your event." + ), ) presale_start = models.DateTimeField( - null=True, blank=True, + null=True, + blank=True, verbose_name=_("Start of presale"), help_text=_("Optional. No products will be sold before this date."), ) location = I18nTextField( - null=True, blank=True, + null=True, + blank=True, max_length=200, verbose_name=_("Location"), ) geo_lat = models.FloatField( verbose_name=_("Latitude"), - null=True, blank=True, + null=True, + blank=True, validators=[ MinValueValidator(-90), MaxValueValidator(90), - ] + ], ) geo_lon = models.FloatField( verbose_name=_("Longitude"), - null=True, blank=True, + null=True, + blank=True, validators=[ MinValueValidator(-180), MaxValueValidator(180), - ] + ], ) frontpage_text = I18nTextField( - null=True, blank=True, - verbose_name=_("Frontpage text") + null=True, blank=True, verbose_name=_("Frontpage text") ) - seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True, - related_name='subevents') - last_modified = models.DateTimeField( - auto_now=True, db_index=True + seating_plan = models.ForeignKey( + "SeatingPlan", + on_delete=models.PROTECT, + null=True, + blank=True, + related_name="subevents", ) + last_modified = models.DateTimeField(auto_now=True, db_index=True) - items = models.ManyToManyField('Item', through='SubEventItem') - variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation') + items = models.ManyToManyField("Item", through="SubEventItem") + variations = models.ManyToManyField( + "ItemVariation", through="SubEventItemVariation" + ) - objects = ScopedManager(organizer='event__organizer') + objects = ScopedManager(organizer="event__organizer") class Meta: verbose_name = _("Date in event series") @@ -1207,18 +1426,27 @@ class Meta: ordering = ("date_from", "name") def __str__(self): - return '{} - {} {}'.format( + return "{} - {} {}".format( self.name, self.get_date_range_display(), - date_format(self.date_from.astimezone(self.timezone), "TIME_FORMAT") if self.settings.show_times else "" + ( + date_format(self.date_from.astimezone(self.timezone), "TIME_FORMAT") + if self.settings.show_times + else "" + ), ).strip() def _seats(self, ignore_voucher=None): from .seating import Seat - qs_annotated = Seat.annotated(self.seats, self.event_id, self, - ignore_voucher_id=ignore_voucher.pk if ignore_voucher else None, - minimal_distance=self.settings.seating_minimal_distance, - distance_only_within_row=self.settings.seating_distance_within_row) + + qs_annotated = Seat.annotated( + self.seats, + self.event_id, + self, + ignore_voucher_id=ignore_voucher.pk if ignore_voucher else None, + minimal_distance=self.settings.seating_minimal_distance, + distance_only_within_row=self.settings.seating_distance_within_row, + ) return qs_annotated @cached_property @@ -1229,10 +1457,7 @@ def settings(self): def item_overrides(self): from .items import SubEventItem - return { - si.item_id: si - for si in SubEventItem.objects.filter(subevent=self) - } + return {si.item_id: si for si in SubEventItem.objects.filter(subevent=self)} @cached_property def var_overrides(self): @@ -1247,20 +1472,27 @@ def var_overrides(self): def item_price_overrides(self): return { si.item_id: si.price - for si in self.item_overrides.values() if si.price is not None + for si in self.item_overrides.values() + if si.price is not None } @property def var_price_overrides(self): return { si.variation_id: si.price - for si in self.var_overrides.values() if si.price is not None + for si in self.var_overrides.values() + if si.price is not None } @property def meta_data(self): data = self.event.meta_data - data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()}) + data.update( + { + v.property.name: v.value + for v in self.meta_values.select_related("property").all() + } + ) return data @property @@ -1271,7 +1503,7 @@ def allow_delete(self): return not self.orderposition_set.exists() def delete(self, *args, **kwargs): - clear_cache = kwargs.pop('clear_cache', False) + clear_cache = kwargs.pop("clear_cache", False) super().delete(*args, **kwargs) if self.event and clear_cache: self.event.cache.clear() @@ -1283,7 +1515,7 @@ def __init__(self, *args, **kwargs): def save(self, *args, **kwargs): from .orders import Order - clear_cache = kwargs.pop('clear_cache', False) + clear_cache = kwargs.pop("clear_cache", False) super().save(*args, **kwargs) if self.event and clear_cache: self.event.cache.clear() @@ -1296,24 +1528,32 @@ def save(self, *args, **kwargs): the app needs to know when a subevent is moved to a date in the future, since that might require it to re-download and re-store the orders. """ - Order.objects.filter(all_positions__subevent=self).update(last_modified=now()) + Order.objects.filter(all_positions__subevent=self).update( + last_modified=now() + ) @staticmethod def clean_items(event, items): for item in items: if event != item.event: - raise ValidationError(_('One or more items do not belong to this event.')) + raise ValidationError( + _("One or more items do not belong to this event.") + ) @staticmethod def clean_variations(event, variations): for variation in variations: if event != variation.item.event: - raise ValidationError(_('One or more variations do not belong to this event.')) + raise ValidationError( + _("One or more variations do not belong to this event.") + ) @scopes_disabled() def generate_invite_token(): - return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits) + return get_random_string( + length=32, allowed_chars=string.ascii_lowercase + string.digits + ) class EventLock(models.Model): @@ -1342,20 +1582,23 @@ class RequiredAction(models.Model): :param data: Arbitrary data that can be used by the log action renderer :type data: str """ + datetime = models.DateTimeField(auto_now_add=True, db_index=True) done = models.BooleanField(default=False) - user = models.ForeignKey('User', null=True, blank=True, on_delete=models.PROTECT) - event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.CASCADE) + user = models.ForeignKey("User", null=True, blank=True, on_delete=models.PROTECT) + event = models.ForeignKey("Event", null=True, blank=True, on_delete=models.CASCADE) action_type = models.CharField(max_length=255) - data = models.TextField(default='{}') + data = models.TextField(default="{}") class Meta: - ordering = ('datetime',) + ordering = ("datetime",) def display(self, request): from ..signals import requiredaction_display - for receiver, response in requiredaction_display.send(self.event, action=self, request=request): + for receiver, response in requiredaction_display.send( + self.event, action=self, request=request + ): if response: return response return self.action_type @@ -1369,9 +1612,9 @@ def save(self, *args, **kwargs): logentry = LogEntry.objects.create( content_object=self, - action_type='pretix.event.action_required', + action_type="pretix.event.action_required", event=self.event, - visible=False + visible=False, ) notify.apply_async(args=(logentry.pk,)) @@ -1388,40 +1631,62 @@ class EventMetaProperty(LoggedModel): :param default: Default value :type default: str """ - organizer = models.ForeignKey(Organizer, related_name="meta_properties", on_delete=models.CASCADE) + + organizer = models.ForeignKey( + Organizer, related_name="meta_properties", on_delete=models.CASCADE + ) name = models.CharField( - max_length=50, db_index=True, - help_text=_( - "Can not contain spaces or special characters except underscores" - ), + max_length=50, + db_index=True, + help_text=_("Can not contain spaces or special characters except underscores"), validators=[ RegexValidator( regex="^[a-zA-Z0-9_]+$", - message=_("The property name may only contain letters, numbers and underscores."), + message=_( + "The property name may only contain letters, numbers and underscores." + ), ), ], verbose_name=_("Name"), ) default = models.TextField(blank=True, verbose_name=_("Default value")) - protected = models.BooleanField(default=False, - verbose_name=_("Can only be changed by organizer-level administrators")) + protected = models.BooleanField( + default=False, + verbose_name=_("Can only be changed by organizer-level administrators"), + ) required = models.BooleanField( - default=False, verbose_name=_("Required for events"), - help_text=_("If checked, an event can only be taken live if the property is set. In event series, its always " - "optional to set a value for individual dates") + default=False, + verbose_name=_("Required for events"), + help_text=_( + "If checked, an event can only be taken live if the property is set. In event series, its always " + "optional to set a value for individual dates" + ), ) allowed_values = models.TextField( - null=True, blank=True, + null=True, + blank=True, verbose_name=_("Valid values"), - help_text=_("If you keep this empty, any value is allowed. Otherwise, enter one possible value per line.") + help_text=_( + "If you keep this empty, any value is allowed. Otherwise, enter one possible value per line." + ), ) def full_clean(self, exclude=None, validate_unique=True): super().full_clean(exclude, validate_unique) if self.default and self.required: - raise ValidationError(_("A property can either be required or have a default value, not both.")) - if self.default and self.allowed_values and self.default not in self.allowed_values.splitlines(): - raise ValidationError(_("You cannot set a default value that is not a valid value.")) + raise ValidationError( + _( + "A property can either be required or have a default value, not both." + ) + ) + if ( + self.default + and self.allowed_values + and self.default not in self.allowed_values.splitlines() + ): + raise ValidationError( + _("You cannot set a default value that is not a valid value.") + ) class EventMetaValue(LoggedModel): @@ -1435,14 +1700,17 @@ class EventMetaValue(LoggedModel): :param value: The actual value :type value: str """ - event = models.ForeignKey('Event', on_delete=models.CASCADE, - related_name='meta_values') - property = models.ForeignKey('EventMetaProperty', on_delete=models.CASCADE, - related_name='event_values') + + event = models.ForeignKey( + "Event", on_delete=models.CASCADE, related_name="meta_values" + ) + property = models.ForeignKey( + "EventMetaProperty", on_delete=models.CASCADE, related_name="event_values" + ) value = models.TextField() class Meta: - unique_together = ('event', 'property') + unique_together = ("event", "property") def delete(self, *args, **kwargs): super().delete(*args, **kwargs) @@ -1466,14 +1734,17 @@ class SubEventMetaValue(LoggedModel): :param value: The actual value :type value: str """ - subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE, - related_name='meta_values') - property = models.ForeignKey('EventMetaProperty', on_delete=models.CASCADE, - related_name='subevent_values') + + subevent = models.ForeignKey( + "SubEvent", on_delete=models.CASCADE, related_name="meta_values" + ) + property = models.ForeignKey( + "EventMetaProperty", on_delete=models.CASCADE, related_name="subevent_values" + ) value = models.TextField() class Meta: - unique_together = ('subevent', 'property') + unique_together = ("subevent", "property") def delete(self, *args, **kwargs): super().delete(*args, **kwargs) @@ -1490,9 +1761,10 @@ class EventFooterLinkModel(models.Model): """ FooterLink model - support show link for event's footer """ - event = models.ForeignKey('Event', - on_delete=models.CASCADE, - related_name='footer_links') + + event = models.ForeignKey( + "Event", on_delete=models.CASCADE, related_name="footer_links" + ) label = I18nCharField( max_length=255, verbose_name=_("Link's text"), diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index dd13e527c..42178545f 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -5,7 +5,9 @@ from django.core.exceptions import ValidationError from django.core.validators import validate_email from django.db.models import Q -from django.forms import CheckboxSelectMultiple, formset_factory, inlineformset_factory +from django.forms import ( + CheckboxSelectMultiple, formset_factory, inlineformset_factory, +) from django.urls import reverse from django.utils.html import escape from django.utils.safestring import mark_safe @@ -21,14 +23,16 @@ from pretix.base.email import get_available_placeholders from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm from pretix.base.models import Event, Organizer, TaxRule, Team -from pretix.base.models.event import EventMetaValue, SubEvent, EventFooterLinkModel +from pretix.base.models.event import ( + EventFooterLinkModel, EventMetaValue, SubEvent, +) from pretix.base.reldate import RelativeDateField, RelativeDateTimeField from pretix.base.settings import ( PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_event_settings, ) from pretix.control.forms import ( - MultipleLanguagesWidget, SlugWidget, SplitDateTimeField, - SplitDateTimePickerWidget, SMTPSettingsMixin, + MultipleLanguagesWidget, SlugWidget, SMTPSettingsMixin, SplitDateTimeField, + SplitDateTimePickerWidget, ) from pretix.control.forms.widgets import Select2 from pretix.helpers.countries import CachedCountries @@ -42,50 +46,55 @@ class EventWizardFoundationForm(forms.Form): choices=settings.LANGUAGES, label=_("Use languages"), widget=MultipleLanguagesWidget, - help_text=_('Choose all languages that your event should be available in.') + help_text=_("Choose all languages that your event should be available in."), ) has_subevents = forms.BooleanField( label=_("This is an event series"), required=False, ) - is_video_create = forms.BooleanField( + is_video_creation = forms.BooleanField( label=_("Create Video platform for this Event."), help_text=_("This will create a new Video platform for this event."), required=False, - widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}) + widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), ) def __init__(self, *args, **kwargs): - self.user = kwargs.pop('user') - self.session = kwargs.pop('session') + self.user = kwargs.pop("user") + self.session = kwargs.pop("session") super().__init__(*args, **kwargs) qs = Organizer.objects.all() if not self.user.has_active_staff_session(self.session.session_key): qs = qs.filter( - id__in=self.user.teams.filter(can_create_events=True).values_list('organizer', flat=True) + id__in=self.user.teams.filter(can_create_events=True).values_list( + "organizer", flat=True + ) ) - self.fields['organizer'] = forms.ModelChoiceField( + self.fields["organizer"] = forms.ModelChoiceField( label=_("Organizer"), queryset=qs, widget=Select2( attrs={ - 'data-model-select2': 'generic', - 'data-select2-url': reverse('control:organizers.select2') + '?can_create=1', - 'data-placeholder': _('Organizer') + "data-model-select2": "generic", + "data-select2-url": reverse("control:organizers.select2") + + "?can_create=1", + "data-placeholder": _("Organizer"), } ), empty_label=None, - required=True + required=True, ) - self.fields['organizer'].widget.choices = self.fields['organizer'].choices + self.fields["organizer"].widget.choices = self.fields["organizer"].choices - if len(self.fields['organizer'].choices) == 1: - self.fields['organizer'].initial = self.fields['organizer'].queryset.first() + if len(self.fields["organizer"].choices) == 1: + self.fields["organizer"].initial = self.fields["organizer"].queryset.first() class EventWizardBasicsForm(I18nModelForm): error_messages = { - 'duplicate_slug': _("You already used this slug for a different event. Please choose a new one."), + "duplicate_slug": _( + "You already used this slug for a different event. Please choose a new one." + ), } timezone = forms.ChoiceField( choices=((a, a) for a in common_timezones), @@ -97,97 +106,117 @@ class EventWizardBasicsForm(I18nModelForm): ) tax_rate = forms.DecimalField( label=_("Sales tax rate"), - help_text=_("Do you need to pay sales tax on your tickets? In this case, please enter the applicable tax rate " - "here in percent. If you have a more complicated tax situation, you can add more tax rates and " - "detailed configuration later."), - required=False + help_text=_( + "Do you need to pay sales tax on your tickets? In this case, please enter the applicable tax rate " + "here in percent. If you have a more complicated tax situation, you can add more tax rates and " + "detailed configuration later." + ), + required=False, ) team = forms.ModelChoiceField( label=_("Grant access to team"), - help_text=_("You are allowed to create events under this organizer, however you do not have permission " - "to edit all events under this organizer. Please select one of your existing teams that will" - " be granted access to this event."), + help_text=_( + "You are allowed to create events under this organizer, however you do not have permission " + "to edit all events under this organizer. Please select one of your existing teams that will" + " be granted access to this event." + ), queryset=Team.objects.none(), required=False, - empty_label=_('Create a new team for this event with me as the only member') + empty_label=_("Create a new team for this event with me as the only member"), ) class Meta: model = Event fields = [ - 'name', - 'slug', - 'currency', - 'date_from', - 'date_to', - 'presale_start', - 'presale_end', - 'location', - 'geo_lat', - 'geo_lon', + "name", + "slug", + "currency", + "date_from", + "date_to", + "presale_start", + "presale_end", + "location", + "geo_lat", + "geo_lon", ] field_classes = { - 'date_from': SplitDateTimeField, - 'date_to': SplitDateTimeField, - 'presale_start': SplitDateTimeField, - 'presale_end': SplitDateTimeField, + "date_from": SplitDateTimeField, + "date_to": SplitDateTimeField, + "presale_start": SplitDateTimeField, + "presale_end": SplitDateTimeField, } widgets = { - 'date_from': SplitDateTimePickerWidget(), - 'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-date_from_0'}), - 'presale_start': SplitDateTimePickerWidget(), - 'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-presale_start_0'}), - 'slug': SlugWidget, + "date_from": SplitDateTimePickerWidget(), + "date_to": SplitDateTimePickerWidget( + attrs={"data-date-after": "#id_basics-date_from_0"} + ), + "presale_start": SplitDateTimePickerWidget(), + "presale_end": SplitDateTimePickerWidget( + attrs={"data-date-after": "#id_basics-presale_start_0"} + ), + "slug": SlugWidget, } def __init__(self, *args, **kwargs): - self.organizer = kwargs.pop('organizer') - self.locales = kwargs.get('locales') - self.has_subevents = kwargs.pop('has_subevents') - self.is_video_create = kwargs.pop('is_video_create') - self.user = kwargs.pop('user') - kwargs.pop('session') + self.organizer = kwargs.pop("organizer") + self.locales = kwargs.get("locales") + self.has_subevents = kwargs.pop("has_subevents") + self.is_video_creation = kwargs.pop("is_video_creation") + self.user = kwargs.pop("user") + kwargs.pop("session") super().__init__(*args, **kwargs) - if 'timezone' not in self.initial: - self.initial['timezone'] = get_current_timezone_name() - self.fields['locale'].choices = [(a, b) for a, b in settings.LANGUAGES if a in self.locales] - self.fields['location'].widget.attrs['rows'] = '3' - self.fields['location'].widget.attrs['placeholder'] = _( - 'Sample Conference Center\nHeidelberg, Germany' + if "timezone" not in self.initial: + self.initial["timezone"] = get_current_timezone_name() + self.fields["locale"].choices = [ + (a, b) for a, b in settings.LANGUAGES if a in self.locales + ] + self.fields["location"].widget.attrs["rows"] = "3" + self.fields["location"].widget.attrs["placeholder"] = _( + "Sample Conference Center\nHeidelberg, Germany" + ) + self.fields["slug"].widget.prefix = build_absolute_uri( + self.organizer, "presale:organizer.index" ) - self.fields['slug'].widget.prefix = build_absolute_uri(self.organizer, 'presale:organizer.index') if self.has_subevents: - del self.fields['presale_start'] - del self.fields['presale_end'] - del self.fields['date_to'] + del self.fields["presale_start"] + del self.fields["presale_end"] + del self.fields["date_to"] if self.has_control_rights(self.user, self.organizer): - del self.fields['team'] + del self.fields["team"] else: - self.fields['team'].queryset = self.user.teams.filter(organizer=self.organizer) - if not self.organizer.settings.get("event_team_provisioning", True, as_type=bool): - self.fields['team'].required = True - self.fields['team'].empty_label = None - self.fields['team'].initial = 0 + self.fields["team"].queryset = self.user.teams.filter( + organizer=self.organizer + ) + if not self.organizer.settings.get( + "event_team_provisioning", True, as_type=bool + ): + self.fields["team"].required = True + self.fields["team"].empty_label = None + self.fields["team"].initial = 0 def clean(self): data = super().clean() - if data.get('locale') not in self.locales: - raise ValidationError({ - 'locale': _('Your default locale must also be enabled for your event (see box above).') - }) - if data.get('timezone') not in common_timezones: - raise ValidationError({ - 'timezone': _('Your default locale must be specified.') - }) + if data.get("locale") not in self.locales: + raise ValidationError( + { + "locale": _( + "Your default locale must also be enabled for your event (see box above)." + ) + } + ) + if data.get("timezone") not in common_timezones: + raise ValidationError( + {"timezone": _("Your default locale must be specified.")} + ) # change timezone - zone = timezone(data.get('timezone')) - data['date_from'] = self.reset_timezone(zone, data.get('date_from')) - data['date_to'] = self.reset_timezone(zone, data.get('date_to')) - data['presale_start'] = self.reset_timezone(zone, data.get('presale_start')) - data['presale_end'] = self.reset_timezone(zone, data.get('presale_end')) + zone = timezone(data.get("timezone")) + data["date_from"] = self.reset_timezone(zone, data.get("date_from")) + data["date_to"] = self.reset_timezone(zone, data.get("date_to")) + data["presale_start"] = self.reset_timezone(zone, data.get("presale_start")) + data["presale_end"] = self.reset_timezone(zone, data.get("presale_end")) return data @staticmethod @@ -195,29 +224,41 @@ def reset_timezone(tz, dt): return tz.localize(dt.replace(tzinfo=None)) if dt is not None else None def clean_slug(self): - slug = self.cleaned_data['slug'] + slug = self.cleaned_data["slug"] if Event.objects.filter(slug__iexact=slug, organizer=self.organizer).exists(): raise forms.ValidationError( - self.error_messages['duplicate_slug'], - code='duplicate_slug' + self.error_messages["duplicate_slug"], code="duplicate_slug" ) return slug @staticmethod def has_control_rights(user, organizer): - return user.teams.filter( - organizer=organizer, all_events=True, can_change_event_settings=True, can_change_items=True, - can_change_orders=True, can_change_vouchers=True - ).exists() or user.is_staff + return ( + user.teams.filter( + organizer=organizer, + all_events=True, + can_change_event_settings=True, + can_change_items=True, + can_change_orders=True, + can_change_vouchers=True, + ).exists() + or user.is_staff + ) class EventChoiceMixin: def label_from_instance(self, obj): - return mark_safe('{}
{} · {}'.format( - escape(str(obj)), - obj.get_date_range_display() if not obj.has_subevents else _("Event series"), - obj.slug - )) + return mark_safe( + '{}
{} · {}'.format( + escape(str(obj)), + ( + obj.get_date_range_display() + if not obj.has_subevents + else _("Event series") + ), + obj.slug, + ) + ) class EventChoiceField(forms.ModelChoiceField): @@ -237,130 +278,163 @@ def copy_from_queryset(user, session): if user.has_active_staff_session(session.session_key): return Event.objects.all() return Event.objects.filter( - Q(organizer_id__in=user.teams.filter( - all_events=True, can_change_event_settings=True, can_change_items=True - ).values_list('organizer', flat=True)) | Q(id__in=user.teams.filter( - can_change_event_settings=True, can_change_items=True - ).values_list('limit_events__id', flat=True)) + Q( + organizer_id__in=user.teams.filter( + all_events=True, + can_change_event_settings=True, + can_change_items=True, + ).values_list("organizer", flat=True) + ) + | Q( + id__in=user.teams.filter( + can_change_event_settings=True, can_change_items=True + ).values_list("limit_events__id", flat=True) + ) ) def __init__(self, *args, **kwargs): - kwargs.pop('organizer') - kwargs.pop('locales') - self.session = kwargs.pop('session') - kwargs.pop('has_subevents') - self.user = kwargs.pop('user') + kwargs.pop("organizer") + kwargs.pop("locales") + self.session = kwargs.pop("session") + kwargs.pop("has_subevents") + self.user = kwargs.pop("user") super().__init__(*args, **kwargs) - self.fields['copy_from_event'] = EventChoiceField( + self.fields["copy_from_event"] = EventChoiceField( label=_("Copy configuration from"), queryset=EventWizardCopyForm.copy_from_queryset(self.user, self.session), widget=Select2( attrs={ - 'data-model-select2': 'event', - 'data-select2-url': reverse('control:events.typeahead') + '?can_copy=1', - 'data-placeholder': _('Do not copy') + "data-model-select2": "event", + "data-select2-url": reverse("control:events.typeahead") + + "?can_copy=1", + "data-placeholder": _("Do not copy"), } ), - empty_label=_('Do not copy'), - required=False + empty_label=_("Do not copy"), + required=False, ) - self.fields['copy_from_event'].widget.choices = self.fields['copy_from_event'].choices + self.fields["copy_from_event"].widget.choices = self.fields[ + "copy_from_event" + ].choices class EventMetaValueForm(forms.ModelForm): def __init__(self, *args, **kwargs): - self.property = kwargs.pop('property') - self.disabled = kwargs.pop('disabled') + self.property = kwargs.pop("property") + self.disabled = kwargs.pop("disabled") super().__init__(*args, **kwargs) if self.property.allowed_values: - self.fields['value'] = forms.ChoiceField( + self.fields["value"] = forms.ChoiceField( label=self.property.name, choices=[ - ('', _('Default ({value})').format(value=self.property.default) if self.property.default else ''), - ] + [(a.strip(), a.strip()) for a in self.property.allowed_values.splitlines()], + ( + "", + ( + _("Default ({value})").format(value=self.property.default) + if self.property.default + else "" + ), + ), + ] + + [ + (a.strip(), a.strip()) + for a in self.property.allowed_values.splitlines() + ], ) else: - self.fields['value'].label = self.property.name - self.fields['value'].widget.attrs['placeholder'] = self.property.default - self.fields['value'].widget.attrs['data-typeahead-url'] = ( - reverse('control:events.meta.typeahead') + '?' + urlencode({ - 'property': self.property.name, - 'organizer': self.property.organizer.slug, - }) + self.fields["value"].label = self.property.name + self.fields["value"].widget.attrs["placeholder"] = self.property.default + self.fields["value"].widget.attrs["data-typeahead-url"] = ( + reverse("control:events.meta.typeahead") + + "?" + + urlencode( + { + "property": self.property.name, + "organizer": self.property.organizer.slug, + } + ) ) - self.fields['value'].required = False + self.fields["value"].required = False if self.disabled: - self.fields['value'].widget.attrs['readonly'] = 'readonly' + self.fields["value"].widget.attrs["readonly"] = "readonly" def clean_slug(self): if self.disabled: return self.instance.value if self.instance else None - return self.cleaned_data['slug'] + return self.cleaned_data["slug"] class Meta: model = EventMetaValue - fields = ['value'] - widgets = { - 'value': forms.TextInput() - } + fields = ["value"] + widgets = {"value": forms.TextInput()} class EventUpdateForm(I18nModelForm): - is_video_create = forms.BooleanField( + is_video_creation = forms.BooleanField( required=False, ) + def __init__(self, *args, **kwargs): - self.change_slug = kwargs.pop('change_slug', False) - self.domain = kwargs.pop('domain', False) + self.change_slug = kwargs.pop("change_slug", False) + self.domain = kwargs.pop("domain", False) - kwargs.setdefault('initial', {}) - self.instance = kwargs['instance'] + 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) + kwargs["initial"].setdefault("domain", initial_domain.domainname) super().__init__(*args, **kwargs) if not self.change_slug: - self.fields['slug'].widget.attrs['readonly'] = 'readonly' - self.fields['location'].widget.attrs['rows'] = '3' - self.fields['location'].widget.attrs['placeholder'] = _( - 'Sample Conference Center\nHeidelberg, Germany' + self.fields["slug"].widget.attrs["readonly"] = "readonly" + self.fields["location"].widget.attrs["rows"] = "3" + self.fields["location"].widget.attrs["placeholder"] = _( + "Sample Conference Center\nHeidelberg, Germany" ) if self.domain: - self.fields['domain'] = forms.CharField( + self.fields["domain"] = forms.CharField( max_length=255, - label=_('Custom domain'), + label=_("Custom domain"), required=False, - help_text=_('You need to configure the custom domain in the webserver beforehand.') + help_text=_( + "You need to configure the custom domain in the webserver beforehand." + ), ) - self.fields['sales_channels'] = forms.MultipleChoiceField( - label=self.fields['sales_channels'].label, - help_text=self.fields['sales_channels'].help_text, - required=self.fields['sales_channels'].required, - initial=self.fields['sales_channels'].initial, + self.fields["sales_channels"] = forms.MultipleChoiceField( + label=self.fields["sales_channels"].label, + help_text=self.fields["sales_channels"].help_text, + required=self.fields["sales_channels"].required, + initial=self.fields["sales_channels"].initial, choices=( - (c.identifier, c.verbose_name) for c in get_all_sales_channels().values() + (c.identifier, c.verbose_name) + for c in get_all_sales_channels().values() ), - widget=forms.CheckboxSelectMultiple + widget=forms.CheckboxSelectMultiple, ) - self.is_video_create = self.initial.get('is_video_create') - if self.is_video_create: - self.fields['is_video_create'].disabled = True + self.is_video_creation = self.initial.get("is_video_creation") + if self.is_video_creation: + self.fields["is_video_creation"].disabled = True def clean_domain(self): - d = self.cleaned_data['domain'] + 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.') + _("You cannot choose the base domain of this installation.") ) - if KnownDomain.objects.filter(domainname=d).exclude(event=self.instance.pk).exists(): + 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.') + _( + "This domain is already in use for a different event or organizer." + ) ) return d @@ -369,15 +443,22 @@ def save(self, commit=True): if self.domain: current_domain = instance.domains.first() - if self.cleaned_data['domain']: - if current_domain and current_domain.domainname != self.cleaned_data['domain']: + 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'] + 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'] + organizer=instance.organizer, + event=instance, + domainname=self.cleaned_data["domain"], ) elif current_domain: current_domain.delete() @@ -387,42 +468,48 @@ def save(self, commit=True): def clean_slug(self): if self.change_slug: - return self.cleaned_data['slug'] + return self.cleaned_data["slug"] return self.instance.slug class Meta: model = Event - localized_fields = '__all__' + localized_fields = "__all__" fields = [ - 'name', - 'slug', - 'currency', - 'date_from', - 'date_to', - 'date_admission', - 'is_public', - 'presale_start', - 'presale_end', - 'location', - 'geo_lat', - 'geo_lon', - 'sales_channels', - 'is_video_create' + "name", + "slug", + "currency", + "date_from", + "date_to", + "date_admission", + "is_public", + "presale_start", + "presale_end", + "location", + "geo_lat", + "geo_lon", + "sales_channels", + "is_video_creation", ] field_classes = { - 'date_from': SplitDateTimeField, - 'date_to': SplitDateTimeField, - 'date_admission': SplitDateTimeField, - 'presale_start': SplitDateTimeField, - 'presale_end': SplitDateTimeField, + "date_from": SplitDateTimeField, + "date_to": SplitDateTimeField, + "date_admission": SplitDateTimeField, + "presale_start": SplitDateTimeField, + "presale_end": SplitDateTimeField, } widgets = { - 'date_from': SplitDateTimePickerWidget(), - 'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}), - 'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}), - 'presale_start': SplitDateTimePickerWidget(), - 'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}), - 'sales_channels': CheckboxSelectMultiple(), + "date_from": SplitDateTimePickerWidget(), + "date_to": SplitDateTimePickerWidget( + attrs={"data-date-after": "#id_date_from_0"} + ), + "date_admission": SplitDateTimePickerWidget( + attrs={"data-date-default": "#id_date_from_0"} + ), + "presale_start": SplitDateTimePickerWidget(), + "presale_end": SplitDateTimePickerWidget( + attrs={"data-date-after": "#id_presale_start_0"} + ), + "sales_channels": CheckboxSelectMultiple(), } @@ -433,85 +520,89 @@ class EventSettingsForm(SettingsForm): ) name_scheme = forms.ChoiceField( label=_("Name format"), - help_text=_("This defines how pretix will ask for human names. Changing this after you already received " - "orders might lead to unexpected behavior when sorting or changing names."), + help_text=_( + "This defines how pretix will ask for human names. Changing this after you already received " + "orders might lead to unexpected behavior when sorting or changing names." + ), required=True, ) name_scheme_titles = forms.ChoiceField( label=_("Allowed titles"), - help_text=_("If the naming scheme you defined above allows users to input a title, you can use this to " - "restrict the set of selectable titles."), + help_text=_( + "If the naming scheme you defined above allows users to input a title, you can use this to " + "restrict the set of selectable titles." + ), required=False, ) auto_fields = [ - 'imprint_url', - 'checkout_email_helptext', - 'presale_has_ended_text', - 'voucher_explanation_text', - 'checkout_success_text', - 'show_dates_on_frontpage', - 'show_date_to', - 'show_times', - 'show_items_outside_presale_period', - 'display_net_prices', - 'presale_start_show_date', - 'locales', - 'locale', - 'region', - 'show_quota_left', - 'waiting_list_enabled', - 'waiting_list_hours', - 'waiting_list_auto', - 'waiting_list_names_asked', - 'waiting_list_names_required', - 'waiting_list_phones_asked', - 'waiting_list_phones_required', - 'waiting_list_phones_explanation_text', - 'max_items_per_order', - 'reservation_time', - 'contact_mail', - 'show_variations_expanded', - 'hide_sold_out', - 'meta_noindex', - 'redirect_to_checkout_directly', - 'frontpage_subevent_ordering', - 'event_list_type', - 'event_list_available_only', - 'frontpage_text', - 'event_info_text', - 'attendee_names_asked', - 'attendee_names_required', - 'attendee_emails_asked', - 'attendee_emails_required', - 'attendee_company_asked', - 'attendee_company_required', - 'attendee_addresses_asked', - 'attendee_addresses_required', - 'attendee_data_explanation_text', - 'order_phone_asked', - 'order_phone_required', - 'checkout_phone_helptext', - 'banner_text', - 'banner_text_bottom', - 'order_email_asked_twice', - 'last_order_modification_date', - 'allow_modifications_after_checkin', - 'checkout_show_copy_answers_button', - 'primary_color', - 'theme_color_success', - 'theme_color_danger', - 'theme_color_background', - 'theme_round_borders', - 'hover_button_color', - 'primary_font', - 'logo_image', - 'logo_image_large', - 'logo_show_title', - 'og_image', - 'schedule_link', - 'session_link', - 'speaker_link', + "imprint_url", + "checkout_email_helptext", + "presale_has_ended_text", + "voucher_explanation_text", + "checkout_success_text", + "show_dates_on_frontpage", + "show_date_to", + "show_times", + "show_items_outside_presale_period", + "display_net_prices", + "presale_start_show_date", + "locales", + "locale", + "region", + "show_quota_left", + "waiting_list_enabled", + "waiting_list_hours", + "waiting_list_auto", + "waiting_list_names_asked", + "waiting_list_names_required", + "waiting_list_phones_asked", + "waiting_list_phones_required", + "waiting_list_phones_explanation_text", + "max_items_per_order", + "reservation_time", + "contact_mail", + "show_variations_expanded", + "hide_sold_out", + "meta_noindex", + "redirect_to_checkout_directly", + "frontpage_subevent_ordering", + "event_list_type", + "event_list_available_only", + "frontpage_text", + "event_info_text", + "attendee_names_asked", + "attendee_names_required", + "attendee_emails_asked", + "attendee_emails_required", + "attendee_company_asked", + "attendee_company_required", + "attendee_addresses_asked", + "attendee_addresses_required", + "attendee_data_explanation_text", + "order_phone_asked", + "order_phone_required", + "checkout_phone_helptext", + "banner_text", + "banner_text_bottom", + "order_email_asked_twice", + "last_order_modification_date", + "allow_modifications_after_checkin", + "checkout_show_copy_answers_button", + "primary_color", + "theme_color_success", + "theme_color_danger", + "theme_color_background", + "theme_round_borders", + "hover_button_color", + "primary_font", + "logo_image", + "logo_image_large", + "logo_show_title", + "og_image", + "schedule_link", + "session_link", + "speaker_link", ] def clean(self): @@ -524,19 +615,19 @@ def clean(self): for virtual_key in self.virtual_keys: if virtual_key not in data: continue - base_key = virtual_key.rsplit('_', 2)[0] - asked_key = base_key + '_asked' - required_key = base_key + '_required' + base_key = virtual_key.rsplit("_", 2)[0] + asked_key = base_key + "_asked" + required_key = base_key + "_required" - if data[virtual_key] == 'optional': + if data[virtual_key] == "optional": data[asked_key] = True data[required_key] = False - elif data[virtual_key] == 'required': + elif data[virtual_key] == "required": data[asked_key] = True data[required_key] = True # Explicitly check for 'do_not_ask'. # Do not overwrite as default-behaviour when no value for virtual field is transmitted! - elif data[virtual_key] == 'do_not_ask': + elif data[virtual_key] == "do_not_ask": data[asked_key] = False data[required_key] = False @@ -547,32 +638,32 @@ def clean(self): return data def __init__(self, *args, **kwargs): - self.event = kwargs['obj'] + self.event = kwargs["obj"] super().__init__(*args, **kwargs) - self.fields['name_scheme'].choices = ( - (k, _('Ask for {fields}, display like {example}').format( - fields=' + '.join(str(vv[1]) for vv in v['fields']), - example=v['concatenation'](v['sample']) - )) + self.fields["name_scheme"].choices = ( + ( + k, + _("Ask for {fields}, display like {example}").format( + fields=" + ".join(str(vv[1]) for vv in v["fields"]), + example=v["concatenation"](v["sample"]), + ), + ) for k, v in PERSON_NAME_SCHEMES.items() ) - self.fields['name_scheme_titles'].choices = [('', _('Free text input'))] + [ - (k, '{scheme}: {samples}'.format( - scheme=v[0], - samples=', '.join(v[1]) - )) + self.fields["name_scheme_titles"].choices = [("", _("Free text input"))] + [ + (k, "{scheme}: {samples}".format(scheme=v[0], samples=", ".join(v[1]))) for k, v in PERSON_NAME_TITLE_GROUPS.items() ] if not self.event.has_subevents: - del self.fields['frontpage_subevent_ordering'] - del self.fields['event_list_type'] - del self.fields['event_list_available_only'] + del self.fields["frontpage_subevent_ordering"] + del self.fields["event_list_type"] + del self.fields["event_list_available_only"] # create "virtual" fields for better UX when editing _asked and _required fields self.virtual_keys = [] - for asked_key in [key for key in self.fields.keys() if key.endswith('_asked')]: - required_key = asked_key.rsplit('_', 1)[0] + '_required' - virtual_key = asked_key + '_required' + for asked_key in [key for key in self.fields.keys() if key.endswith("_asked")]: + required_key = asked_key.rsplit("_", 1)[0] + "_required" + virtual_key = asked_key + "_required" if required_key not in self.fields or virtual_key in self.fields: # either no matching required key or # there already is a field with virtual_key defined manually, so do not overwrite @@ -587,77 +678,81 @@ def __init__(self, *args, **kwargs): widget=forms.RadioSelect, choices=[ # default key needs a value other than '' because with '' it would also overwrite even if combi-field is not transmitted - ('do_not_ask', _('Do not ask')), - ('optional', _('Ask, but do not require input')), - ('required', _('Ask and require input')) - ] + ("do_not_ask", _("Do not ask")), + ("optional", _("Ask, but do not require input")), + ("required", _("Ask and require input")), + ], ) self.virtual_keys.append(virtual_key) if self.initial[required_key]: - self.initial[virtual_key] = 'required' + self.initial[virtual_key] = "required" elif self.initial[asked_key]: - self.initial[virtual_key] = 'optional' + self.initial[virtual_key] = "optional" else: - self.initial[virtual_key] = 'do_not_ask' + self.initial[virtual_key] = "do_not_ask" class CancelSettingsForm(SettingsForm): auto_fields = [ - 'cancel_allow_user', - 'cancel_allow_user_until', - 'cancel_allow_user_paid', - 'cancel_allow_user_paid_until', - 'cancel_allow_user_paid_keep', - 'cancel_allow_user_paid_keep_fees', - 'cancel_allow_user_paid_keep_percentage', - 'cancel_allow_user_paid_adjust_fees', - 'cancel_allow_user_paid_adjust_fees_explanation', - 'cancel_allow_user_paid_adjust_fees_step', - 'cancel_allow_user_paid_refund_as_giftcard', - 'cancel_allow_user_paid_require_approval', - 'change_allow_user_variation', - 'change_allow_user_price', - 'change_allow_user_until', + "cancel_allow_user", + "cancel_allow_user_until", + "cancel_allow_user_paid", + "cancel_allow_user_paid_until", + "cancel_allow_user_paid_keep", + "cancel_allow_user_paid_keep_fees", + "cancel_allow_user_paid_keep_percentage", + "cancel_allow_user_paid_adjust_fees", + "cancel_allow_user_paid_adjust_fees_explanation", + "cancel_allow_user_paid_adjust_fees_step", + "cancel_allow_user_paid_refund_as_giftcard", + "cancel_allow_user_paid_require_approval", + "change_allow_user_variation", + "change_allow_user_price", + "change_allow_user_until", ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.obj.settings.giftcard_expiry_years is not None: - self.fields['cancel_allow_user_paid_refund_as_giftcard'].help_text = gettext( - 'You have configured gift cards to be valid {} years plus the year the gift card is issued in.' - ).format(self.obj.settings.giftcard_expiry_years) + self.fields["cancel_allow_user_paid_refund_as_giftcard"].help_text = ( + gettext( + "You have configured gift cards to be valid {} years plus the year the gift card is issued in." + ).format(self.obj.settings.giftcard_expiry_years) + ) class PaymentSettingsForm(SettingsForm): auto_fields = [ - 'payment_term_mode', - 'payment_term_days', - 'payment_term_weekdays', - 'payment_term_minutes', - 'payment_term_last', - 'payment_term_expire_automatically', - 'payment_term_accept_late', - 'payment_pending_hidden', - 'payment_explanation', + "payment_term_mode", + "payment_term_days", + "payment_term_weekdays", + "payment_term_minutes", + "payment_term_last", + "payment_term_expire_automatically", + "payment_term_accept_late", + "payment_pending_hidden", + "payment_explanation", ] tax_rate_default = forms.ModelChoiceField( queryset=TaxRule.objects.none(), - label=_('Tax rule for payment fees'), + label=_("Tax rule for payment fees"), required=False, - help_text=_("The tax rule that applies for additional fees you configured for single payment methods. This " - "will set the tax rate and reverse charge rules, other settings of the tax rule are ignored.") + help_text=_( + "The tax rule that applies for additional fees you configured for single payment methods. This " + "will set the tax rate and reverse charge rules, other settings of the tax rule are ignored." + ), ) def clean_payment_term_days(self): - value = self.cleaned_data.get('payment_term_days') - if self.cleaned_data.get('payment_term_mode') == 'days' and value is None: + value = self.cleaned_data.get("payment_term_days") + if self.cleaned_data.get("payment_term_mode") == "days" and value is None: raise ValidationError(_("This field is required.")) return value def clean_payment_term_minutes(self): - value = self.cleaned_data.get('payment_term_minutes') - if self.cleaned_data.get('payment_term_mode') == 'minutes' and value is None: + value = self.cleaned_data.get("payment_term_minutes") + if self.cleaned_data.get("payment_term_mode") == "minutes" and value is None: raise ValidationError(_("This field is required.")) return value @@ -670,7 +765,7 @@ def clean(self): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['tax_rate_default'].queryset = self.obj.tax_rules.all() + self.fields["tax_rate_default"].queryset = self.obj.tax_rules.all() class ProviderForm(SettingsForm): @@ -680,8 +775,8 @@ class ProviderForm(SettingsForm): """ def __init__(self, *args, **kwargs): - self.settingspref = kwargs.pop('settingspref') - self.provider = kwargs.pop('provider', None) + self.settingspref = kwargs.pop("settingspref") + self.provider = kwargs.pop("provider", None) super().__init__(*args, **kwargs) def prepare_fields(self): @@ -696,20 +791,22 @@ def prepare_fields(self): elif isinstance(v, (RelativeDateTimeField, RelativeDateField)): v.set_event(self.obj) - if hasattr(v, '_as_type'): - self.initial[k] = self.obj.settings.get(k, as_type=v._as_type, default=v.initial) + if hasattr(v, "_as_type"): + self.initial[k] = self.obj.settings.get( + k, as_type=v._as_type, default=v.initial + ) def clean(self): cleaned_data = super().clean() - enabled = cleaned_data.get(self.settingspref + '_enabled') + enabled = cleaned_data.get(self.settingspref + "_enabled") if not enabled: return - if cleaned_data.get(self.settingspref + '_hidden_url', None): - cleaned_data[self.settingspref + '_hidden_url'] = None + if cleaned_data.get(self.settingspref + "_hidden_url", None): + cleaned_data[self.settingspref + "_hidden_url"] = None for k, v in self.fields.items(): val = cleaned_data.get(k) if v._required and not val: - self.add_error(k, _('This field is required.')) + self.add_error(k, _("This field is required.")) if self.provider: cleaned_data = self.provider.settings_form_clean(cleaned_data) return cleaned_data @@ -718,72 +815,82 @@ def clean(self): class InvoiceSettingsForm(SettingsForm): auto_fields = [ - 'invoice_address_asked', - 'invoice_address_required', - 'invoice_address_vatid', - 'invoice_address_company_required', - 'invoice_address_beneficiary', - 'invoice_address_custom_field', - 'invoice_name_required', - 'invoice_address_not_asked_free', - 'invoice_include_free', - 'invoice_show_payments', - 'invoice_reissue_after_modify', - 'invoice_generate', - 'invoice_attendee_name', - 'invoice_include_expire_date', - 'invoice_numbers_consecutive', - 'invoice_numbers_prefix', - 'invoice_numbers_prefix_cancellations', - 'invoice_numbers_counter_length', - 'invoice_address_explanation_text', - 'invoice_email_attachment', - 'invoice_address_from_name', - 'invoice_address_from', - 'invoice_address_from_zipcode', - 'invoice_address_from_city', - 'invoice_address_from_country', - 'invoice_address_from_tax_id', - 'invoice_address_from_vat_id', - 'invoice_introductory_text', - 'invoice_additional_text', - 'invoice_footer_text', - 'invoice_eu_currencies', - 'invoice_logo_image', + "invoice_address_asked", + "invoice_address_required", + "invoice_address_vatid", + "invoice_address_company_required", + "invoice_address_beneficiary", + "invoice_address_custom_field", + "invoice_name_required", + "invoice_address_not_asked_free", + "invoice_include_free", + "invoice_show_payments", + "invoice_reissue_after_modify", + "invoice_generate", + "invoice_attendee_name", + "invoice_include_expire_date", + "invoice_numbers_consecutive", + "invoice_numbers_prefix", + "invoice_numbers_prefix_cancellations", + "invoice_numbers_counter_length", + "invoice_address_explanation_text", + "invoice_email_attachment", + "invoice_address_from_name", + "invoice_address_from", + "invoice_address_from_zipcode", + "invoice_address_from_city", + "invoice_address_from_country", + "invoice_address_from_tax_id", + "invoice_address_from_vat_id", + "invoice_introductory_text", + "invoice_additional_text", + "invoice_footer_text", + "invoice_eu_currencies", + "invoice_logo_image", ] invoice_generate_sales_channels = forms.MultipleChoiceField( - label=_('Generate invoices for Sales channels'), + label=_("Generate invoices for Sales channels"), choices=[], widget=forms.CheckboxSelectMultiple, - help_text=_("If you have enabled invoice generation in the previous setting, you can limit it here to specific " - "sales channels.") + help_text=_( + "If you have enabled invoice generation in the previous setting, you can limit it here to specific " + "sales channels." + ), ) invoice_renderer = forms.ChoiceField( - label=_("Invoice style"), - required=True, - choices=[] + label=_("Invoice style"), required=True, choices=[] ) invoice_language = forms.ChoiceField( - widget=forms.Select, required=True, + widget=forms.Select, + required=True, label=_("Invoice language"), - choices=[('__user__', _('The user\'s language'))] + settings.LANGUAGES, + choices=[("__user__", _("The user's language"))] + settings.LANGUAGES, ) def __init__(self, *args, **kwargs): - event = kwargs.get('obj') + event = kwargs.get("obj") super().__init__(*args, **kwargs) - self.fields['invoice_renderer'].choices = [ - (r.identifier, r.verbose_name) for r in event.get_invoice_renderers().values() + self.fields["invoice_renderer"].choices = [ + (r.identifier, r.verbose_name) + for r in event.get_invoice_renderers().values() ] - self.fields['invoice_numbers_prefix'].widget.attrs['placeholder'] = event.slug.upper() + '-' + self.fields["invoice_numbers_prefix"].widget.attrs["placeholder"] = ( + event.slug.upper() + "-" + ) if event.settings.invoice_numbers_prefix: - self.fields['invoice_numbers_prefix_cancellations'].widget.attrs['placeholder'] = event.settings.invoice_numbers_prefix + self.fields["invoice_numbers_prefix_cancellations"].widget.attrs[ + "placeholder" + ] = event.settings.invoice_numbers_prefix else: - self.fields['invoice_numbers_prefix_cancellations'].widget.attrs['placeholder'] = event.slug.upper() + '-' + self.fields["invoice_numbers_prefix_cancellations"].widget.attrs[ + "placeholder" + ] = (event.slug.upper() + "-") locale_names = dict(settings.LANGUAGES) - self.fields['invoice_language'].choices = [('__user__', _('The user\'s language'))] + [(a, locale_names[a]) for a in event.settings.locales] - self.fields['invoice_generate_sales_channels'].choices = ( + self.fields["invoice_language"].choices = [ + ("__user__", _("The user's language")) + ] + [(a, locale_names[a]) for a in event.settings.locales] + self.fields["invoice_generate_sales_channels"].choices = ( (c.identifier, c.verbose_name) for c in get_all_sales_channels().values() ) @@ -796,7 +903,7 @@ def clean(self): def multimail_validate(val): - s = val.split(',') + s = val.split(",") for part in s: validate_email(part.strip()) return s @@ -804,35 +911,45 @@ def multimail_validate(val): def contains_web_channel_validate(val): if "web" not in val: - raise ValidationError(_("The online shop must be selected to receive these emails.")) + raise ValidationError( + _("The online shop must be selected to receive these emails.") + ) class MailSettingsForm(SMTPSettingsMixin, SettingsForm): auto_fields = [ - 'mail_prefix', - 'mail_from', - 'mail_from_name', - 'mail_attach_ical', - 'mail_attach_tickets', + "mail_prefix", + "mail_from", + "mail_from_name", + "mail_attach_ical", + "mail_attach_tickets", ] mail_sales_channel_placed_paid = forms.MultipleChoiceField( - choices=lambda: [(ident, sc.verbose_name) for ident, sc in get_all_sales_channels().items()], - label=_('Sales channels for checkout emails'), - help_text=_('The order placed and paid emails will only be send to orders from these sales channels. ' - 'The online shop must be enabled.'), + choices=lambda: [ + (ident, sc.verbose_name) for ident, sc in get_all_sales_channels().items() + ], + label=_("Sales channels for checkout emails"), + help_text=_( + "The order placed and paid emails will only be send to orders from these sales channels. " + "The online shop must be enabled." + ), widget=forms.CheckboxSelectMultiple( - attrs={'class': 'scrolling-multiple-choice'} + attrs={"class": "scrolling-multiple-choice"} ), validators=[contains_web_channel_validate], ) mail_sales_channel_download_reminder = forms.MultipleChoiceField( - choices=lambda: [(ident, sc.verbose_name) for ident, sc in get_all_sales_channels().items()], - label=_('Sales channels'), - help_text=_('This email will only be send to orders from these sales channels. The online shop must be enabled.'), + choices=lambda: [ + (ident, sc.verbose_name) for ident, sc in get_all_sales_channels().items() + ], + label=_("Sales channels"), + help_text=_( + "This email will only be send to orders from these sales channels. The online shop must be enabled." + ), widget=forms.CheckboxSelectMultiple( - attrs={'class': 'scrolling-multiple-choice'} + attrs={"class": "scrolling-multiple-choice"} ), validators=[contains_web_channel_validate], ) @@ -842,25 +959,22 @@ class MailSettingsForm(SMTPSettingsMixin, SettingsForm): help_text=_("All emails will be sent to this address as a Bcc copy"), validators=[multimail_validate], required=False, - max_length=255 + max_length=255, ) mail_text_signature = I18nFormField( label=_("Signature"), required=False, widget=I18nTextarea, - help_text=_("This will be attached to every email. Available placeholders: {event}"), - validators=[PlaceholderValidator(['{event}'])], - widget_kwargs={'attrs': { - 'rows': '4', - 'placeholder': _( - 'e.g. your contact details' - ) - }} + help_text=_( + "This will be attached to every email. Available placeholders: {event}" + ), + validators=[PlaceholderValidator(["{event}"])], + widget_kwargs={ + "attrs": {"rows": "4", "placeholder": _("e.g. your contact details")} + }, ) mail_html_renderer = forms.ChoiceField( - label=_("HTML mail renderer"), - required=True, - choices=[] + label=_("HTML mail renderer"), required=True, choices=[] ) mail_text_order_placed = I18nFormField( label=_("Text sent to order contact address"), @@ -869,8 +983,10 @@ class MailSettingsForm(SMTPSettingsMixin, SettingsForm): ) mail_send_order_placed_attendee = forms.BooleanField( label=_("Send an email to attendees"), - help_text=_('If the order contains attendees with email addresses different from the person who orders the ' - 'tickets, the following email will be sent out to the attendees.'), + help_text=_( + "If the order contains attendees with email addresses different from the person who orders the " + "tickets, the following email will be sent out to the attendees." + ), required=False, ) mail_text_order_placed_attendee = I18nFormField( @@ -886,8 +1002,10 @@ class MailSettingsForm(SMTPSettingsMixin, SettingsForm): ) mail_send_order_paid_attendee = forms.BooleanField( label=_("Send an email to attendees"), - help_text=_('If the order contains attendees with email addresses different from the person who orders the ' - 'tickets, the following email will be sent out to the attendees.'), + help_text=_( + "If the order contains attendees with email addresses different from the person who orders the " + "tickets, the following email will be sent out to the attendees." + ), required=False, ) mail_text_order_paid_attendee = I18nFormField( @@ -903,8 +1021,10 @@ class MailSettingsForm(SMTPSettingsMixin, SettingsForm): ) mail_send_order_free_attendee = forms.BooleanField( label=_("Send an email to attendees"), - help_text=_('If the order contains attendees with email addresses different from the person who orders the ' - 'tickets, the following email will be sent out to the attendees.'), + help_text=_( + "If the order contains attendees with email addresses different from the person who orders the " + "tickets, the following email will be sent out to the attendees." + ), required=False, ) mail_text_order_free_attendee = I18nFormField( @@ -932,8 +1052,10 @@ class MailSettingsForm(SMTPSettingsMixin, SettingsForm): label=_("Number of days"), required=True, min_value=0, - help_text=_("This email will be sent out this many days before the order expires. If the " - "value is 0, the mail will never be sent.") + help_text=_( + "This email will be sent out this many days before the order expires. If the " + "value is 0, the mail will never be sent." + ), ) mail_text_order_expire_warning = I18nFormField( label=_("Text"), @@ -962,8 +1084,10 @@ class MailSettingsForm(SMTPSettingsMixin, SettingsForm): ) mail_send_download_reminder_attendee = forms.BooleanField( label=_("Send an email to attendees"), - help_text=_('If the order contains attendees with email addresses different from the person who orders the ' - 'tickets, the following email will be sent out to the attendees.'), + help_text=_( + "If the order contains attendees with email addresses different from the person who orders the " + "tickets, the following email will be sent out to the attendees." + ), required=False, ) mail_text_download_reminder_attendee = I18nFormField( @@ -975,8 +1099,10 @@ class MailSettingsForm(SMTPSettingsMixin, SettingsForm): label=_("Number of days"), required=False, min_value=0, - help_text=_("This email will be sent out this many days before the order event starts. If the " - "field is empty, the mail will never be sent.") + help_text=_( + "This email will be sent out this many days before the order event starts. If the " + "field is empty, the mail will never be sent." + ), ) mail_text_order_placed_require_approval = I18nFormField( label=_("Received order"), @@ -987,15 +1113,19 @@ class MailSettingsForm(SMTPSettingsMixin, SettingsForm): label=_("Approved order"), required=False, widget=I18nTextarea, - help_text=_("This will only be sent out for non-free orders. Free orders will receive the free order " - "template from below instead."), + help_text=_( + "This will only be sent out for non-free orders. Free orders will receive the free order " + "template from below instead." + ), ) mail_text_order_approved_free = I18nFormField( label=_("Approved free order"), required=False, widget=I18nTextarea, - help_text=_("This will only be sent out for free orders. Non-free orders will receive the non-free order " - "template from above instead."), + help_text=_( + "This will only be sent out for free orders. Non-free orders will receive the non-free order " + "template from above instead." + ), ) mail_text_order_denied = I18nFormField( label=_("Denied order"), @@ -1004,108 +1134,102 @@ class MailSettingsForm(SMTPSettingsMixin, SettingsForm): ) smtp_use_custom = forms.BooleanField( label=_("Use Custom Email"), - help_text=_("All mail related to your event will be sent over your specified email gateway."), - required=False + help_text=_( + "All mail related to your event will be sent over your specified email gateway." + ), + required=False, ) send_grid_api_key = forms.CharField( label=_("Sendgrid Token"), required=True, - widget=forms.TextInput(attrs={'placeholder': 'SG.xxxxxxxx'}) + widget=forms.TextInput(attrs={"placeholder": "SG.xxxxxxxx"}), ) - smtp_select = [ - - ('sendgrid', _("SendGrid")), - ('smtp', _("SMTP"))] - + smtp_select = [("sendgrid", _("SendGrid")), ("smtp", _("SMTP"))] email_vendor = forms.ChoiceField( - label=_(""), - required=True, - widget=forms.RadioSelect, - choices=smtp_select + label=_(""), required=True, widget=forms.RadioSelect, choices=smtp_select ) smtp_host = forms.CharField( label=_("Hostname"), required=False, - widget=forms.TextInput(attrs={'placeholder': 'mail.example.org'}) + widget=forms.TextInput(attrs={"placeholder": "mail.example.org"}), ) smtp_port = forms.IntegerField( label=_("Port"), required=False, - widget=forms.TextInput(attrs={'placeholder': 'e.g. 587, 465, 25, ...'}) + widget=forms.TextInput(attrs={"placeholder": "e.g. 587, 465, 25, ..."}), ) smtp_username = forms.CharField( label=_("Username"), - widget=forms.TextInput(attrs={'placeholder': 'myuser@example.org'}), - required=False + widget=forms.TextInput(attrs={"placeholder": "myuser@example.org"}), + required=False, ) smtp_password = forms.CharField( label=_("Password"), required=False, - widget=forms.PasswordInput(attrs={ - 'autocomplete': 'new-password' # see https://bugs.chromium.org/p/chromium/issues/detail?id=370363#c7 - }), + widget=forms.PasswordInput( + attrs={ + "autocomplete": "new-password" # see https://bugs.chromium.org/p/chromium/issues/detail?id=370363#c7 + } + ), ) smtp_use_tls = forms.BooleanField( label=_("Use STARTTLS"), help_text=_("Commonly enabled on port 587."), - required=False + required=False, ) smtp_use_ssl = forms.BooleanField( - label=_("Use SSL"), - help_text=_("Commonly enabled on port 465."), - required=False + label=_("Use SSL"), help_text=_("Commonly enabled on port 465."), required=False ) base_context = { - 'mail_text_order_placed': ['event', 'order', 'payment'], - 'mail_text_order_placed_attendee': ['event', 'order', 'position'], - 'mail_text_order_placed_require_approval': ['event', 'order'], - 'mail_text_order_approved': ['event', 'order'], - 'mail_text_order_approved_free': ['event', 'order'], - 'mail_text_order_denied': ['event', 'order', 'comment'], - 'mail_text_order_paid': ['event', 'order', 'payment_info'], - 'mail_text_order_paid_attendee': ['event', 'order', 'position'], - 'mail_text_order_free': ['event', 'order'], - 'mail_text_order_free_attendee': ['event', 'order', 'position'], - 'mail_text_order_changed': ['event', 'order'], - 'mail_text_order_canceled': ['event', 'order'], - 'mail_text_order_expire_warning': ['event', 'order'], - 'mail_text_order_custom_mail': ['event', 'order'], - 'mail_text_download_reminder': ['event', 'order'], - 'mail_text_download_reminder_attendee': ['event', 'order', 'position'], - 'mail_text_resend_link': ['event', 'order'], - 'mail_text_waiting_list': ['event', 'waiting_list_entry'], - 'mail_text_resend_all_links': ['event', 'orders'] + "mail_text_order_placed": ["event", "order", "payment"], + "mail_text_order_placed_attendee": ["event", "order", "position"], + "mail_text_order_placed_require_approval": ["event", "order"], + "mail_text_order_approved": ["event", "order"], + "mail_text_order_approved_free": ["event", "order"], + "mail_text_order_denied": ["event", "order", "comment"], + "mail_text_order_paid": ["event", "order", "payment_info"], + "mail_text_order_paid_attendee": ["event", "order", "position"], + "mail_text_order_free": ["event", "order"], + "mail_text_order_free_attendee": ["event", "order", "position"], + "mail_text_order_changed": ["event", "order"], + "mail_text_order_canceled": ["event", "order"], + "mail_text_order_expire_warning": ["event", "order"], + "mail_text_order_custom_mail": ["event", "order"], + "mail_text_download_reminder": ["event", "order"], + "mail_text_download_reminder_attendee": ["event", "order", "position"], + "mail_text_resend_link": ["event", "order"], + "mail_text_waiting_list": ["event", "waiting_list_entry"], + "mail_text_resend_all_links": ["event", "orders"], } def _set_field_placeholders(self, fn, base_parameters): phs = [ - '{%s}' % p - for p in sorted(get_available_placeholders(self.event, base_parameters).keys()) + "{%s}" % p + for p in sorted( + get_available_placeholders(self.event, base_parameters).keys() + ) ] - ht = _('Available placeholders: {list}').format( - list=', '.join(phs) - ) + ht = _("Available placeholders: {list}").format(list=", ".join(phs)) if self.fields[fn].help_text: - self.fields[fn].help_text += ' ' + str(ht) + self.fields[fn].help_text += " " + str(ht) else: self.fields[fn].help_text = ht - self.fields[fn].validators.append( - PlaceholderValidator(phs) - ) + self.fields[fn].validators.append(PlaceholderValidator(phs)) def __init__(self, *args, **kwargs): - self.event = event = kwargs.get('obj') + self.event = event = kwargs.get("obj") super().__init__(*args, **kwargs) - self.fields['mail_html_renderer'].choices = [ - (r.identifier, r.verbose_name) for r in event.get_html_mail_renderers().values() + self.fields["mail_html_renderer"].choices = [ + (r.identifier, r.verbose_name) + for r in event.get_html_mail_renderers().values() ] for k, v in self.base_context.items(): self._set_field_placeholders(k, v) for k, v in list(self.fields.items()): - if k.endswith('_attendee') and not event.settings.attendee_emails_asked: + if k.endswith("_attendee") and not event.settings.attendee_emails_asked: # If we don't ask for attendee emails, we can't send them anything and we don't need to clutter # the user interface with it del self.fields[k] @@ -1113,27 +1237,28 @@ def __init__(self, *args, **kwargs): class TicketSettingsForm(SettingsForm): auto_fields = [ - 'ticket_download', - 'ticket_download_date', - 'ticket_download_addons', - 'ticket_download_nonadm', - 'ticket_download_pending', - 'ticket_download_require_validated_email', - 'require_registered_account_for_tickets' + "ticket_download", + "ticket_download_date", + "ticket_download_addons", + "ticket_download_nonadm", + "ticket_download_pending", + "ticket_download_require_validated_email", + "require_registered_account_for_tickets", ] ticket_secret_generator = forms.ChoiceField( label=_("Ticket code generator"), help_text=_("For advanced users, usually does not need to be changed."), required=True, widget=forms.RadioSelect, - choices=[] + choices=[], ) def __init__(self, *args, **kwargs): - event = kwargs.get('obj') + event = kwargs.get("obj") super().__init__(*args, **kwargs) - self.fields['ticket_secret_generator'].choices = [ - (r.identifier, r.verbose_name) for r in event.ticket_secret_generators.values() + self.fields["ticket_secret_generator"].choices = [ + (r.identifier, r.verbose_name) + for r in event.ticket_secret_generators.values() ] def prepare_fields(self): @@ -1150,74 +1275,66 @@ def prepare_fields(self): def clean(self): # required=True files should only be required if the feature is enabled cleaned_data = super().clean() - enabled = cleaned_data.get('ticket_download') == 'True' + enabled = cleaned_data.get("ticket_download") == "True" if not enabled: return for k, v in self.fields.items(): val = cleaned_data.get(k) if v._required and (val is None or val == ""): - self.add_error(k, _('This field is required.')) + self.add_error(k, _("This field is required.")) class CommentForm(I18nModelForm): def __init__(self, *args, **kwargs): - self.readonly = kwargs.pop('readonly', None) + self.readonly = kwargs.pop("readonly", None) super().__init__(*args, **kwargs) if self.readonly: - self.fields['comment'].widget.attrs['readonly'] = 'readonly' + self.fields["comment"].widget.attrs["readonly"] = "readonly" class Meta: model = Event - fields = ['comment'] + fields = ["comment"] widgets = { - 'comment': forms.Textarea(attrs={ - 'rows': 3, - 'class': 'helper-width-100', - }), + "comment": forms.Textarea( + attrs={ + "rows": 3, + "class": "helper-width-100", + } + ), } class CountriesAndEU(CachedCountries): - override = { - 'ZZ': _('Any country'), - 'EU': _('European Union') - } - first = ['ZZ', 'EU'] - cache_subkey = 'with_any_or_eu' + override = {"ZZ": _("Any country"), "EU": _("European Union")} + first = ["ZZ", "EU"] + cache_subkey = "with_any_or_eu" class TaxRuleLineForm(I18nForm): - country = LazyTypedChoiceField( - choices=CountriesAndEU(), - required=False - ) + country = LazyTypedChoiceField(choices=CountriesAndEU(), required=False) address_type = forms.ChoiceField( choices=[ - ('', _('Any customer')), - ('individual', _('Individual')), - ('business', _('Business')), - ('business_vat_id', _('Business with valid VAT ID')), + ("", _("Any customer")), + ("individual", _("Individual")), + ("business", _("Business")), + ("business_vat_id", _("Business with valid VAT ID")), ], - required=False + required=False, ) action = forms.ChoiceField( choices=[ - ('vat', _('Charge VAT')), - ('reverse', _('Reverse charge')), - ('no', _('No VAT')), - ('block', _('Sale not allowed')), + ("vat", _("Charge VAT")), + ("reverse", _("Reverse charge")), + ("no", _("No VAT")), + ("block", _("Sale not allowed")), ], ) rate = forms.DecimalField( - label=_('Deviating tax rate'), - max_digits=10, decimal_places=2, - required=False + label=_("Deviating tax rate"), max_digits=10, decimal_places=2, required=False ) invoice_text = I18nFormField( - label=_('Text on invoice'), - required=False, - widget=I18nTextInput + label=_("Text on invoice"), required=False, widget=I18nTextInput ) @@ -1225,74 +1342,83 @@ class I18nBaseFormSet(I18nFormSetMixin, forms.BaseFormSet): # compatibility shim for django-i18nfield library def __init__(self, *args, **kwargs): - self.event = kwargs.pop('event', None) + self.event = kwargs.pop("event", None) if self.event: - kwargs['locales'] = self.event.settings.get('locales') + kwargs["locales"] = self.event.settings.get("locales") super().__init__(*args, **kwargs) TaxRuleLineFormSet = formset_factory( - TaxRuleLineForm, formset=I18nBaseFormSet, - can_order=True, can_delete=True, extra=0 + TaxRuleLineForm, formset=I18nBaseFormSet, can_order=True, can_delete=True, extra=0 ) class TaxRuleForm(I18nModelForm): class Meta: model = TaxRule - fields = ['name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country'] + fields = [ + "name", + "rate", + "price_includes_tax", + "eu_reverse_charge", + "home_country", + ] class WidgetCodeForm(forms.Form): subevent = forms.ModelChoiceField( - label=pgettext_lazy('subevent', "Date"), + label=pgettext_lazy("subevent", "Date"), required=False, - queryset=SubEvent.objects.none() + queryset=SubEvent.objects.none(), ) language = forms.ChoiceField( - label=_("Language"), - required=True, - choices=settings.LANGUAGES + label=_("Language"), required=True, choices=settings.LANGUAGES ) voucher = forms.CharField( label=_("Pre-selected voucher"), required=False, - help_text=_("If set, the widget will show products as if this voucher has been entered and when a product is " - "bought via the widget, this voucher will be used. This can for example be used to provide " - "widgets that give discounts or unlock secret products.") + help_text=_( + "If set, the widget will show products as if this voucher has been entered and when a product is " + "bought via the widget, this voucher will be used. This can for example be used to provide " + "widgets that give discounts or unlock secret products." + ), ) compatibility_mode = forms.BooleanField( label=_("Compatibility mode"), required=False, - help_text=_("Our regular widget doesn't work in all website builders. If you run into trouble, try using " - "this compatibility mode.") + help_text=_( + "Our regular widget doesn't work in all website builders. If you run into trouble, try using " + "this compatibility mode." + ), ) def __init__(self, *args, **kwargs): - self.event = kwargs.pop('event') + self.event = kwargs.pop("event") super().__init__(*args, **kwargs) if self.event.has_subevents: - self.fields['subevent'].queryset = self.event.subevents.all() + self.fields["subevent"].queryset = self.event.subevents.all() else: - del self.fields['subevent'] + del self.fields["subevent"] - self.fields['language'].choices = [(l, n) for l, n in settings.LANGUAGES if l in self.event.settings.locales] + self.fields["language"].choices = [ + (l, n) for l, n in settings.LANGUAGES if l in self.event.settings.locales + ] def clean_voucher(self): - v = self.cleaned_data.get('voucher') + v = self.cleaned_data.get("voucher") if not v: return if not self.event.vouchers.filter(code=v).exists(): - raise ValidationError(_('The given voucher code does not exist.')) + raise ValidationError(_("The given voucher code does not exist.")) return v class EventDeleteForm(forms.Form): error_messages = { - 'slug_wrong': _("The slug you entered was not correct."), + "slug_wrong": _("The slug you entered was not correct."), } slug = forms.CharField( max_length=255, @@ -1300,15 +1426,15 @@ class EventDeleteForm(forms.Form): ) def __init__(self, *args, **kwargs): - self.event = kwargs.pop('event') + self.event = kwargs.pop("event") super().__init__(*args, **kwargs) def clean_slug(self): - slug = self.cleaned_data.get('slug') + slug = self.cleaned_data.get("slug") if slug != self.event.slug: raise forms.ValidationError( - self.error_messages['slug_wrong'], - code='slug_wrong', + self.error_messages["slug_wrong"], + code="slug_wrong", ) return slug @@ -1316,31 +1442,41 @@ def clean_slug(self): class QuickSetupForm(I18nForm): show_quota_left = forms.BooleanField( label=_("Show number of tickets left"), - help_text=_("Publicly show how many tickets of a certain type are still available."), - required=False + help_text=_( + "Publicly show how many tickets of a certain type are still available." + ), + required=False, ) waiting_list_enabled = forms.BooleanField( label=_("Waiting list"), - help_text=_("Once a ticket is sold out, people can add themselves to a waiting list. As soon as a ticket " - "becomes available again, it will be reserved for the first person on the waiting list and this " - "person will receive an email notification with a voucher that can be used to buy a ticket."), - required=False + help_text=_( + "Once a ticket is sold out, people can add themselves to a waiting list. As soon as a ticket " + "becomes available again, it will be reserved for the first person on the waiting list and this " + "person will receive an email notification with a voucher that can be used to buy a ticket." + ), + required=False, ) ticket_download = forms.BooleanField( label=_("Ticket downloads"), - help_text=_("Your customers will be able to download their tickets in PDF format."), - required=False + help_text=_( + "Your customers will be able to download their tickets in PDF format." + ), + required=False, ) attendee_names_required = forms.BooleanField( label=_("Require all attendees to fill in their names"), - help_text=_("By default, we will ask for names but not require them. You can turn this off completely in the " - "settings."), - required=False + help_text=_( + "By default, we will ask for names but not require them. You can turn this off completely in the " + "settings." + ), + required=False, ) imprint_url = forms.URLField( label=_("Imprint URL"), - help_text=_("This should point e.g. to a part of your website that has your contact details and legal " - "information."), + help_text=_( + "This should point e.g. to a part of your website that has your contact details and legal " + "information." + ), required=False, ) schedule_link = forms.URLField( @@ -1361,54 +1497,58 @@ class QuickSetupForm(I18nForm): contact_mail = forms.EmailField( label=_("Contact address"), required=False, - help_text=_("We'll show this publicly to allow attendees to contact you.") + help_text=_("We'll show this publicly to allow attendees to contact you."), ) total_quota = forms.IntegerField( label=_("Total capacity"), min_value=0, - widget=forms.NumberInput( - attrs={ - 'placeholder': '∞' - } - ), - required=False + widget=forms.NumberInput(attrs={"placeholder": "∞"}), + required=False, ) payment_stripe__enabled = forms.BooleanField( label=_("Payment via Stripe"), - help_text=_("Stripe is an online payments processor supporting credit cards and lots of other payment options. " - "To accept payments via Stripe, you will need to set up an account with them, which takes less " - "than five minutes using their simple interface."), - required=False + help_text=_( + "Stripe is an online payments processor supporting credit cards and lots of other payment options. " + "To accept payments via Stripe, you will need to set up an account with them, which takes less " + "than five minutes using their simple interface." + ), + required=False, ) payment_banktransfer__enabled = forms.BooleanField( label=_("Payment by bank transfer"), - help_text=_("Your customers will be instructed to wire the money to your account. You can then import your " - "bank statements to process the payments within pretix, or mark them as paid manually."), - required=False + help_text=_( + "Your customers will be instructed to wire the money to your account. You can then import your " + "bank statements to process the payments within pretix, or mark them as paid manually." + ), + required=False, ) btf = BankTransfer.form_fields() - payment_banktransfer_bank_details_type = btf['bank_details_type'] - payment_banktransfer_bank_details_sepa_name = btf['bank_details_sepa_name'] - payment_banktransfer_bank_details_sepa_iban = btf['bank_details_sepa_iban'] - payment_banktransfer_bank_details_sepa_bic = btf['bank_details_sepa_bic'] - payment_banktransfer_bank_details_sepa_bank = btf['bank_details_sepa_bank'] - payment_banktransfer_bank_details = btf['bank_details'] + payment_banktransfer_bank_details_type = btf["bank_details_type"] + payment_banktransfer_bank_details_sepa_name = btf["bank_details_sepa_name"] + payment_banktransfer_bank_details_sepa_iban = btf["bank_details_sepa_iban"] + payment_banktransfer_bank_details_sepa_bic = btf["bank_details_sepa_bic"] + payment_banktransfer_bank_details_sepa_bank = btf["bank_details_sepa_bank"] + payment_banktransfer_bank_details = btf["bank_details"] def __init__(self, *args, **kwargs): - self.obj = kwargs.pop('event', None) - self.locales = self.obj.settings.get('locales') if self.obj else kwargs.pop('locales', None) - kwargs['locales'] = self.locales + self.obj = kwargs.pop("event", None) + self.locales = ( + self.obj.settings.get("locales") + if self.obj + else kwargs.pop("locales", None) + ) + kwargs["locales"] = self.locales super().__init__(*args, **kwargs) if not self.obj.settings.payment_stripe_connect_client_id: - del self.fields['payment_stripe__enabled'] - self.fields['payment_banktransfer_bank_details'].required = False + del self.fields["payment_stripe__enabled"] + self.fields["payment_banktransfer_bank_details"].required = False for f in self.fields.values(): - if 'data-required-if' in f.widget.attrs: - del f.widget.attrs['data-required-if'] + if "data-required-if" in f.widget.attrs: + del f.widget.attrs["data-required-if"] def clean(self): cleaned_data = super().clean() - if cleaned_data.get('payment_banktransfer__enabled'): + if cleaned_data.get("payment_banktransfer__enabled"): provider = BankTransfer(self.obj) cleaned_data = provider.settings_form_clean(cleaned_data) return cleaned_data @@ -1418,94 +1558,93 @@ class QuickSetupProductForm(I18nForm): name = I18nFormField( max_length=200, # Max length of Quota.name label=_("Product name"), - widget=I18nTextInput + widget=I18nTextInput, ) default_price = forms.DecimalField( label=_("Price (optional)"), - max_digits=7, decimal_places=2, required=False, + max_digits=7, + decimal_places=2, + required=False, localize=True, - widget=forms.TextInput( - attrs={ - 'placeholder': _('Free') - } - ), + widget=forms.TextInput(attrs={"placeholder": _("Free")}), ) quota = forms.IntegerField( label=_("Quantity available"), min_value=0, - widget=forms.NumberInput( - attrs={ - 'placeholder': '∞' - } - ), + widget=forms.NumberInput(attrs={"placeholder": "∞"}), initial=100, - required=False + required=False, ) class BaseQuickSetupProductFormSet(I18nFormSetMixin, forms.BaseFormSet): def __init__(self, *args, **kwargs): - event = kwargs.pop('event', None) + event = kwargs.pop("event", None) if event: - kwargs['locales'] = event.settings.get('locales') + kwargs["locales"] = event.settings.get("locales") super().__init__(*args, **kwargs) QuickSetupProductFormSet = formset_factory( QuickSetupProductForm, formset=BaseQuickSetupProductFormSet, - can_order=False, can_delete=True, extra=0 + can_order=False, + can_delete=True, + extra=0, ) class ItemMetaPropertyForm(forms.ModelForm): class Meta: - fields = ['name', 'default'] - widgets = { - 'default': forms.TextInput() - } + fields = ["name", "default"] + widgets = {"default": forms.TextInput()} class ConfirmTextForm(I18nForm): text = I18nFormField( widget=I18nTextarea, - widget_kwargs={'attrs': {'rows': '2'}}, + widget_kwargs={"attrs": {"rows": "2"}}, ) class BaseConfirmTextFormSet(I18nFormSetMixin, forms.BaseFormSet): def __init__(self, *args, **kwargs): - event = kwargs.pop('event', None) + event = kwargs.pop("event", None) if event: - kwargs['locales'] = event.settings.get('locales') + kwargs["locales"] = event.settings.get("locales") super().__init__(*args, **kwargs) ConfirmTextFormset = formset_factory( ConfirmTextForm, formset=BaseConfirmTextFormSet, - can_order=True, can_delete=True, extra=0 + can_order=True, + can_delete=True, + extra=0, ) class EventFooterLinkForm(I18nModelForm): class Meta: model = EventFooterLinkModel - fields = ('label', 'url') + fields = ("label", "url") class BaseEventFooterLink(I18nFormSetMixin, forms.BaseInlineFormSet): def __init__(self, *args, **kwargs): - event = kwargs.pop('event', None) + event = kwargs.pop("event", None) if event: - kwargs['locales'] = event.settings.get('locales') + kwargs["locales"] = event.settings.get("locales") super().__init__(*args, **kwargs) EventFooterLink = inlineformset_factory( - Event, EventFooterLinkModel, + Event, + EventFooterLinkModel, EventFooterLinkForm, formset=BaseEventFooterLink, - can_order=False, can_delete=True, extra=0 -) \ No newline at end of file + can_order=False, + can_delete=True, + extra=0, +) diff --git a/src/pretix/eventyay_common/tasks.py b/src/pretix/eventyay_common/tasks.py index 27b7d9dd0..558c8ff85 100644 --- a/src/pretix/eventyay_common/tasks.py +++ b/src/pretix/eventyay_common/tasks.py @@ -10,26 +10,30 @@ logger = logging.getLogger(__name__) -@shared_task(bind=True, max_retries=5, default_retry_delay=60) # Retries up to 5 times with a 60-second delay +@shared_task( + bind=True, max_retries=5, default_retry_delay=60 +) # Retries up to 5 times with a 60-second delay def send_organizer_webhook(self, user_id, organizer): # Define the payload to send to the webhook payload = { - 'name': organizer.get('name'), - 'slug': organizer.get('slug'), - 'action': organizer.get('action') + "name": organizer.get("name"), + "slug": organizer.get("slug"), + "action": organizer.get("action"), } # Define the headers, including the Authorization header with the Bearer token headers = get_header_token(user_id) try: # Send the POST request with the payload and the headers - response = requests.post(settings.TALK_HOSTNAME + '/webhook/organiser/', - json=payload, - headers=headers) + response = requests.post( + settings.TALK_HOSTNAME + "/webhook/organiser/", + json=payload, + headers=headers, + ) response.raise_for_status() # Raise exception for bad status codes except requests.RequestException as e: # Log any errors that occur - logger.error('Error sending webhook to talk component: %s', e) + logger.error("Error sending webhook to talk component: %s", e) # Retry the task if an exception occurs (with exponential backoff by default) try: self.retry(exc=e) @@ -37,31 +41,33 @@ def send_organizer_webhook(self, user_id, organizer): logger.error("Max retries exceeded for sending organizer webhook.") -@shared_task(bind=True, max_retries=5, default_retry_delay=60) # Retries up to 5 times with a 60-second delay +@shared_task( + bind=True, max_retries=5, default_retry_delay=60 +) # Retries up to 5 times with a 60-second delay def send_team_webhook(self, user_id, team): # Define the payload to send to the webhook payload = { - 'organiser_slug': team.get('organiser_slug'), - 'name': team.get('name'), - 'old_name': team.get('old_name'), - 'all_events': team.get('all_events'), - 'can_create_events': team.get('can_create_events'), - 'can_change_teams': team.get('can_change_teams'), - 'can_change_organiser_settings': team.get('can_change_organizer_settings'), - 'can_change_event_settings': team.get('can_change_event_settings'), - 'action': team.get('action') + "organiser_slug": team.get("organiser_slug"), + "name": team.get("name"), + "old_name": team.get("old_name"), + "all_events": team.get("all_events"), + "can_create_events": team.get("can_create_events"), + "can_change_teams": team.get("can_change_teams"), + "can_change_organiser_settings": team.get("can_change_organizer_settings"), + "can_change_event_settings": team.get("can_change_event_settings"), + "action": team.get("action"), } headers = get_header_token(user_id) try: # Send the POST request with the payload and the headers - response = requests.post(settings.TALK_HOSTNAME + '/webhook/team/', - json=payload, - headers=headers) + response = requests.post( + settings.TALK_HOSTNAME + "/webhook/team/", json=payload, headers=headers + ) response.raise_for_status() # Raise exception for bad status codes except requests.RequestException as e: # Log any errors that occur - logger.error('Error sending webhook to talk component: %s', e) + logger.error("Error sending webhook to talk component: %s", e) # Retry the task if an exception occurs (with exponential backoff by default) try: self.retry(exc=e) @@ -69,81 +75,86 @@ def send_team_webhook(self, user_id, team): logger.error("Max retries exceeded for sending organizer webhook.") -@shared_task(bind=True, max_retries=5, default_retry_delay=60) # Retries up to 5 times with a 60-second delay +@shared_task( + bind=True, max_retries=5, default_retry_delay=60 +) # Retries up to 5 times with a 60-second delay def send_event_webhook(self, user_id, event, action): # Define the payload to send to the webhook user_model = get_user_model() user = user_model.objects.get(id=user_id) payload = { - 'organiser_slug': event.get('organiser_slug'), - 'name': event.get('name'), - 'slug': event.get('slug'), - 'date_from': event.get('date_from'), - 'date_to': event.get('date_to'), - 'timezone': event.get('timezone'), - 'locale': event.get('locale'), - 'locales': event.get('locales'), - 'user_email': user.email, - 'action': action + "organiser_slug": event.get("organiser_slug"), + "name": event.get("name"), + "slug": event.get("slug"), + "date_from": event.get("date_from"), + "date_to": event.get("date_to"), + "timezone": event.get("timezone"), + "locale": event.get("locale"), + "locales": event.get("locales"), + "user_email": user.email, + "action": action, } headers = get_header_token(user_id) try: # Send the POST request with the payload and the headers - response = requests.post(settings.TALK_HOSTNAME + '/webhook/event/', - json=payload, - headers=headers) + response = requests.post( + settings.TALK_HOSTNAME + "/webhook/event/", json=payload, headers=headers + ) response.raise_for_status() # Raise exception for bad status codes except requests.RequestException as e: # Log any errors that occur - logger.error('Error sending webhook to talk component: %s', e) + logger.error("Error sending webhook to talk component: %s", e) # Retry the task if an exception occurs (with exponential backoff by default) try: self.retry(exc=e) except self.MaxRetriesExceededError: logger.error("Max retries exceeded for sending organizer webhook.") -@shared_task(bind=True, max_retries=5, default_retry_delay=60) # Retries up to 5 times with a 60-second delay -def create_world(self, is_video_create, data): + +@shared_task( + bind=True, max_retries=5, default_retry_delay=60 +) # Retries up to 5 times with a 60-second delay +def create_world(self, is_video_creation, data): """ Create video system for the event @self: task instance - @param is_video_create: allow user to add video system + @param is_video_creation: allow user to add video system @param data: event's data """ event_slug = data.get("id") title = data.get("title") event_timezone = data.get("timezone") locale = data.get("locale") - token = data.get("token") + token = data.get("token") has_permission = data.get("has_permission") payload = { - 'id': event_slug, - 'title': title, - 'timezone': event_timezone, - 'locale': locale, + "id": event_slug, + "title": title, + "timezone": event_timezone, + "locale": locale, } - headers = { - "Authorization": "Bearer " + token - } + headers = {"Authorization": "Bearer " + token} # Check if user choose add video option and has permission to create video system ('can_create_events' permission) - if is_video_create and has_permission: + if is_video_creation and has_permission: try: - requests.post( + requests.post( "{}/api/v1/create-world/".format(settings.VIDEO_SERVER_HOSTNAME), - json= payload, - headers= headers, + json=payload, + headers=headers, ) - except requests.RequestException as e: - # Log any errors that occur - logger.error('An error occurred while requesting to create a video: %s', e) - try: - self.retry(exc=e) - except self.MaxRetriesExceededError: - logger.error("Max retries exceeded for sending organizer webhook.") + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {e}") + raise self.retry(exc=e) + except requests.exceptions.Timeout as e: + logger.error(f"Request timed out: {e}") + raise self.retry(exc=e) + except requests.exceptions.RequestException as e: + logger.error(f"Request failed: {e}") + raise self.retry(exc=e) def get_header_token(user_id): @@ -155,7 +166,7 @@ def get_header_token(user_id): # Define the headers, including the Authorization header with the Bearer token headers = { - 'Authorization': f'Bearer {token}', - 'Content-Type': 'application/json', + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", } return headers diff --git a/src/pretix/eventyay_common/templates/eventyay_common/event/settings.html b/src/pretix/eventyay_common/templates/eventyay_common/event/settings.html index 6cb84bfdc..c9869ed19 100644 --- a/src/pretix/eventyay_common/templates/eventyay_common/event/settings.html +++ b/src/pretix/eventyay_common/templates/eventyay_common/event/settings.html @@ -25,7 +25,7 @@

{{ request.event.name }} {% trans "- Settings" %}

{% bootstrap_field form.sales_channels layout="control" %}
- {% bootstrap_field form.is_video_create %} + {% bootstrap_field form.is_video_creation %}
diff --git a/src/pretix/eventyay_common/templates/eventyay_common/events/create_foundation.html b/src/pretix/eventyay_common/templates/eventyay_common/events/create_foundation.html index a62de93f1..1dca7ac44 100644 --- a/src/pretix/eventyay_common/templates/eventyay_common/events/create_foundation.html +++ b/src/pretix/eventyay_common/templates/eventyay_common/events/create_foundation.html @@ -30,7 +30,7 @@ - {% bootstrap_field form.is_video_create layout="horizontal" %} + {% bootstrap_field form.is_video_creation layout="horizontal" %} {% bootstrap_field form.organizer layout="horizontal" %}
diff --git a/src/pretix/eventyay_common/utils.py b/src/pretix/eventyay_common/utils.py index 8fa86fd98..efb03011b 100644 --- a/src/pretix/eventyay_common/utils.py +++ b/src/pretix/eventyay_common/utils.py @@ -1,13 +1,11 @@ import hashlib import logging -import random -import string +import secrets from datetime import datetime, timedelta, timezone import jwt from django.conf import settings - logger = logging.getLogger(__name__) @@ -32,14 +30,13 @@ def generate_token(request): def encode_email(email): + random_token = secrets.token_urlsafe(32)[:7] + hash_object = hashlib.sha256(email.encode()) hash_hex = hash_object.hexdigest() short_hash = hash_hex[:7] - characters = string.ascii_letters + string.digits - random_suffix = "".join( - random.choice(characters) for _ in range(7 - len(short_hash)) - ) - final_result = short_hash + random_suffix + + final_result = short_hash + random_token return final_result.upper() @@ -61,4 +58,3 @@ def check_create_permission(request): if is_create_permission or is_active_staff_session: return True return False - diff --git a/src/pretix/eventyay_common/views/event.py b/src/pretix/eventyay_common/views/event.py index 2e61321a7..0ed4d08fd 100644 --- a/src/pretix/eventyay_common/views/event.py +++ b/src/pretix/eventyay_common/views/event.py @@ -1,8 +1,8 @@ from django.conf import settings from django.contrib import messages from django.db import transaction -from django.db.models import Prefetch, Min, Max, F -from django.db.models.functions import Greatest, Coalesce +from django.db.models import F, Max, Min, Prefetch +from django.db.models.functions import Coalesce, Greatest from django.shortcuts import redirect from django.urls import reverse from django.utils.functional import cached_property @@ -12,50 +12,66 @@ from pretix.base.forms import SafeSessionWizardView from pretix.base.i18n import language -from pretix.base.models import Event, EventMetaValue, Quota, Organizer +from pretix.base.models import Event, EventMetaValue, Organizer, Quota from pretix.base.services import tickets from pretix.base.services.quotas import QuotaAvailability -from pretix.control.forms.event import EventWizardFoundationForm, EventWizardBasicsForm, EventUpdateForm +from pretix.control.forms.event import ( + EventUpdateForm, EventWizardBasicsForm, EventWizardFoundationForm, +) from pretix.control.forms.filter import EventFilterForm from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.views import PaginationMixin, UpdateView from pretix.control.views.event import DecoupleMixin, EventSettingsViewMixin from pretix.control.views.item import MetaDataEditorMixin from pretix.eventyay_common.forms.event import EventCommonSettingsForm -from pretix.eventyay_common.tasks import send_event_webhook, create_world -from pretix.eventyay_common.utils import check_create_permission, generate_token +from pretix.eventyay_common.tasks import create_world, send_event_webhook +from pretix.eventyay_common.utils import ( + check_create_permission, generate_token, +) class EventList(PaginationMixin, ListView): model = Event - context_object_name = 'events' - template_name = 'eventyay_common/events/index.html' + context_object_name = "events" + template_name = "eventyay_common/events/index.html" def get_queryset(self): - query_set = self.request.user.get_events_with_any_permission(self.request).prefetch_related( - 'organizer', '_settings_objects', 'organizer___settings_objects', 'organizer__meta_properties', - Prefetch( - 'meta_values', - EventMetaValue.objects.select_related('property'), - to_attr='meta_values_cached' + query_set = ( + self.request.user.get_events_with_any_permission(self.request) + .prefetch_related( + "organizer", + "_settings_objects", + "organizer___settings_objects", + "organizer__meta_properties", + Prefetch( + "meta_values", + EventMetaValue.objects.select_related("property"), + to_attr="meta_values_cached", + ), ) - ).order_by('-date_from') + .order_by("-date_from") + ) query_set = query_set.annotate( - min_from=Min('subevents__date_from'), - max_from=Max('subevents__date_from'), - max_to=Max('subevents__date_to'), - max_fromto=Greatest(Max('subevents__date_to'), Max('subevents__date_from')) + min_from=Min("subevents__date_from"), + max_from=Max("subevents__date_from"), + max_to=Max("subevents__date_to"), + max_fromto=Greatest(Max("subevents__date_to"), Max("subevents__date_from")), ).annotate( - order_from=Coalesce('min_from', 'date_from'), - order_to=Coalesce('max_fromto', 'max_to', 'max_from', 'date_to', 'date_from'), + order_from=Coalesce("min_from", "date_from"), + order_to=Coalesce( + "max_fromto", "max_to", "max_from", "date_to", "date_from" + ), ) query_set = query_set.prefetch_related( - Prefetch('quotas', - queryset=Quota.objects.filter(subevent__isnull=True).annotate(s=Coalesce(F('size'), 0)).order_by( - '-s'), - to_attr='first_quotas') + Prefetch( + "quotas", + queryset=Quota.objects.filter(subevent__isnull=True) + .annotate(s=Coalesce(F("size"), 0)) + .order_by("-s"), + to_attr="first_quotas", + ) ) if self.filter_form.is_valid(): @@ -64,10 +80,10 @@ def get_queryset(self): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - ctx['filter_form'] = self.filter_form + ctx["filter_form"] = self.filter_form quotas = [] - for s in ctx['events']: + for s in ctx["events"]: s.first_quotas = s.first_quotas[:4] quotas += list(s.first_quotas) @@ -82,7 +98,11 @@ def get_context_data(self, **kwargs): if q.size is not None: q.percent_paid = min( 100, - round(q.cached_availability_paid_orders / q.size * 100) if q.size > 0 else 100 + ( + round(q.cached_availability_paid_orders / q.size * 100) + if q.size > 0 + else 100 + ), ) return ctx @@ -93,12 +113,12 @@ def filter_form(self): class EventCreateView(SafeSessionWizardView): form_list = [ - ('foundation', EventWizardFoundationForm), - ('basics', EventWizardBasicsForm), + ("foundation", EventWizardFoundationForm), + ("basics", EventWizardBasicsForm), ] templates = { - 'foundation': 'eventyay_common/events/create_foundation.html', - 'basics': 'eventyay_common/events/create_basics.html', + "foundation": "eventyay_common/events/create_foundation.html", + "basics": "eventyay_common/events/create_basics.html", } condition_dict = {} @@ -107,14 +127,20 @@ def get_form_initial(self, step): request_user = self.request.user request_get = self.request.GET - if step == 'foundation' and 'organizer' in request_get: + if step == "foundation" and "organizer" in request_get: try: queryset = Organizer.objects.all() - if not request_user.has_active_staff_session(self.request.session.session_key): + if not request_user.has_active_staff_session( + self.request.session.session_key + ): queryset = queryset.filter( - id__in=request_user.teams.filter(can_create_events=True).values_list('organizer', flat=True) + id__in=request_user.teams.filter( + can_create_events=True + ).values_list("organizer", flat=True) ) - initial_form['organizer'] = queryset.get(slug=request_get.get('organizer')) + initial_form["organizer"] = queryset.get( + slug=request_get.get("organizer") + ) except Organizer.DoesNotExist: pass @@ -125,34 +151,38 @@ def dispatch(self, request, *args, **kwargs): def get_context_data(self, form, **kwargs): context = super().get_context_data(form, **kwargs) - context['create_for'] = self.storage.extra_data.get('create_for', 'all') - context['has_organizer'] = self.request.user.teams.filter(can_create_events=True).exists() - if self.steps.current == 'basics': - context['organizer'] = self.get_cleaned_data_for_step('foundation').get('organizer') + context["create_for"] = self.storage.extra_data.get("create_for", "all") + context["has_organizer"] = self.request.user.teams.filter( + can_create_events=True + ).exists() + if self.steps.current == "basics": + context["organizer"] = self.get_cleaned_data_for_step("foundation").get( + "organizer" + ) return context def render(self, form=None, **kwargs): - if self.steps.current == 'basics' and 'create_for' in self.request.POST: - self.storage.extra_data['create_for'] = self.request.POST.get('create_for') - if self.steps.current != 'foundation': - form_data = self.get_cleaned_data_for_step('foundation') + if self.steps.current == "basics" and "create_for" in self.request.POST: + self.storage.extra_data["create_for"] = self.request.POST.get("create_for") + if self.steps.current != "foundation": + form_data = self.get_cleaned_data_for_step("foundation") if form_data is None: - return self.render_goto_step('foundation') + return self.render_goto_step("foundation") return super().render(form, **kwargs) def get_form_kwargs(self, step=None): kwargs = { - 'user': self.request.user, - 'session': self.request.session, + "user": self.request.user, + "session": self.request.session, } - if step != 'foundation': - form_data = self.get_cleaned_data_for_step('foundation') + if step != "foundation": + form_data = self.get_cleaned_data_for_step("foundation") if form_data is None: form_data = { - 'organizer': Organizer(slug='_nonexisting'), - 'has_subevents': False, - 'locales': ['en'] + "organizer": Organizer(slug="_nonexisting"), + "has_subevents": False, + "locales": ["en"], } kwargs.update(form_data) return kwargs @@ -161,78 +191,97 @@ def get_template_names(self): return [self.templates[self.steps.current]] def done(self, form_list, form_dict, **kwargs): - foundation_data = self.get_cleaned_data_for_step('foundation') - basics_data = self.get_cleaned_data_for_step('basics') + foundation_data = self.get_cleaned_data_for_step("foundation") + basics_data = self.get_cleaned_data_for_step("basics") - create_for = self.storage.extra_data.get('create_for') + create_for = self.storage.extra_data.get("create_for") - self.request.organizer = foundation_data['organizer'] + self.request.organizer = foundation_data["organizer"] if create_for == "talk": event_dict = { - 'organiser_slug': foundation_data.get('organizer').slug if foundation_data.get('organizer') else None, - 'name': basics_data.get('name').data if basics_data.get('name') else None, - 'slug': basics_data.get('slug'), - 'is_public': False, - 'date_from': str(basics_data.get('date_from')), - 'date_to': str(basics_data.get('date_to')), - 'timezone': str(basics_data.get('timezone')), - 'locale': basics_data.get('locale'), - 'locales': foundation_data.get('locales'), + "organiser_slug": ( + foundation_data.get("organizer").slug + if foundation_data.get("organizer") + else None + ), + "name": ( + basics_data.get("name").data if basics_data.get("name") else None + ), + "slug": basics_data.get("slug"), + "is_public": False, + "date_from": str(basics_data.get("date_from")), + "date_to": str(basics_data.get("date_to")), + "timezone": str(basics_data.get("timezone")), + "locale": basics_data.get("locale"), + "locales": foundation_data.get("locales"), } - send_event_webhook.delay(user_id=self.request.user.id, event=event_dict, action='create') + send_event_webhook.delay( + user_id=self.request.user.id, event=event_dict, action="create" + ) else: - with transaction.atomic(), language(basics_data['locale']): - event = form_dict['basics'].instance - event.organizer = foundation_data['organizer'] + with transaction.atomic(), language(basics_data["locale"]): + event = form_dict["basics"].instance + event.organizer = foundation_data["organizer"] event.plugins = settings.PRETIX_PLUGINS_DEFAULT - event.has_subevents = foundation_data['has_subevents'] + event.has_subevents = foundation_data["has_subevents"] if check_create_permission(self.request): - event.is_video_create = foundation_data['is_video_create'] + event.is_video_creation = foundation_data["is_video_creation"] else: - event.is_video_create = False + event.is_video_creation = False event.testmode = True - form_dict['basics'].save() + form_dict["basics"].save() - event.checkin_lists.create( - name=_('Default'), - all_products=True - ) + event.checkin_lists.create(name=_("Default"), all_products=True) event.set_defaults() - event.settings.set('timezone', basics_data['timezone']) - event.settings.set('locale', basics_data['locale']) - event.settings.set('locales', foundation_data['locales']) - if create_for == 'all': + event.settings.set("timezone", basics_data["timezone"]) + event.settings.set("locale", basics_data["locale"]) + event.settings.set("locales", foundation_data["locales"]) + if create_for == "all": event_dict = { - 'organiser_slug': event.organizer.slug, - 'name': event.name.data, - 'slug': event.slug, - 'is_public': event.live, - 'date_from': str(event.date_from), - 'date_to': str(event.date_to), - 'timezone': str(basics_data.get('timezone')), - 'locale': event.settings.locale, - 'locales': event.settings.locales, + "organiser_slug": event.organizer.slug, + "name": event.name.data, + "slug": event.slug, + "is_public": event.live, + "date_from": str(event.date_from), + "date_to": str(event.date_to), + "timezone": str(basics_data.get("timezone")), + "locale": event.settings.locale, + "locales": event.settings.locales, } - send_event_webhook.delay(user_id=self.request.user.id, event=event_dict, action='create') - event.settings.set('create_for', create_for) + send_event_webhook.delay( + user_id=self.request.user.id, event=event_dict, action="create" + ) + event.settings.set("create_for", create_for) # The user automatically creates a world when selecting the add video option in the create ticket form. - data = dict(id=basics_data.get('slug'), title=basics_data.get('name').data, - timezone=basics_data.get('timezone'), - locale=basics_data.get('locale'), has_permission=check_create_permission(self.request), - token=generate_token(self.request)) - create_world.delay(is_video_create=foundation_data.get('is_video_create'), data=data) + data = dict( + id=basics_data.get("slug"), + title=basics_data.get("name").data, + timezone=basics_data.get("timezone"), + locale=basics_data.get("locale"), + has_permission=check_create_permission(self.request), + token=generate_token(self.request), + ) + create_world.delay( + is_video_creation=foundation_data.get("is_video_creation"), data=data + ) - return redirect(reverse('eventyay_common:events') + '?congratulations=1') + return redirect(reverse("eventyay_common:events") + "?congratulations=1") -class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView): +class EventUpdate( + DecoupleMixin, + EventSettingsViewMixin, + EventPermissionRequiredMixin, + MetaDataEditorMixin, + UpdateView, +): model = Event form_class = EventUpdateForm - template_name = 'eventyay_common/event/settings.html' - permission = 'can_change_event_settings' + template_name = "eventyay_common/event/settings.html" + permission = "can_change_event_settings" @cached_property def object(self) -> Event: @@ -245,78 +294,99 @@ def get_object(self, queryset=None) -> Event: def sform(self): return EventCommonSettingsForm( obj=self.object, - prefix='settings', - data=self.request.POST if self.request.method == 'POST' else None, - files=self.request.FILES if self.request.method == 'POST' else None, + prefix="settings", + data=self.request.POST if self.request.method == "POST" else None, + files=self.request.FILES if self.request.method == "POST" else None, ) def get_context_data(self, *args, **kwargs) -> dict: context = super().get_context_data(*args, **kwargs) - context['sform'] = self.sform + context["sform"] = self.sform talk_host = settings.TALK_HOSTNAME - context['talk_edit_url'] = talk_host + '/orga/event/' + self.object.slug + '/settings' + context["talk_edit_url"] = ( + talk_host + "/orga/event/" + self.object.slug + "/settings" + ) return context + def handle_video_creation(self, form): + if check_create_permission(self.request): + form.instance.is_video_creation = form.cleaned_data.get("is_video_creation") + elif not check_create_permission(self.request) and form.cleaned_data.get( + "is_video_creation" + ): + form.instance.is_video_creation = True + else: + form.instance.is_video_creation = False + + data = dict( + id=form.cleaned_data.get("slug"), + title=form.cleaned_data.get("name").data, + timezone=self.sform.cleaned_data.get("timezone"), + locale=self.sform.cleaned_data.get("locale"), + has_permission=check_create_permission(self.request), + token=generate_token(self.request), + ) + + create_world.delay( + is_video_creation=form.cleaned_data.get("is_video_creation"), data=data + ) + @transaction.atomic def form_valid(self, form): self._save_decoupled(self.sform) self.sform.save() - tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk}) - - if check_create_permission(self.request): - form.instance.is_video_create = form.cleaned_data.get('is_video_create') - elif not check_create_permission(self.request) and form.cleaned_data.get('is_video_create'): - form.instance.is_video_create = True - else: - form.instance.is_video_create = False - - ## The user automatically creates a world when selecting the add video option in the update ticket form. - data = dict(id=form.cleaned_data.get('slug'), title=form.cleaned_data.get('name').data, - timezone=self.sform.cleaned_data.get('timezone'), locale=self.sform.cleaned_data.get('locale'), - has_permission=check_create_permission(self.request), token=generate_token(self.request)) + tickets.invalidate_cache.apply_async(kwargs={"event": self.request.event.pk}) - create_world.delay(is_video_create=form.cleaned_data.get('is_video_create'), data=data) + self.handle_video_creation(form) - messages.success(self.request, _('Your changes have been saved.')) + messages.success(self.request, _("Your changes have been saved.")) return super().form_valid(form) def get_success_url(self) -> str: - return reverse('eventyay_common:event.update', kwargs={ - 'organizer': self.object.organizer.slug, - 'event': self.object.slug, - }) + return reverse( + "eventyay_common:event.update", + kwargs={ + "organizer": self.object.organizer.slug, + "event": self.object.slug, + }, + ) def get_form_kwargs(self): kwargs = super().get_form_kwargs() if self.request.user.has_active_staff_session(self.request.session.session_key): - kwargs['change_slug'] = True - kwargs['domain'] = True + kwargs["change_slug"] = True + kwargs["domain"] = True return kwargs def post(self, request, *args, **kwargs): form = self.get_form() - form.instance.sales_channels = ['web'] + form.instance.sales_channels = ["web"] if form.is_valid() and self.sform.is_valid(): - zone = timezone(self.sform.cleaned_data['timezone']) + zone = timezone(self.sform.cleaned_data["timezone"]) event = form.instance event.date_from = self.reset_timezone(zone, event.date_from) event.date_to = self.reset_timezone(zone, event.date_to) - if event.settings.create_for and event.settings.create_for == 'all': + if event.settings.create_for and event.settings.create_for == "all": event_dict = { - 'organiser_slug': event.organizer.slug, - 'name': event.name.data, - 'slug': event.slug, - 'date_from': str(event.date_from), - 'date_to': str(event.date_to), - 'timezone': str(event.settings.timezone), - 'locale': event.settings.locale, - 'locales': event.settings.locales, + "organiser_slug": event.organizer.slug, + "name": event.name.data, + "slug": event.slug, + "date_from": str(event.date_from), + "date_to": str(event.date_to), + "timezone": str(event.settings.timezone), + "locale": event.settings.locale, + "locales": event.settings.locales, } - send_event_webhook.delay(user_id=self.request.user.id, event=event_dict, action='update') + send_event_webhook.delay( + user_id=self.request.user.id, event=event_dict, action="update" + ) return self.form_valid(form) else: - messages.error(self.request, _('We could not save your changes. See below for details.')) + messages.error( + self.request, + _("We could not save your changes. See below for details."), + ) return self.form_invalid(form) @staticmethod From cf23cdf9dba6d8e8c15c1b9df23f3b987e7fc6ea Mon Sep 17 00:00:00 2001 From: odkhang Date: Fri, 25 Oct 2024 12:18:47 +0700 Subject: [PATCH 07/10] Fix isort, flake8 in pipeline --- src/pretix/eventyay_common/forms/event.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pretix/eventyay_common/forms/event.py b/src/pretix/eventyay_common/forms/event.py index 13d692663..9b5c7327b 100644 --- a/src/pretix/eventyay_common/forms/event.py +++ b/src/pretix/eventyay_common/forms/event.py @@ -1,12 +1,9 @@ from django import forms -from django.conf import settings from django.utils.translation import gettext_lazy as _ from pytz import common_timezones from pretix.base.forms import SettingsForm from pretix.base.settings import validate_event_settings -from pretix.control.forms import MultipleLanguagesWidget -from pretix.control.forms.event import EventWizardFoundationForm class EventCommonSettingsForm(SettingsForm): @@ -16,8 +13,8 @@ class EventCommonSettingsForm(SettingsForm): ) auto_fields = [ - 'locales', - 'locale', + "locales", + "locale", ] def clean(self): @@ -28,5 +25,5 @@ def clean(self): return data def __init__(self, *args, **kwargs): - self.event = kwargs['obj'] + self.event = kwargs["obj"] super().__init__(*args, **kwargs) From 76ea23f12e7a4489a23f35062f47c39e5e7bdada Mon Sep 17 00:00:00 2001 From: lcduong Date: Tue, 29 Oct 2024 18:05:06 +0700 Subject: [PATCH 08/10] fix UT --- src/pretix/control/forms/event.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 06883deb5..99a765dbf 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -294,6 +294,7 @@ def __init__(self, *args, **kwargs): self.session = kwargs.pop("session") kwargs.pop("has_subevents") self.user = kwargs.pop("user") + kwargs.pop("is_video_creation") super().__init__(*args, **kwargs) self.fields["copy_from_event"] = EventChoiceField( From 9d9574184e73bd1ad8780841d93d03a61e723f4d Mon Sep 17 00:00:00 2001 From: odkhang Date: Wed, 30 Oct 2024 14:35:31 +0700 Subject: [PATCH 09/10] Add comment --- src/pretix/eventyay_common/tasks.py | 17 +++++++++++------ src/pretix/eventyay_common/utils.py | 14 +++++++------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/pretix/eventyay_common/tasks.py b/src/pretix/eventyay_common/tasks.py index 558c8ff85..e1f244152 100644 --- a/src/pretix/eventyay_common/tasks.py +++ b/src/pretix/eventyay_common/tasks.py @@ -119,8 +119,8 @@ def create_world(self, is_video_creation, data): """ Create video system for the event @self: task instance - @param is_video_creation: allow user to add video system - @param data: event's data + @param is_video_creation: A boolean value to check if the user has chosen the option to add a video. + @param data: A dictionary containing event details like id, title, timezone, locale, token, has_permission """ event_slug = data.get("id") title = data.get("title") @@ -138,7 +138,12 @@ def create_world(self, is_video_creation, data): headers = {"Authorization": "Bearer " + token} - # Check if user choose add video option and has permission to create video system ('can_create_events' permission) + """ + is_video_creation: A boolean value to check if the user has chosen the option to add a video. + has_permission: A boolean value to check if the user has 'can_create_events' permission or has admin session mode. + payload: A dictionary containing the event details like id, title, timezone, and locale. + To create a world, both conditions must be satisfied: the user must have permission and must choose to create the video option. + """ if is_video_creation and has_permission: try: requests.post( @@ -147,13 +152,13 @@ def create_world(self, is_video_creation, data): headers=headers, ) except requests.exceptions.ConnectionError as e: - logger.error(f"Connection error: {e}") + logger.error("Connection error: %s", str(e)) raise self.retry(exc=e) except requests.exceptions.Timeout as e: - logger.error(f"Request timed out: {e}") + logger.error("Request timed out: %s", str(e)) raise self.retry(exc=e) except requests.exceptions.RequestException as e: - logger.error(f"Request failed: {e}") + logger.error("Request failed: %s", str(e)) raise self.retry(exc=e) diff --git a/src/pretix/eventyay_common/utils.py b/src/pretix/eventyay_common/utils.py index efb03011b..0cb80639b 100644 --- a/src/pretix/eventyay_common/utils.py +++ b/src/pretix/eventyay_common/utils.py @@ -1,6 +1,5 @@ import hashlib import logging -import secrets from datetime import datetime, timedelta, timezone import jwt @@ -30,14 +29,15 @@ def generate_token(request): def encode_email(email): - random_token = secrets.token_urlsafe(32)[:7] - + """ + Generate a unique UID token by hashing the email address using SHA-256 and return the first 7 characters. + @param email: The user's email address. + @return: The UID token, which consists of the first 7 characters of the hashed email. + """ hash_object = hashlib.sha256(email.encode()) hash_hex = hash_object.hexdigest() short_hash = hash_hex[:7] - - final_result = short_hash + random_token - return final_result.upper() + return short_hash.upper() def check_create_permission(request): @@ -45,7 +45,7 @@ def check_create_permission(request): Check if the user has permission to create videos ('can_create_events' permission) and has admin session mode (admin session mode has full permissions) @param request: user request - @return: True if user has permission, False otherwise + @return: true if the user has permission to create videos or has admin session mode else false """ is_create_permission = ( "can_create_events" From 2bc6e6b1712e8946ed8a8b51b812c43a240f99b8 Mon Sep 17 00:00:00 2001 From: odkhang Date: Wed, 30 Oct 2024 16:49:24 +0700 Subject: [PATCH 10/10] Update comment --- src/pretix/eventyay_common/tasks.py | 39 +++++++++++++---------- src/pretix/eventyay_common/views/event.py | 8 ++--- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/pretix/eventyay_common/tasks.py b/src/pretix/eventyay_common/tasks.py index e1f244152..b88aab5f6 100644 --- a/src/pretix/eventyay_common/tasks.py +++ b/src/pretix/eventyay_common/tasks.py @@ -115,19 +115,30 @@ def send_event_webhook(self, user_id, event, action): @shared_task( bind=True, max_retries=5, default_retry_delay=60 ) # Retries up to 5 times with a 60-second delay -def create_world(self, is_video_creation, data): +def create_world(self, is_video_creation, event_data): """ - Create video system for the event - @self: task instance - @param is_video_creation: A boolean value to check if the user has chosen the option to add a video. - @param data: A dictionary containing event details like id, title, timezone, locale, token, has_permission + Create a video system for the specified event. + + :param self: Task instance + :param is_video_creation: A boolean indicating whether the user has chosen to add a video. + :param event_data: A dictionary containing the following event details: + - id (str): The unique identifier for the event. + - title (str): The title of the event. + - timezone (str): The timezone in which the event takes place. + - locale (str): The locale for the event. + - token (str): Authorization token for making the request. + - has_permission (bool): Indicates if the user has 'can_create_events' permission or is in admin session mode. + + To successfully create a world, both conditions must be satisfied: + - The user must have the necessary permission. + - The user must choose to create a video. """ - event_slug = data.get("id") - title = data.get("title") - event_timezone = data.get("timezone") - locale = data.get("locale") - token = data.get("token") - has_permission = data.get("has_permission") + event_slug = event_data.get("id") + title = event_data.get("title") + event_timezone = event_data.get("timezone") + locale = event_data.get("locale") + token = event_data.get("token") + has_permission = event_data.get("has_permission") payload = { "id": event_slug, @@ -138,12 +149,6 @@ def create_world(self, is_video_creation, data): headers = {"Authorization": "Bearer " + token} - """ - is_video_creation: A boolean value to check if the user has chosen the option to add a video. - has_permission: A boolean value to check if the user has 'can_create_events' permission or has admin session mode. - payload: A dictionary containing the event details like id, title, timezone, and locale. - To create a world, both conditions must be satisfied: the user must have permission and must choose to create the video option. - """ if is_video_creation and has_permission: try: requests.post( diff --git a/src/pretix/eventyay_common/views/event.py b/src/pretix/eventyay_common/views/event.py index 0ed4d08fd..3cbadffcd 100644 --- a/src/pretix/eventyay_common/views/event.py +++ b/src/pretix/eventyay_common/views/event.py @@ -256,7 +256,7 @@ def done(self, form_list, form_dict, **kwargs): event.settings.set("create_for", create_for) # The user automatically creates a world when selecting the add video option in the create ticket form. - data = dict( + event_data = dict( id=basics_data.get("slug"), title=basics_data.get("name").data, timezone=basics_data.get("timezone"), @@ -265,7 +265,7 @@ def done(self, form_list, form_dict, **kwargs): token=generate_token(self.request), ) create_world.delay( - is_video_creation=foundation_data.get("is_video_creation"), data=data + is_video_creation=foundation_data.get("is_video_creation"), event_data=event_data ) return redirect(reverse("eventyay_common:events") + "?congratulations=1") @@ -318,7 +318,7 @@ def handle_video_creation(self, form): else: form.instance.is_video_creation = False - data = dict( + event_data = dict( id=form.cleaned_data.get("slug"), title=form.cleaned_data.get("name").data, timezone=self.sform.cleaned_data.get("timezone"), @@ -328,7 +328,7 @@ def handle_video_creation(self, form): ) create_world.delay( - is_video_creation=form.cleaned_data.get("is_video_creation"), data=data + is_video_creation=form.cleaned_data.get("is_video_creation"), event_data=event_data ) @transaction.atomic