Fix #1378 -- API: Allow to access and modify (some) event setti… (#1569)

* API: Allow to access event settings

* Convert most "general" settings

* Smaller fixes

* Add more settings

* Relative dates, nulling

* Fix a test failure

* Fix wrong attribute access
This commit is contained in:
Raphael Michel
2020-02-04 17:06:23 +01:00
committed by GitHub
parent 87b10ef055
commit fd1c964c92
16 changed files with 1357 additions and 585 deletions

View File

@@ -4,7 +4,9 @@ from django.db import transaction
from django.utils.functional import cached_property
from django.utils.translation import ugettext as _
from django_countries.serializers import CountryFieldMixin
from hierarkey.proxy import HierarkeyProxy
from pytz import common_timezones
from rest_framework import serializers
from rest_framework.fields import ChoiceField, Field
from rest_framework.relations import SlugRelatedField
@@ -15,6 +17,8 @@ from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.services.seating import (
SeatProtected, generate_seats, validate_plan_change,
)
from pretix.base.settings import DEFAULTS, validate_settings
from pretix.base.signals import api_event_settings_fields
class MetaDataField(Field):
@@ -469,3 +473,124 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
class Meta:
model = TaxRule
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country')
class EventSettingsSerializer(serializers.Serializer):
default_fields = [
'imprint_url',
'checkout_email_helptext',
'presale_has_ended_text',
'voucher_explanation_text',
'show_date_to',
'show_times',
'show_items_outside_presale_period',
'display_net_prices',
'presale_start_show_date',
'locales',
'locale',
'last_order_modification_date',
'show_quota_left',
'waiting_list_enabled',
'waiting_list_hours',
'waiting_list_auto',
'max_items_per_order',
'reservation_time',
'contact_mail',
'show_variations_expanded',
'hide_sold_out',
'meta_noindex',
'redirect_to_checkout_directly',
'frontpage_subevent_ordering',
'frontpage_text',
'attendee_names_asked',
'attendee_names_required',
'attendee_emails_asked',
'attendee_emails_required',
'confirm_text',
'order_email_asked_twice',
'payment_term_days',
'payment_term_last',
'payment_term_weekdays',
'payment_term_expire_automatically',
'payment_term_accept_late',
'payment_explanation',
'ticket_download',
'ticket_download_date',
'ticket_download_addons',
'ticket_download_nonadm',
'ticket_download_pending',
'mail_prefix',
'mail_from',
'mail_from_name',
'mail_attach_ical',
'invoice_address_asked',
'invoice_address_required',
'invoice_address_vatid',
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_name_required',
'invoice_address_not_asked_free',
'invoice_include_free',
'invoice_generate',
'invoice_numbers_consecutive',
'invoice_numbers_prefix',
'invoice_numbers_prefix_cancellations',
'invoice_attendee_name',
'invoice_include_expire_date',
'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',
'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',
]
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
super().__init__(*args, **kwargs)
for fname in self.default_fields:
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
kwargs.setdefault('required', False)
kwargs.setdefault('allow_null', True)
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
if 'serializer_class' not in DEFAULTS[fname]:
raise ValidationError('{} has no serializer class'.format(fname))
f = DEFAULTS[fname]['serializer_class'](
**kwargs
)
f._label = form_kwargs.get('label', fname)
f._help_text = form_kwargs.get('help_text')
self.fields[fname] = f
for recv, resp in api_event_settings_fields.send(sender=self.event):
for fname, field in resp.items():
field.required = False
self.fields[fname] = field
def update(self, instance: HierarkeyProxy, validated_data):
for attr, value in validated_data.items():
if value is None:
instance.delete(attr)
elif instance.get(attr, as_type=type(value)) != value:
instance.set(attr, value)
return instance
def validate(self, data):
data = super().validate(data)
settings_dict = self.instance.freeze()
settings_dict.update(data)
validate_settings(self.event, settings_dict)
return data

View File

@@ -67,6 +67,8 @@ for app in apps.get_app_configs():
urlpatterns = [
url(r'^', include(router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/', include(orga_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/settings/$', event.EventSettingsView.as_view(),
name="event.settings"),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/teams/(?P<team>[^/]+)/', include(team_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),

View File

@@ -4,13 +4,14 @@ from django.db.models import ProtectedError, Q
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import filters, viewsets
from rest_framework import filters, views, viewsets
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from pretix.api.auth.permission import EventCRUDPermission
from pretix.api.serializers.event import (
CloneEventSerializer, EventSerializer, SubEventSerializer,
TaxRuleSerializer,
CloneEventSerializer, EventSerializer, EventSettingsSerializer,
SubEventSerializer, TaxRuleSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
@@ -333,3 +334,33 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
auth=self.request.auth,
)
super().perform_destroy(instance)
class EventSettingsView(views.APIView):
permission = 'can_change_event_settings'
def get(self, request, *args, **kwargs):
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
if 'explain' in request.GET:
return Response({
fname: {
'value': s.data[fname],
'label': getattr(field, '_label', fname),
'help_text': getattr(field, '_help_text', None)
} for fname, field in s.fields.items()
})
return Response(s.data)
def patch(self, request, *wargs, **kwargs):
s = EventSettingsSerializer(instance=request.event.settings, data=request.data, partial=True,
event=request.event)
s.is_valid(raise_exception=True)
with transaction.atomic():
s.save()
self.request.event.log_action(
'pretix.event.settings', user=self.request.user, auth=self.request.auth, data={
k: v for k, v in s.validated_data.items()
}
)
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
return Response(s.data)

View File

@@ -1,5 +1,4 @@
from django.apps import AppConfig
from django.conf import settings
class PretixBaseConfig(AppConfig):
@@ -14,6 +13,7 @@ class PretixBaseConfig(AppConfig):
from . import notifications # NOQA
from . import email # NOQA
from .services import auth, checkin, export, mail, tickets, cart, orderimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
from django.conf import settings
try:
from .celery_app import app as celery_app # NOQA

View File

@@ -8,7 +8,6 @@ from django.utils.crypto import get_random_string
from formtools.wizard.views import SessionWizardView
from hierarkey.forms import HierarkeyForm
from pretix.base.models import Event
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from .validators import PlaceholderValidator # NOQA
@@ -51,19 +50,33 @@ class I18nInlineFormSet(i18nfield.forms.I18nInlineFormSet):
class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
auto_fields = []
def __init__(self, *args, **kwargs):
from pretix.base.settings import DEFAULTS
self.obj = kwargs.get('obj', None)
self.locales = self.obj.settings.get('locales') if self.obj else kwargs.pop('locales', None)
kwargs['attribute_name'] = 'settings'
kwargs['locales'] = self.locales
kwargs['initial'] = self.obj.settings.freeze()
super().__init__(*args, **kwargs)
for fname in self.auto_fields:
kwargs = DEFAULTS[fname].get('form_kwargs', {})
kwargs.setdefault('required', False)
field = DEFAULTS[fname]['form_class'](
**kwargs
)
if isinstance(field, i18nfield.forms.I18nFormField):
field.widget.enabled_locales = self.locales
self.fields[fname] = field
for k, f in self.fields.items():
if isinstance(f, (RelativeDateTimeField, RelativeDateField)):
f.set_event(self.obj)
def get_new_filename(self, name: str) -> str:
from pretix.base.models import Event
nonce = get_random_string(length=8)
if isinstance(self.obj, Event):
fname = '%s/%s/%s.%s.%s' % (

View File

@@ -6,9 +6,6 @@ from django.utils.functional import lazy
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import OrderPosition
from pretix.multidomain.urlreverse import eventreverse
class DatePickerWidget(forms.DateInput):
def __init__(self, attrs=None, date_format=None):
@@ -71,6 +68,9 @@ class UploadedFileWidget(forms.ClearableFileInput):
@property
def url(self):
from pretix.base.models import OrderPosition
from pretix.multidomain.urlreverse import eventreverse
if isinstance(self.position, OrderPosition):
return eventreverse(self.event, 'presale:event.order.download.answer', kwargs={
'order': self.position.order.code,

View File

@@ -8,6 +8,7 @@ from django import forms
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
BASE_CHOICES = (
('date_from', _('Event start')),
@@ -115,6 +116,8 @@ class RelativeDateWrapper:
base_date_name=parts[3],
time=time
)
if data.base_date_name not in [k[0] for k in BASE_CHOICES]:
raise ValueError('{} is not a valid base date'.format(data.base_date_name))
else:
data = parser.parse(input)
return RelativeDateWrapper(data)
@@ -330,3 +333,39 @@ class ModelRelativeDateTimeField(models.CharField):
defaults = {'form_class': self.form_class}
defaults.update(kwargs)
return super().formfield(**defaults)
class SerializerRelativeDateField(serializers.CharField):
def to_internal_value(self, data):
if data is None:
return None
try:
r = RelativeDateWrapper.from_string(data)
if isinstance(r.data, RelativeDate):
if r.data.time is not None:
raise ValidationError("Do not specify a time for a date field")
return r
except:
raise ValidationError("Invalid relative date")
def to_representation(self, value: RelativeDateWrapper):
if value is None:
return None
return value.to_string()
class SerializerRelativeDateTimeField(serializers.CharField):
def to_internal_value(self, data):
if data is None:
return None
try:
return RelativeDateWrapper.from_string(data)
except:
raise ValidationError("Invalid relative date")
def to_representation(self, value: RelativeDateWrapper):
if value is None:
return None
return value.to_string()

File diff suppressed because it is too large Load Diff

View File

@@ -641,3 +641,27 @@ to define additional columns that can be read during import. You are expected to
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
validate_event_settings = EventPluginSignal(
providing_args=["settings_dict"]
)
"""
This signal is sent out if the user performs an update of event settings through the API or web interface.
You are passed a ``settings_dict`` dictionary with the new state of the event settings object and are expected
to raise a ``django.core.exceptions.ValidationError`` if the new state is not valid.
You can not modify the dictionary. This is only recommended to use if you have multiple settings
that can only be validated together. To validate individual settings, pass a validator to the
serializer field instead.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
api_event_settings_fields = EventPluginSignal(
providing_args=[]
)
"""
This signal is sent out to collect serializable settings fields for the API. You are expected to
return a dictionary mapping names of attributes in the settings store to DRF serializer field instances.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""

View File

@@ -3,19 +3,15 @@ from urllib.parse import urlencode
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import (
MaxValueValidator, MinValueValidator, RegexValidator, validate_email,
)
from django.core.validators import RegexValidator, validate_email
from django.db.models import Q
from django.forms import formset_factory
from django.urls import reverse
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.timezone import get_current_timezone_name
from django.utils.translation import (
pgettext, pgettext_lazy, ugettext_lazy as _,
)
from django_countries import Countries, countries
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_countries import Countries
from django_countries.fields import LazyTypedChoiceField
from i18nfield.forms import (
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
@@ -28,10 +24,12 @@ 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
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.settings import PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS
from pretix.base.settings import (
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_settings,
)
from pretix.control.forms import (
ExtFileField, FontSelect, MultipleLanguagesWidget, SingleLanguageWidget,
SlugWidget, SplitDateTimeField, SplitDateTimePickerWidget,
ExtFileField, FontSelect, MultipleLanguagesWidget, SlugWidget,
SplitDateTimeField, SplitDateTimePickerWidget,
)
from pretix.control.forms.widgets import Select2
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -340,94 +338,10 @@ class EventUpdateForm(I18nModelForm):
class EventSettingsForm(SettingsForm):
show_date_to = forms.BooleanField(
label=_("Show event end date"),
help_text=_("If disabled, only event's start date will be displayed to the public."),
required=False
)
show_times = forms.BooleanField(
label=_("Show dates with time"),
help_text=_("If disabled, the event's start and end date will be displayed without the time of day."),
required=False
)
show_items_outside_presale_period = forms.BooleanField(
label=_("Show items outside presale period"),
help_text=_("Show item details before presale has started and after presale has ended"),
required=False
)
display_net_prices = forms.BooleanField(
label=_("Show net prices instead of gross prices in the product list (not recommended!)"),
help_text=_("Independent of your choice, the cart will show gross prices as this is the price that needs to be "
"paid"),
required=False
)
presale_start_show_date = forms.BooleanField(
label=_("Show start date"),
help_text=_("Show the presale start date before presale has started."),
widget=forms.CheckboxInput,
required=False
)
last_order_modification_date = RelativeDateTimeField(
label=_('Last date of modifications'),
help_text=_("The last date users can modify details of their orders, such as attendee names or "
"answers to questions. If you use the event series feature and an order contains tickets for "
"multiple event dates, the earliest date will be used."),
required=False,
)
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Event timezone"),
)
locales = forms.MultipleChoiceField(
choices=settings.LANGUAGES,
widget=MultipleLanguagesWidget,
label=_("Available languages"),
)
locale = forms.ChoiceField(
choices=settings.LANGUAGES,
widget=SingleLanguageWidget,
label=_("Default language"),
)
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
)
waiting_list_enabled = forms.BooleanField(
label=_("Enable 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
)
waiting_list_hours = forms.IntegerField(
label=_("Waiting list response time"),
min_value=6,
help_text=_("If a ticket voucher is sent to a person on the waiting list, it has to be redeemed within this "
"number of hours until it expires and can be re-assigned to the next person on the list."),
required=False,
widget=forms.NumberInput(),
)
waiting_list_auto = forms.BooleanField(
label=_("Automatic waiting list assignments"),
help_text=_("If ticket capacity becomes free, automatically create a voucher and send it to the first person "
"on the waiting list for that product. If this is not active, mails will not be send automatically "
"but you can send them manually via the control panel. If you disable the waiting list but keep "
"this option enabled, tickets will still be sent out."),
required=False,
widget=forms.CheckboxInput(),
)
attendee_names_asked = forms.BooleanField(
label=_("Ask for attendee names"),
help_text=_("Ask for a name for all tickets which include admission to the event."),
required=False,
)
attendee_names_required = forms.BooleanField(
label=_("Require attendee names"),
help_text=_("Require customers to fill in the names of all attendees."),
required=False,
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_names_asked'}),
)
name_scheme = forms.ChoiceField(
label=_("Name format"),
help_text=_("This defines how pretix will ask for human names. Changing this after you already received "
@@ -440,83 +354,6 @@ class EventSettingsForm(SettingsForm):
"restrict the set of selectable titles."),
required=False,
)
attendee_emails_asked = forms.BooleanField(
label=_("Ask for email addresses per ticket"),
help_text=_("Normally, pretix asks for one email address per order and the order confirmation will be sent "
"only to that email address. If you enable this option, the system will additionally ask for "
"individual email addresses for every admission ticket. This might be useful if you want to "
"obtain individual addresses for every attendee even in case of group orders. However, "
"pretix will send the order confirmation by default only to the one primary email address, not to "
"the per-attendee addresses. You can however enable this in the E-mail settings."),
required=False
)
attendee_emails_required = forms.BooleanField(
label=_("Require email addresses per ticket"),
help_text=_("Require customers to fill in individual e-mail addresses for all admission tickets. See the "
"above option for more details. One email address for the order confirmation will always be "
"required regardless of this setting."),
required=False,
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_emails_asked'}),
)
order_email_asked_twice = forms.BooleanField(
label=_("Ask for the order email address twice"),
help_text=_("Require customers to fill in the primary email address twice to avoid errors."),
required=False,
)
max_items_per_order = forms.IntegerField(
min_value=1,
label=_("Maximum number of items per order"),
help_text=_("Add-on products will not be counted.")
)
reservation_time = forms.IntegerField(
min_value=0,
label=_("Reservation period"),
help_text=_("The number of minutes the items in a user's cart are reserved for this user."),
)
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."),
required=False,
)
confirm_text = I18nFormField(
label=_('Confirmation text'),
help_text=_('This text needs to be confirmed by the user before a purchase is possible. You could for example '
'link your terms of service here. If you use the Pages feature to publish your terms of service, '
'you don\'t need this setting since you can configure it there.'),
required=False,
widget=I18nTextarea
)
contact_mail = forms.EmailField(
label=_("Contact address"),
required=False,
help_text=_("We'll show this publicly to allow attendees to contact you.")
)
show_variations_expanded = forms.BooleanField(
label=_("Show variations of a product expanded by default"),
required=False
)
hide_sold_out = forms.BooleanField(
label=_("Hide all products that are sold out"),
required=False
)
meta_noindex = forms.BooleanField(
label=_('Ask search engines not to index the ticket shop'),
required=False
)
redirect_to_checkout_directly = forms.BooleanField(
label=_('Directly redirect to check-out after a product has been added to the cart.'),
required=False
)
frontpage_subevent_ordering = forms.ChoiceField(
label=pgettext('subevent', 'Date ordering'),
choices=[
('date_ascending', _('Event start time')),
('date_descending', _('Event start time (descending)')),
('name_ascending', _('Name')),
('name_descending', _('Name (descending)')),
], # When adding a new ordering, remember to also define it in the event model
)
logo_image = ExtFileField(
label=_('Logo image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
@@ -533,33 +370,6 @@ class EventSettingsForm(SettingsForm):
'WhatsApp and Reddit only show a square preview, so we recommend to make sure it still looks good '
'only the center square is shown. If you do not fill this, we will use the logo given above.')
)
frontpage_text = I18nFormField(
label=_("Frontpage text"),
required=False,
widget=I18nTextarea
)
checkout_email_helptext = I18nFormField(
label=_("Help text of the email field"),
required=False,
widget_kwargs={'attrs': {'rows': '2'}},
widget=I18nTextarea
)
presale_has_ended_text = I18nFormField(
label=_("End of presale text"),
required=False,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above the ticket shop once the designated sales timeframe for this event "
"is over. You can use it to describe other options to get a ticket, such as a box office.")
)
voucher_explanation_text = I18nFormField(
label=_("Voucher explanation"),
required=False,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown next to the input for a voucher code. You can use it e.g. to explain "
"how to obtain a voucher code.")
)
primary_color = forms.CharField(
label=_("Primary color"),
required=False,
@@ -598,24 +408,49 @@ class EventSettingsForm(SettingsForm):
help_text=_('Only respected by modern browsers.')
)
auto_fields = [
'imprint_url',
'checkout_email_helptext',
'presale_has_ended_text',
'voucher_explanation_text',
'show_date_to',
'show_times',
'show_items_outside_presale_period',
'display_net_prices',
'presale_start_show_date',
'locales',
'locale',
'show_quota_left',
'waiting_list_enabled',
'waiting_list_hours',
'waiting_list_auto',
'max_items_per_order',
'reservation_time',
'contact_mail',
'show_variations_expanded',
'hide_sold_out',
'meta_noindex',
'redirect_to_checkout_directly',
'frontpage_subevent_ordering',
'frontpage_text',
'attendee_names_asked',
'attendee_names_required',
'attendee_emails_asked',
'attendee_emails_required',
'confirm_text',
'order_email_asked_twice',
'last_order_modification_date',
]
def clean(self):
data = super().clean()
if 'locales' in data and data['locale'] not in data['locales']:
raise ValidationError({
'locale': _('Your default locale must also be enabled for your event (see box above).')
})
if data['attendee_names_required'] and not data['attendee_names_asked']:
raise ValidationError({
'attendee_names_required': _('You cannot require specifying attendee names if you do not ask for them.')
})
if data['attendee_emails_required'] and not data['attendee_emails_asked']:
raise ValidationError({
'attendee_emails_required': _('You have to ask for attendee emails if you want to make them required.')
})
settings_dict = self.event.settings.freeze()
settings_dict.update(data)
validate_settings(self.event, data)
return data
def __init__(self, *args, **kwargs):
event = kwargs['obj']
self.event = kwargs['obj']
super().__init__(*args, **kwargs)
self.fields['confirm_text'].widget.attrs['rows'] = '3'
self.fields['confirm_text'].widget.attrs['placeholder'] = _(
@@ -636,7 +471,7 @@ class EventSettingsForm(SettingsForm):
))
for k, v in PERSON_NAME_TITLE_GROUPS.items()
]
if not event.has_subevents:
if not self.event.has_subevents:
del self.fields['frontpage_subevent_ordering']
self.fields['primary_font'].choices += [
(a, {"title": a, "data": v}) for a, v in get_fonts().items()
@@ -644,77 +479,26 @@ class EventSettingsForm(SettingsForm):
class CancelSettingsForm(SettingsForm):
cancel_allow_user = forms.BooleanField(
label=_("Customers can cancel their unpaid orders"),
required=False
)
cancel_allow_user_until = RelativeDateTimeField(
label=_("Do not allow cancellations after"),
required=False
)
cancel_allow_user_paid = forms.BooleanField(
label=_("Customers can cancel their paid orders"),
help_text=_("Paid money will be automatically paid back if the payment method allows it. "
"Otherwise, a manual refund will be created for you to process manually."),
required=False
)
cancel_allow_user_paid_keep = forms.DecimalField(
label=_("Keep a fixed cancellation fee"),
required=False
)
cancel_allow_user_paid_keep_fees = forms.BooleanField(
label=_("Keep payment, shipping and service fees"),
required=False
)
cancel_allow_user_paid_keep_percentage = forms.DecimalField(
label=_("Keep a percentual cancellation fee"),
required=False
)
cancel_allow_user_paid_until = RelativeDateTimeField(
label=_("Do not allow cancellations after"),
required=False
)
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',
]
class PaymentSettingsForm(SettingsForm):
payment_term_days = forms.IntegerField(
label=_('Payment term in days'),
help_text=_("The number of days after placing an order the user has to pay to preserve their reservation. If "
"you use slow payment methods like bank transfer, we recommend 14 days. If you only use real-time "
"payment methods, we recommend still setting two or three days to allow people to retry failed "
"payments."),
validators=[MinValueValidator(0),
MaxValueValidator(1000000)]
)
payment_term_last = RelativeDateField(
label=_('Last date of payments'),
help_text=_("The last date any payments are accepted. This has precedence over the number of "
"days configured above. If you use the event series feature and an order contains tickets for "
"multiple dates, the earliest date will be used."),
required=False,
)
payment_term_weekdays = forms.BooleanField(
label=_('Only end payment terms on weekdays'),
help_text=_("If this is activated and the payment term of any order ends on a Saturday or Sunday, it will be "
"moved to the next Monday instead. This is required in some countries by civil law. This will "
"not effect the last date of payments configured above."),
required=False,
)
payment_term_expire_automatically = forms.BooleanField(
label=_('Automatically expire unpaid orders'),
help_text=_("If checked, all unpaid orders will automatically go from 'pending' to 'expired' "
"after the end of their payment deadline. This means that those tickets go back to "
"the pool and can be ordered by other people."),
required=False
)
payment_term_accept_late = forms.BooleanField(
label=_('Accept late payments'),
help_text=_("Accept payments for orders even when they are in 'expired' state as long as enough "
"capacity is available. No payments will ever be accepted after the 'Last date of payments' "
"configured above."),
required=False
)
auto_fields = [
'payment_term_days',
'payment_term_last',
'payment_term_weekdays',
'payment_term_expire_automatically',
'payment_term_accept_late',
'payment_explanation',
]
tax_rate_default = forms.ModelChoiceField(
queryset=TaxRule.objects.none(),
label=_('Tax rule for payment fees'),
@@ -722,27 +506,13 @@ class PaymentSettingsForm(SettingsForm):
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.")
)
payment_explanation = I18nFormField(
widget=I18nTextarea,
widget_kwargs={'attrs': {
'rows': 3,
}},
required=False,
label=_("Guidance text"),
help_text=_("This text will be shown above the payment options. You can explain the choices to the user here, "
"if you want.")
)
def clean(self):
cleaned_data = super().clean()
payment_term_last = cleaned_data.get('payment_term_last')
if payment_term_last and self.obj.presale_end:
if payment_term_last.date(self.obj) < self.obj.presale_end.date():
self.add_error(
'payment_term_last',
_('The last payment date cannot be before the end of presale.'),
)
return cleaned_data
data = super().clean()
settings_dict = self.obj.settings.freeze()
settings_dict.update(data)
validate_settings(self.obj, data)
return data
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -790,90 +560,37 @@ class ProviderForm(SettingsForm):
class InvoiceSettingsForm(SettingsForm):
allcountries = list(countries)
allcountries.insert(0, ('', _('Select country')))
invoice_address_asked = forms.BooleanField(
label=_("Ask for invoice address"),
required=False
)
invoice_address_required = forms.BooleanField(
label=_("Require invoice address"),
required=False,
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}),
)
invoice_address_company_required = forms.BooleanField(
label=_("Require a business addresses"),
help_text=_('This will require users to enter a company name.'),
required=False,
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_required'}),
)
invoice_name_required = forms.BooleanField(
label=_("Require customer name"),
required=False,
)
invoice_address_vatid = forms.BooleanField(
label=_("Ask for VAT ID"),
help_text=_("Does only work if an invoice address is asked for. VAT ID is not required."),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}),
required=False
)
invoice_address_beneficiary = forms.BooleanField(
label=_("Ask for beneficiary"),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}),
required=False
)
invoice_address_not_asked_free = forms.BooleanField(
label=_('Do not ask for invoice address if an order is free'),
required=False
)
invoice_include_free = forms.BooleanField(
label=_("Show free products on invoices"),
help_text=_("Note that invoices will never be generated for orders that contain only free "
"products."),
required=False
)
invoice_address_explanation_text = I18nFormField(
label=_("Invoice address explanation"),
required=False,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above the invoice address form during checkout.")
)
invoice_numbers_consecutive = forms.BooleanField(
label=_("Generate invoices with consecutive numbers"),
help_text=_("If deactivated, the order code will be used in the invoice number."),
required=False
)
invoice_numbers_prefix = forms.CharField(
label=_("Invoice number prefix"),
help_text=_("This will be prepended to invoice numbers. If you leave this field empty, your event slug will "
"be used followed by a dash. Attention: If multiple events within the same organization use the "
"same value in this field, they will share their number range, i.e. every full number will be "
"used at most once over all of your events. This setting only affects future invoices. You can "
"use %Y (with century) %y (without century) to insert the year of the invoice, or %m and %d for "
"the day of month."),
required=False,
)
invoice_numbers_prefix_cancellations = forms.CharField(
label=_("Invoice number prefix for cancellations"),
help_text=_("This will be prepended to invoice numbers of cancellations. If you leave this field empty, "
"the same numbering scheme will be used that you configured for regular invoices."),
required=False,
)
invoice_generate = forms.ChoiceField(
label=_("Generate invoices"),
required=False,
widget=forms.RadioSelect,
choices=(
('False', _('Do not generate invoices')),
('admin', _('Only manually in admin panel')),
('user', _('Automatically on user request')),
('True', _('Automatically for all created orders')),
('paid', _('Automatically on payment')),
),
help_text=_("Invoices will never be automatically generated for free orders.")
)
auto_fields = [
'invoice_address_asked',
'invoice_address_required',
'invoice_address_vatid',
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_name_required',
'invoice_address_not_asked_free',
'invoice_include_free',
'invoice_generate',
'invoice_attendee_name',
'invoice_include_expire_date',
'invoice_numbers_consecutive',
'invoice_numbers_prefix',
'invoice_numbers_prefix_cancellations',
'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_generate_sales_channels = forms.MultipleChoiceField(
label=_('Generate invoices for Sales channels'),
choices=[],
@@ -881,105 +598,11 @@ class InvoiceSettingsForm(SettingsForm):
help_text=_("If you have enabled invoice generation in the previous setting, you can limit it here to specific "
"sales channels.")
)
invoice_attendee_name = forms.BooleanField(
label=_("Show attendee names on invoices"),
required=False
)
invoice_include_expire_date = forms.BooleanField(
label=_("Show expiration date of order"),
help_text=_("The expiration date will not be shown if the invoice is generated after the order is paid."),
required=False
)
invoice_email_attachment = forms.BooleanField(
label=_("Attach invoices to emails"),
help_text=_("If invoices are automatically generated for all orders, they will be attached to the order "
"confirmation mail. If they are automatically generated on payment, they will be attached to the "
"payment confirmation mail. If they are not automatically generated, they will not be attached "
"to emails."),
required=False
)
invoice_renderer = forms.ChoiceField(
label=_("Invoice style"),
required=True,
choices=[]
)
invoice_address_from_name = forms.CharField(
label=_("Company name"),
required=False,
)
invoice_address_from = forms.CharField(
label=_("Address line"),
widget=forms.Textarea(attrs={
'rows': 2,
'placeholder': _(
'Albert Einstein Road 52'
)
}),
required=False,
)
invoice_address_from_zipcode = forms.CharField(
widget=forms.TextInput(attrs={
'placeholder': '12345'
}),
required=False,
label=_("ZIP code"),
)
invoice_address_from_city = forms.CharField(
widget=forms.TextInput(attrs={
'placeholder': _('Random City')
}),
required=False,
label=_("City"),
)
invoice_address_from_country = forms.ChoiceField(
choices=allcountries,
required=False,
label=_("Country"),
)
invoice_address_from_tax_id = forms.CharField(
required=False,
label=_("Domestic tax ID"),
)
invoice_address_from_vat_id = forms.CharField(
required=False,
label=_("EU VAT ID"),
)
invoice_introductory_text = I18nFormField(
widget=I18nTextarea,
widget_kwargs={'attrs': {
'rows': 3,
'placeholder': _(
'e.g. With this document, we sent you the invoice for your ticket order.'
)
}},
required=False,
label=_("Introductory text"),
help_text=_("Will be printed on every invoice above the invoice rows.")
)
invoice_additional_text = I18nFormField(
widget=I18nTextarea,
widget_kwargs={'attrs': {
'rows': 3,
'placeholder': _(
'e.g. Thank you for your purchase! You can find more information on the event at ...'
)
}},
required=False,
label=_("Additional text"),
help_text=_("Will be printed on every invoice below the invoice total.")
)
invoice_footer_text = I18nFormField(
widget=I18nTextarea,
widget_kwargs={'attrs': {
'rows': 5,
'placeholder': _(
'e.g. your bank details, legal details like your VAT ID, registration numbers, etc.'
)
}},
required=False,
label=_("Footer"),
help_text=_("Will be printed centered and in a smaller font at the end of every invoice page.")
)
invoice_language = forms.ChoiceField(
widget=forms.Select, required=True,
label=_("Invoice language"),
@@ -1009,6 +632,13 @@ class InvoiceSettingsForm(SettingsForm):
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
)
def clean(self):
data = super().clean()
settings_dict = self.obj.settings.freeze()
settings_dict.update(data)
validate_settings(self.obj, data)
return data
def multimail_validate(val):
s = val.split(',')
@@ -1018,22 +648,13 @@ def multimail_validate(val):
class MailSettingsForm(SettingsForm):
mail_prefix = forms.CharField(
label=_("Subject prefix"),
help_text=_("This will be prepended to the subject of all outgoing emails, formatted as [prefix]. "
"Choose, for example, a short form of your event name."),
required=False
)
mail_from = forms.EmailField(
label=_("Sender address"),
help_text=_("Sender address for outgoing emails"),
)
mail_from_name = forms.CharField(
label=_("Sender name"),
help_text=_("Sender name used in conjunction with the sender address for outgoing emails. "
"Defaults to your event name."),
required=False
)
auto_fields = [
'mail_prefix',
'mail_from',
'mail_from_name',
'mail_attach_ical',
]
mail_bcc = forms.CharField(
label=_("Bcc address"),
help_text=_("All emails will be sent to this address as a Bcc copy"),
@@ -1041,12 +662,6 @@ class MailSettingsForm(SettingsForm):
required=False,
max_length=255
)
mail_attach_ical = forms.BooleanField(
label=_("Attach calendar files"),
help_text=_("If enabled, we will attach an .ics calendar file to order confirmation emails."),
required=False
)
mail_text_signature = I18nFormField(
label=_("Signature"),
required=False,
@@ -1065,7 +680,6 @@ class MailSettingsForm(SettingsForm):
required=True,
choices=[]
)
mail_text_order_placed = I18nFormField(
label=_("Text sent to order contact address"),
required=False,
@@ -1301,30 +915,13 @@ class MailSettingsForm(SettingsForm):
class TicketSettingsForm(SettingsForm):
ticket_download = forms.BooleanField(
label=_("Use feature"),
help_text=_("Use pretix to generate tickets for the user to download and print out."),
required=False
)
ticket_download_date = RelativeDateTimeField(
label=_("Download date"),
help_text=_("Ticket download will be offered after this date. If you use the event series feature and an order "
"contains tickets for multiple event dates, download of all tickets will be available if at least "
"one of the event dates allows it."),
required=False,
)
ticket_download_addons = forms.BooleanField(
label=_("Offer to download tickets separately for add-on products"),
required=False,
)
ticket_download_nonadm = forms.BooleanField(
label=_("Generate tickets for non-admission products"),
required=False,
)
ticket_download_pending = forms.BooleanField(
label=_("Offer to download tickets even before an order is paid"),
required=False,
)
auto_fields = [
'ticket_download',
'ticket_download_date',
'ticket_download_addons',
'ticket_download_nonadm',
'ticket_download_pending',
]
def prepare_fields(self):
# See clean()