mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Customer accounts & Memberships (#2024)
This commit is contained in:
@@ -45,7 +45,7 @@ from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ...base.forms import I18nModelForm
|
||||
from ...base.forms import I18nModelForm, SecretKeySettingsField
|
||||
|
||||
# Import for backwards compatibility with okd import paths
|
||||
from ...base.forms.widgets import ( # noqa
|
||||
@@ -368,3 +368,46 @@ class SplitDateTimeField(forms.SplitDateTimeField):
|
||||
|
||||
class FontSelect(forms.RadioSelect):
|
||||
option_template_name = 'pretixcontrol/font_option.html'
|
||||
|
||||
|
||||
class SMTPSettingsMixin(forms.Form):
|
||||
smtp_use_custom = forms.BooleanField(
|
||||
label=_("Use custom SMTP server"),
|
||||
help_text=_("All mail related to your event will be sent over the smtp server specified by you."),
|
||||
required=False
|
||||
)
|
||||
smtp_host = forms.CharField(
|
||||
label=_("Hostname"),
|
||||
required=False,
|
||||
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, ...'})
|
||||
)
|
||||
smtp_username = forms.CharField(
|
||||
label=_("Username"),
|
||||
widget=forms.TextInput(attrs={'placeholder': 'myuser@example.org'}),
|
||||
required=False
|
||||
)
|
||||
smtp_password = SecretKeySettingsField(
|
||||
label=_("Password"),
|
||||
required=False,
|
||||
)
|
||||
smtp_use_tls = forms.BooleanField(
|
||||
label=_("Use STARTTLS"),
|
||||
help_text=_("Commonly enabled on port 587."),
|
||||
required=False
|
||||
)
|
||||
smtp_use_ssl = forms.BooleanField(
|
||||
label=_("Use SSL"),
|
||||
help_text=_("Commonly enabled on port 465."),
|
||||
required=False
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
if data.get('smtp_use_tls') and data.get('smtp_use_ssl'):
|
||||
raise ValidationError(_('You can activate either SSL or STARTTLS security, but not both at the same time.'))
|
||||
return data
|
||||
|
||||
@@ -63,7 +63,7 @@ from pretix.base.settings import (
|
||||
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_event_settings,
|
||||
)
|
||||
from pretix.control.forms import (
|
||||
MultipleLanguagesWidget, SlugWidget, SplitDateTimeField,
|
||||
MultipleLanguagesWidget, SlugWidget, SMTPSettingsMixin, SplitDateTimeField,
|
||||
SplitDateTimePickerWidget,
|
||||
)
|
||||
from pretix.control.forms.widgets import Select2
|
||||
@@ -825,7 +825,7 @@ def contains_web_channel_validate(val):
|
||||
raise ValidationError(_("The online shop must be selected to receive these emails."))
|
||||
|
||||
|
||||
class MailSettingsForm(SettingsForm):
|
||||
class MailSettingsForm(SMTPSettingsMixin, SettingsForm):
|
||||
auto_fields = [
|
||||
'mail_prefix',
|
||||
'mail_from',
|
||||
@@ -1020,43 +1020,6 @@ class MailSettingsForm(SettingsForm):
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
)
|
||||
smtp_use_custom = forms.BooleanField(
|
||||
label=_("Use custom SMTP server"),
|
||||
help_text=_("All mail related to your event will be sent over the smtp server specified by you."),
|
||||
required=False
|
||||
)
|
||||
smtp_host = forms.CharField(
|
||||
label=_("Hostname"),
|
||||
required=False,
|
||||
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, ...'})
|
||||
)
|
||||
smtp_username = forms.CharField(
|
||||
label=_("Username"),
|
||||
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
|
||||
}),
|
||||
)
|
||||
smtp_use_tls = forms.BooleanField(
|
||||
label=_("Use STARTTLS"),
|
||||
help_text=_("Commonly enabled on port 587."),
|
||||
required=False
|
||||
)
|
||||
smtp_use_ssl = forms.BooleanField(
|
||||
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'],
|
||||
@@ -1110,17 +1073,6 @@ class MailSettingsForm(SettingsForm):
|
||||
# the user interface with it
|
||||
del self.fields[k]
|
||||
|
||||
def clean(self):
|
||||
data = self.cleaned_data
|
||||
if not data.get('smtp_password') and data.get('smtp_username'):
|
||||
# Leave password unchanged if the username is set and the password field is empty.
|
||||
# This makes it impossible to set an empty password as long as a username is set, but
|
||||
# Python's smtplib does not support password-less schemes anyway.
|
||||
data['smtp_password'] = self.initial.get('smtp_password')
|
||||
|
||||
if data.get('smtp_use_tls') and data.get('smtp_use_ssl'):
|
||||
raise ValidationError(_('You can activate either SSL or STARTTLS security, but not both at the same time.'))
|
||||
|
||||
|
||||
class TicketSettingsForm(SettingsForm):
|
||||
auto_fields = [
|
||||
|
||||
@@ -1022,6 +1022,44 @@ class GiftCardFilterForm(FilterForm):
|
||||
return qs.distinct()
|
||||
|
||||
|
||||
class CustomerFilterForm(FilterForm):
|
||||
orders = {
|
||||
'email': 'email',
|
||||
'identifier': 'identifier',
|
||||
'name_cached': 'name_cached',
|
||||
}
|
||||
query = forms.CharField(
|
||||
label=_('Search query'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Search query'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.pop('request')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
|
||||
if fdata.get('query'):
|
||||
query = fdata.get('query')
|
||||
qs = qs.filter(
|
||||
Q(email__icontains=query)
|
||||
| Q(name_cached__icontains=query)
|
||||
| Q(identifier__istartswith=query)
|
||||
)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(self.get_order_by())
|
||||
else:
|
||||
qs = qs.order_by('-email')
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class TeamFilterForm(FilterForm):
|
||||
orders = {
|
||||
'name': 'name',
|
||||
|
||||
@@ -368,6 +368,11 @@ class ItemCreateForm(I18nModelForm):
|
||||
'hidden_if_available',
|
||||
'require_bundling',
|
||||
'checkin_attention',
|
||||
'require_membership',
|
||||
'grant_membership_type',
|
||||
'grant_membership_duration_like_event',
|
||||
'grant_membership_duration_days',
|
||||
'grant_membership_duration_months',
|
||||
)
|
||||
for f in fields:
|
||||
setattr(self.instance, f, getattr(self.cleaned_data['copy_from'], f))
|
||||
@@ -399,6 +404,10 @@ class ItemCreateForm(I18nModelForm):
|
||||
'items': [self.instance.pk]
|
||||
})
|
||||
|
||||
if self.cleaned_data.get('copy_from'):
|
||||
self.instance.require_membership_types.set(
|
||||
self.cleaned_data['copy_from'].require_membership_types.all()
|
||||
)
|
||||
if self.cleaned_data.get('has_variations'):
|
||||
if self.cleaned_data.get('copy_from') and self.cleaned_data.get('copy_from').has_variations:
|
||||
for variation in self.cleaned_data['copy_from'].variations.all():
|
||||
@@ -523,6 +532,19 @@ class ItemUpdateForm(I18nModelForm):
|
||||
)
|
||||
self.fields['category'].widget.choices = self.fields['category'].choices
|
||||
|
||||
qs = self.event.organizer.membership_types.all()
|
||||
if qs:
|
||||
self.fields['require_membership_types'].queryset = qs
|
||||
self.fields['grant_membership_type'].queryset = qs
|
||||
self.fields['grant_membership_type'].empty_label = _('No membership granted')
|
||||
else:
|
||||
del self.fields['require_membership']
|
||||
del self.fields['require_membership_types']
|
||||
del self.fields['grant_membership_type']
|
||||
del self.fields['grant_membership_duration_like_event']
|
||||
del self.fields['grant_membership_duration_days']
|
||||
del self.fields['grant_membership_duration_months']
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
if d['issue_giftcard']:
|
||||
@@ -571,15 +593,26 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'show_quota_left',
|
||||
'hidden_if_available',
|
||||
'issue_giftcard',
|
||||
'require_membership',
|
||||
'require_membership_types',
|
||||
'grant_membership_type',
|
||||
'grant_membership_duration_like_event',
|
||||
'grant_membership_duration_days',
|
||||
'grant_membership_duration_months',
|
||||
]
|
||||
field_classes = {
|
||||
'available_from': SplitDateTimeField,
|
||||
'available_until': SplitDateTimeField,
|
||||
'hidden_if_available': SafeModelChoiceField,
|
||||
'grant_membership_type': SafeModelChoiceField,
|
||||
'require_membership_types': SafeModelMultipleChoiceField,
|
||||
}
|
||||
widgets = {
|
||||
'available_from': SplitDateTimePickerWidget(),
|
||||
'available_until': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_available_from_0'}),
|
||||
'require_membership_types': forms.CheckboxSelectMultiple(attrs={
|
||||
'class': 'scrolling-multiple-choice'
|
||||
}),
|
||||
'generate_tickets': TicketNullBooleanSelect(),
|
||||
'show_quota_left': ShowQuotaNullBooleanSelect()
|
||||
}
|
||||
@@ -632,6 +665,13 @@ class ItemVariationForm(I18nModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
change_decimal_field(self.fields['default_price'], self.event.currency)
|
||||
|
||||
qs = self.event.organizer.membership_types.all()
|
||||
if qs:
|
||||
self.fields['require_membership_types'].queryset = qs
|
||||
else:
|
||||
del self.fields['require_membership']
|
||||
del self.fields['require_membership_types']
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
localized_fields = '__all__'
|
||||
@@ -641,7 +681,14 @@ class ItemVariationForm(I18nModelForm):
|
||||
'default_price',
|
||||
'original_price',
|
||||
'description',
|
||||
'require_membership',
|
||||
'require_membership_types'
|
||||
]
|
||||
widgets = {
|
||||
'require_membership_types': forms.CheckboxSelectMultiple(attrs={
|
||||
'class': 'scrolling-multiple-choice'
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
class ItemAddOnsFormSet(I18nFormSet):
|
||||
|
||||
@@ -46,6 +46,7 @@ from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import (
|
||||
gettext_lazy as _, gettext_noop, pgettext_lazy,
|
||||
)
|
||||
from django_scopes.forms import SafeModelChoiceField
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
@@ -290,6 +291,10 @@ class OrderPositionAddForm(forms.Form):
|
||||
widget=forms.TextInput(attrs={'placeholder': _('General admission'), 'data-seat-guid-field': 'true'}),
|
||||
label=_('Seat')
|
||||
)
|
||||
used_membership = forms.ChoiceField(
|
||||
label=_('Membership'),
|
||||
required=False,
|
||||
)
|
||||
price = forms.DecimalField(
|
||||
required=False,
|
||||
max_digits=10, decimal_places=2,
|
||||
@@ -360,6 +365,23 @@ class OrderPositionAddForm(forms.Form):
|
||||
del self.fields['subevent']
|
||||
change_decimal_field(self.fields['price'], order.event.currency)
|
||||
|
||||
choices = [
|
||||
('', ''),
|
||||
]
|
||||
if order.customer:
|
||||
self.memberships = list(order.customer.memberships.all())
|
||||
for m in self.memberships:
|
||||
choices.append((str(m.pk), str(m)))
|
||||
self.fields['used_membership'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
if d['used_membership']:
|
||||
d['used_membership'] = [m for m in self.memberships if str(m.pk) == d['used_membership']][0]
|
||||
else:
|
||||
d['used_membership'] = None
|
||||
return d
|
||||
|
||||
|
||||
class OrderPositionAddFormset(forms.BaseFormSet):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -405,6 +427,9 @@ class OrderPositionChangeForm(forms.Form):
|
||||
localize=True,
|
||||
label=_('New price (gross)')
|
||||
)
|
||||
used_membership = forms.ChoiceField(
|
||||
required=False,
|
||||
)
|
||||
tax_rule = forms.ModelChoiceField(
|
||||
TaxRule.objects.none(),
|
||||
required=False,
|
||||
@@ -478,6 +503,24 @@ class OrderPositionChangeForm(forms.Form):
|
||||
self.fields['itemvar'].choices = choices
|
||||
change_decimal_field(self.fields['price'], instance.order.event.currency)
|
||||
|
||||
choices = [
|
||||
('', _('(Unchanged)')),
|
||||
('CLEAR', _('(No membership)')),
|
||||
]
|
||||
if instance.order.customer:
|
||||
self.memberships = list(instance.order.customer.memberships.all())
|
||||
for m in self.memberships:
|
||||
choices.append((str(m.pk), str(m)))
|
||||
self.fields['used_membership'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
if d['used_membership'] and d['used_membership'] != 'CLEAR':
|
||||
d['used_membership'] = [m for m in self.memberships if str(m.pk) == d['used_membership']][0]
|
||||
elif not d['used_membership']:
|
||||
d['used_membership'] = None
|
||||
return d
|
||||
|
||||
|
||||
class OrderFeeChangeForm(forms.Form):
|
||||
value = forms.DecimalField(
|
||||
@@ -516,16 +559,36 @@ class OrderContactForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ['email', 'email_known_to_work', 'phone']
|
||||
fields = ['customer', 'email', 'email_known_to_work', 'phone']
|
||||
widgets = {
|
||||
'phone': WrappedPhoneNumberPrefixWidget()
|
||||
'phone': WrappedPhoneNumberPrefixWidget(),
|
||||
}
|
||||
field_classes = {
|
||||
'customer': SafeModelChoiceField,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
customers = kwargs.pop('customers')
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.instance.event.settings.order_phone_asked and not self.instance.phone:
|
||||
del self.fields['phone']
|
||||
|
||||
if customers:
|
||||
self.fields['customer'].queryset = self.instance.event.organizer.customers.all()
|
||||
self.fields['customer'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'generic',
|
||||
'data-select2-url': reverse('control:organizer.customers.select2', kwargs={
|
||||
'organizer': self.instance.event.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': _('Customer')
|
||||
}
|
||||
)
|
||||
self.fields['customer'].widget.choices = self.fields['customer'].choices
|
||||
self.fields['customer'].required = False
|
||||
else:
|
||||
del self.fields['customer']
|
||||
|
||||
|
||||
class OrderLocaleForm(forms.ModelForm):
|
||||
locale = forms.ChoiceField()
|
||||
|
||||
@@ -39,20 +39,31 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
from pytz import common_timezones
|
||||
|
||||
from pretix.api.models import WebHook
|
||||
from pretix.api.webhooks import get_all_webhook_events
|
||||
from pretix.base.forms import I18nModelForm, SettingsForm
|
||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
|
||||
from pretix.base.forms.questions import NamePartsFormField
|
||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||
from pretix.base.models import (
|
||||
Device, EventMetaProperty, Gate, GiftCard, Organizer, Team,
|
||||
Customer, Device, EventMetaProperty, Gate, GiftCard, Membership,
|
||||
MembershipType, Organizer, Team,
|
||||
)
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS
|
||||
from pretix.control.forms import (
|
||||
ExtFileField, SMTPSettingsMixin, SplitDateTimeField,
|
||||
)
|
||||
from pretix.control.forms.event import (
|
||||
SafeEventMultipleChoiceField, multimail_validate,
|
||||
)
|
||||
from pretix.control.forms import ExtFileField, SplitDateTimeField
|
||||
from pretix.control.forms.event import SafeEventMultipleChoiceField
|
||||
from pretix.multidomain.models import KnownDomain
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
|
||||
class OrganizerForm(I18nModelForm):
|
||||
@@ -168,6 +179,12 @@ class EventMetaPropertyForm(forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class MembershipTypeForm(I18nModelForm):
|
||||
class Meta:
|
||||
model = MembershipType
|
||||
fields = ['name', 'transferable', 'allow_parallel_usage', 'max_usages']
|
||||
|
||||
|
||||
class TeamForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -181,7 +198,7 @@ class TeamForm(forms.ModelForm):
|
||||
model = Team
|
||||
fields = ['name', 'all_events', 'limit_events', 'can_create_events',
|
||||
'can_change_teams', 'can_change_organizer_settings',
|
||||
'can_manage_gift_cards',
|
||||
'can_manage_gift_cards', 'can_manage_customers',
|
||||
'can_change_event_settings', 'can_change_items',
|
||||
'can_view_orders', 'can_change_orders', 'can_checkin_orders',
|
||||
'can_view_vouchers', 'can_change_vouchers']
|
||||
@@ -250,7 +267,24 @@ class DeviceForm(forms.ModelForm):
|
||||
|
||||
|
||||
class OrganizerSettingsForm(SettingsForm):
|
||||
timezone = forms.ChoiceField(
|
||||
choices=((a, a) for a in common_timezones),
|
||||
label=_("Default timezone"),
|
||||
)
|
||||
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."),
|
||||
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."),
|
||||
required=False,
|
||||
)
|
||||
auto_fields = [
|
||||
'customer_accounts',
|
||||
'contact_mail',
|
||||
'imprint_url',
|
||||
'organizer_info_text',
|
||||
@@ -292,6 +326,115 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
'We recommend a size of at least 200x200px to accommodate most devices.')
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
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'])
|
||||
))
|
||||
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])
|
||||
))
|
||||
for k, v in PERSON_NAME_TITLE_GROUPS.items()
|
||||
]
|
||||
|
||||
|
||||
class MailSettingsForm(SMTPSettingsMixin, SettingsForm):
|
||||
auto_fields = [
|
||||
'mail_from',
|
||||
'mail_from_name',
|
||||
]
|
||||
|
||||
mail_bcc = forms.CharField(
|
||||
label=_("Bcc address"),
|
||||
help_text=_("All emails will be sent to this address as a Bcc copy"),
|
||||
validators=[multimail_validate],
|
||||
required=False,
|
||||
max_length=255
|
||||
)
|
||||
mail_text_signature = I18nFormField(
|
||||
label=_("Signature"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("This will be attached to every email."),
|
||||
validators=[PlaceholderValidator([])],
|
||||
widget_kwargs={'attrs': {
|
||||
'rows': '4',
|
||||
'placeholder': _(
|
||||
'e.g. your contact details'
|
||||
)
|
||||
}}
|
||||
)
|
||||
|
||||
mail_text_customer_registration = I18nFormField(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
)
|
||||
mail_text_customer_email_change = I18nFormField(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
)
|
||||
mail_text_customer_reset = I18nFormField(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
)
|
||||
|
||||
base_context = {
|
||||
'mail_text_customer_registration': ['customer', 'url'],
|
||||
'mail_text_customer_email_change': ['customer', 'url'],
|
||||
'mail_text_customer_reset': ['customer', 'url'],
|
||||
}
|
||||
|
||||
def _get_sample_context(self, base_parameters):
|
||||
placeholders = {
|
||||
'organizer': self.organizer.name
|
||||
}
|
||||
|
||||
if 'url' in base_parameters:
|
||||
placeholders['url'] = build_absolute_uri(
|
||||
self.organizer,
|
||||
'presale:organizer.customer.activate'
|
||||
) + '?token=' + get_random_string(30)
|
||||
|
||||
if 'customer' in base_parameters:
|
||||
placeholders['name'] = pgettext_lazy('person_name_sample', 'John Doe')
|
||||
name_scheme = PERSON_NAME_SCHEMES[self.organizer.settings.name_scheme]
|
||||
for f, l, w in name_scheme['fields']:
|
||||
if f == 'full_name':
|
||||
continue
|
||||
placeholders['name_%s' % f] = name_scheme['sample'][f]
|
||||
return placeholders
|
||||
|
||||
def _set_field_placeholders(self, fn, base_parameters):
|
||||
phs = [
|
||||
'{%s}' % p
|
||||
for p in sorted(self._get_sample_context(base_parameters).keys())
|
||||
]
|
||||
ht = _('Available placeholders: {list}').format(
|
||||
list=', '.join(phs)
|
||||
)
|
||||
if self.fields[fn].help_text:
|
||||
self.fields[fn].help_text += ' ' + str(ht)
|
||||
else:
|
||||
self.fields[fn].help_text = ht
|
||||
self.fields[fn].validators.append(
|
||||
PlaceholderValidator(phs)
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.organizer = kwargs.get('obj')
|
||||
super().__init__(*args, **kwargs)
|
||||
for k, v in self.base_context.items():
|
||||
self._set_field_placeholders(k, v)
|
||||
|
||||
|
||||
class WebHookForm(forms.ModelForm):
|
||||
events = forms.MultipleChoiceField(
|
||||
@@ -373,3 +516,67 @@ class GiftCardUpdateForm(forms.ModelForm):
|
||||
'expires': SplitDateTimePickerWidget,
|
||||
'conditions': forms.Textarea(attrs={"rows": 2})
|
||||
}
|
||||
|
||||
|
||||
class CustomerUpdateForm(forms.ModelForm):
|
||||
error_messages = {
|
||||
'duplicate': _("An account with this email address is already registered."),
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = Customer
|
||||
fields = ['is_active', 'name_parts', 'email', 'is_verified', 'locale']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['name_parts'] = NamePartsFormField(
|
||||
max_length=255,
|
||||
required=False,
|
||||
scheme=self.instance.organizer.settings.name_scheme,
|
||||
titles=self.instance.organizer.settings.name_scheme_titles,
|
||||
label=_('Name'),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
|
||||
if email is not None:
|
||||
try:
|
||||
self.instance.organizer.customers.exclude(pk=self.instance.pk).get(email=email)
|
||||
except Customer.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['duplicate'],
|
||||
code='duplicate',
|
||||
)
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
class MembershipUpdateForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Membership
|
||||
fields = ['membership_type', 'date_start', 'date_end', 'attendee_name_parts']
|
||||
field_classes = {
|
||||
'date_start': SplitDateTimeField,
|
||||
'date_end': SplitDateTimeField,
|
||||
}
|
||||
widgets = {
|
||||
'date_start': SplitDateTimePickerWidget(),
|
||||
'date_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_Start'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['membership_type'].queryset = self.instance.customer.organizer.membership_types.all()
|
||||
self.fields['attendee_name_parts'] = NamePartsFormField(
|
||||
max_length=255,
|
||||
required=False,
|
||||
scheme=self.instance.customer.organizer.settings.name_scheme,
|
||||
titles=self.instance.customer.organizer.settings.name_scheme_titles,
|
||||
label=_('Attendee name'),
|
||||
)
|
||||
|
||||
@@ -78,6 +78,10 @@ def _display_order_changed(event: Event, logentry: LogEntry):
|
||||
old_price=money_filter(Decimal(data['old_price']), event.currency),
|
||||
new_price=money_filter(Decimal(data['new_price']), event.currency),
|
||||
)
|
||||
elif logentry.action_type == 'pretix.event.order.changed.membership':
|
||||
return text + ' ' + _('Position #{posid}: Used membership changed.').format(
|
||||
posid=data.get('positionid', '?'),
|
||||
)
|
||||
elif logentry.action_type == 'pretix.event.order.changed.seat':
|
||||
return text + ' ' + _('Position #{posid}: Seat "{old_seat}" changed '
|
||||
'to "{new_seat}".').format(
|
||||
@@ -314,6 +318,17 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
|
||||
'pretix.webhook.created': _('The webhook has been created.'),
|
||||
'pretix.webhook.changed': _('The webhook has been changed.'),
|
||||
'pretix.membershiptype.created': _('The membership type has been created.'),
|
||||
'pretix.membershiptype.changed': _('The membership type has been changed.'),
|
||||
'pretix.membershiptype.deleted': _('The membership type has been deleted.'),
|
||||
'pretix.customer.created': _('The account has been created.'),
|
||||
'pretix.customer.changed': _('The account has been changed.'),
|
||||
'pretix.customer.membership.created': _('A membership for this account has been added.'),
|
||||
'pretix.customer.membership.changed': _('A membership of this account has been changed.'),
|
||||
'pretix.customer.anonymized': _('The account has been disabled and anonymized.'),
|
||||
'pretix.customer.password.resetrequested': _('A new password has been requested.'),
|
||||
'pretix.customer.password.set': _('A new password has been set.'),
|
||||
'pretix.email.error': _('Sending of an email has failed.'),
|
||||
'pretix.event.comment': _('The event\'s internal comment has been updated.'),
|
||||
'pretix.event.canceled': _('The event has been canceled.'),
|
||||
'pretix.event.deleted': _('An event has been deleted.'),
|
||||
@@ -338,6 +353,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'in the email for the first time).'),
|
||||
'pretix.event.order.phone.changed': _('The phone number has been changed from "{old_phone}" '
|
||||
'to "{new_phone}".'),
|
||||
'pretix.event.order.customer.changed': _('The customer account has been changed.'),
|
||||
'pretix.event.order.locale.changed': _('The order locale has been changed.'),
|
||||
'pretix.event.order.invoice.generated': _('The invoice has been generated.'),
|
||||
'pretix.event.order.invoice.regenerated': _('The invoice has been regenerated.'),
|
||||
|
||||
@@ -460,6 +460,13 @@ def get_organizer_navigation(request):
|
||||
}),
|
||||
'active': url.url_name.startswith('organizer.propert'),
|
||||
},
|
||||
{
|
||||
'label': _('E-mail'),
|
||||
'url': reverse('control:organizer.settings.mail', kwargs={
|
||||
'organizer': request.organizer.slug,
|
||||
}),
|
||||
'active': url.url_name == 'organizer.settings.mail',
|
||||
},
|
||||
{
|
||||
'label': _('Webhooks'),
|
||||
'url': reverse('control:organizer.webhooks', kwargs={
|
||||
@@ -467,9 +474,10 @@ def get_organizer_navigation(request):
|
||||
}),
|
||||
'active': 'organizer.webhook' in url.url_name,
|
||||
'icon': 'bolt',
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
if 'can_change_teams' in request.orgapermset:
|
||||
nav.append({
|
||||
'label': _('Teams'),
|
||||
@@ -490,6 +498,38 @@ def get_organizer_navigation(request):
|
||||
'icon': 'credit-card',
|
||||
})
|
||||
|
||||
if request.organizer.settings.customer_accounts:
|
||||
children = []
|
||||
if 'can_manage_customers' in request.orgapermset:
|
||||
children.append(
|
||||
{
|
||||
'label': _('Customers'),
|
||||
'url': reverse('control:organizer.customers', kwargs={
|
||||
'organizer': request.organizer.slug
|
||||
}),
|
||||
'active': 'organizer.customer' in url.url_name,
|
||||
}
|
||||
)
|
||||
if 'can_change_organizer_settings' in request.orgapermset:
|
||||
children.append(
|
||||
{
|
||||
'label': _('Membership types'),
|
||||
'url': reverse('control:organizer.membershiptypes', kwargs={
|
||||
'organizer': request.organizer.slug
|
||||
}),
|
||||
'active': 'organizer.membershiptype' in url.url_name,
|
||||
}
|
||||
)
|
||||
if children:
|
||||
nav.append({
|
||||
'label': _('Customer accounts'),
|
||||
'url': reverse('control:organizer.customers', kwargs={
|
||||
'organizer': request.organizer.slug
|
||||
}),
|
||||
'icon': 'user',
|
||||
'children': children,
|
||||
})
|
||||
|
||||
if 'can_change_organizer_settings' in request.orgapermset:
|
||||
nav.append({
|
||||
'label': _('Devices'),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixcontrol/event/settings_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load hierarkey_form %}
|
||||
{% load static %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "E-mail settings" %}</h1>
|
||||
@@ -11,11 +12,14 @@
|
||||
<div class="tabbed-form">
|
||||
<fieldset>
|
||||
<legend>{% trans "General" %}</legend>
|
||||
{% url "control:organizer.settings.mail" organizer=request.organizer.slug as org_url %}
|
||||
{% propagated request.event org_url "mail_from" "mail_from_name" "mail_text_signature" "mail_bcc" %}
|
||||
{% bootstrap_field form.mail_from layout="control" %}
|
||||
{% bootstrap_field form.mail_from_name layout="control" %}
|
||||
{% bootstrap_field form.mail_text_signature layout="control" %}
|
||||
{% bootstrap_field form.mail_bcc layout="control" %}
|
||||
{% endpropagated %}
|
||||
{% bootstrap_field form.mail_prefix layout="control" %}
|
||||
{% bootstrap_field form.mail_from layout="control" %}
|
||||
{% bootstrap_field form.mail_from_name layout="control" %}
|
||||
{% bootstrap_field form.mail_text_signature layout="control" %}
|
||||
{% bootstrap_field form.mail_bcc layout="control" %}
|
||||
{% bootstrap_field form.mail_attach_tickets layout="control" %}
|
||||
{% bootstrap_field form.mail_attach_ical layout="control" %}
|
||||
{% bootstrap_field form.mail_sales_channel_placed_paid layout="control" %}
|
||||
@@ -80,13 +84,15 @@
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "SMTP settings" %}</legend>
|
||||
{% bootstrap_field form.smtp_use_custom layout="control" %}
|
||||
{% bootstrap_field form.smtp_host layout="control" %}
|
||||
{% bootstrap_field form.smtp_port layout="control" %}
|
||||
{% bootstrap_field form.smtp_username layout="control" %}
|
||||
{% bootstrap_field form.smtp_password layout="control" %}
|
||||
{% bootstrap_field form.smtp_use_tls layout="control" %}
|
||||
{% bootstrap_field form.smtp_use_ssl layout="control" %}
|
||||
{% propagated request.event org_url "smtp_use_custom" "smtp_host" "smtp_port" "smtp_username" "smtp_password" "smtp_use_tls" "smtp_use_ssl" %}
|
||||
{% bootstrap_field form.smtp_use_custom layout="control" %}
|
||||
{% bootstrap_field form.smtp_host layout="control" %}
|
||||
{% bootstrap_field form.smtp_port layout="control" %}
|
||||
{% bootstrap_field form.smtp_username layout="control" %}
|
||||
{% bootstrap_field form.smtp_password layout="control" %}
|
||||
{% bootstrap_field form.smtp_use_tls layout="control" %}
|
||||
{% bootstrap_field form.smtp_use_ssl layout="control" %}
|
||||
{% endpropagated %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
|
||||
@@ -35,9 +35,15 @@
|
||||
{% bootstrap_field field show_label=False form_group_class="" %}
|
||||
</div>
|
||||
<div id="{{ item }}_preview" class="tab-pane mail-preview-group">
|
||||
{% for l in request.event.settings.locales %}
|
||||
<div lang="{{ l }}" for="{{ item }}" class="mail-preview"></div>
|
||||
{% endfor %}
|
||||
{% if request.event %}
|
||||
{% for l in request.event.settings.locales %}
|
||||
<div lang="{{ l }}" for="{{ item }}" class="mail-preview"></div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for l in request.organizer.settings.locales %}
|
||||
<div lang="{{ l }}" for="{{ item }}" class="mail-preview"></div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{% load static %}
|
||||
{% load hierarkey_form %}
|
||||
{% load formset_tags %}
|
||||
{% block title %}{% trans "General settings" %}{% endblock %}
|
||||
{% block custom_header %}
|
||||
{{ block.super }}
|
||||
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" %}">
|
||||
@@ -203,7 +204,7 @@
|
||||
{% bootstrap_field sform.logo_show_title layout="control" %}
|
||||
{% bootstrap_field sform.og_image layout="control" %}
|
||||
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
|
||||
{% propagated request.event org_url "primary_color" "primary_font" "theme_color_success" "theme_color_danger" %}
|
||||
{% propagated request.event org_url "primary_color" "primary_font" "theme_color_success" "theme_color_danger" "theme_round_borders" %}
|
||||
{% bootstrap_field sform.primary_color layout="control" %}
|
||||
{% bootstrap_field sform.theme_color_success layout="control" %}
|
||||
{% bootstrap_field sform.theme_color_danger layout="control" %}
|
||||
|
||||
@@ -46,6 +46,12 @@
|
||||
{% bootstrap_field form.default_price addon_after=request.event.currency layout="control" %}
|
||||
{% bootstrap_field form.original_price addon_after=request.event.currency layout="control" %}
|
||||
{% bootstrap_field form.description layout="control" %}
|
||||
{% if form.require_membership %}
|
||||
{% bootstrap_field form.require_membership layout="control" %}
|
||||
<div data-display-dependency="#{{ form.require_membership.id_for_label }}">
|
||||
{% bootstrap_field form.require_membership_types layout="control" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -80,6 +86,12 @@
|
||||
{% bootstrap_field formset.empty_form.default_price addon_after=request.event.currency layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.original_price addon_after=request.event.currency layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.description layout="control" %}
|
||||
{% if formset.empty_form.require_membership %}
|
||||
{% bootstrap_field formset.empty_form.require_membership layout="control" %}
|
||||
<div data-display-dependency="#{{ formset.empty_form.require_membership.id_for_label }}">
|
||||
{% bootstrap_field formset.empty_form.require_membership_types layout="control" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
|
||||
@@ -101,6 +101,12 @@
|
||||
{% bootstrap_field form.require_voucher layout="control" %}
|
||||
{% bootstrap_field form.hide_without_voucher layout="control" %}
|
||||
{% bootstrap_field form.require_bundling layout="control" %}
|
||||
{% if form.require_membership %}
|
||||
{% bootstrap_field form.require_membership layout="control" %}
|
||||
<div data-display-dependency="#{{ form.require_membership.id_for_label }}">
|
||||
{% bootstrap_field form.require_membership_types layout="control" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_field form.allow_cancel layout="control" %}
|
||||
{% bootstrap_field form.allow_waitinglist layout="control" %}
|
||||
{% bootstrap_field form.hidden_if_available layout="control" %}
|
||||
@@ -120,6 +126,28 @@
|
||||
<legend>{% trans "Additional settings" %}</legend>
|
||||
{% bootstrap_field form.issue_giftcard layout="control" %}
|
||||
{% bootstrap_field form.show_quota_left layout="control" %}
|
||||
{% if form.grant_membership_type %}
|
||||
{% bootstrap_field form.grant_membership_type layout="control" %}
|
||||
<div data-display-dependency="#id_grant_membership_type">
|
||||
{% bootstrap_field form.grant_membership_duration_like_event layout="control" %}
|
||||
<div data-display-dependency="#id_grant_membership_duration_like_event" data-inverse class="form-group">
|
||||
{% blocktrans asvar days %}days{% endblocktrans %}
|
||||
{% blocktrans asvar months %}months{% endblocktrans %}
|
||||
<label class="col-md-3 col-xs-12 control-label">
|
||||
{% trans "Membership duration after purchase" %}
|
||||
</label>
|
||||
<div class="col-md-4 col-xs-5">
|
||||
{% bootstrap_field form.grant_membership_duration_months layout="" addon_after=months label_class="sr-only" form_group_class="" %}
|
||||
</div>
|
||||
<label class="col-md-1 col-xs-2 control-label text-center">
|
||||
+
|
||||
</label>
|
||||
<div class="col-md-4 col-xs-5">
|
||||
{% bootstrap_field form.grant_membership_duration_days layout="" addon_after=days label_class="sr-only" form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for f in plugin_forms %}
|
||||
{% bootstrap_form f layout="control" %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -152,6 +152,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<strong>{% trans "Membership" %}</strong>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
{{ position.used_membership|default:"–" }}
|
||||
</div>
|
||||
<div class="col-sm-4 field-container">
|
||||
{% bootstrap_field position.form.used_membership layout='inline' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<strong>{% trans "Price" %}</strong>
|
||||
@@ -232,6 +244,9 @@
|
||||
{% if add_form.subevent %}
|
||||
{% bootstrap_field add_form.subevent layout="control" %}
|
||||
{% endif %}
|
||||
{% if add_form.used_membership %}
|
||||
{% bootstrap_field add_form.used_membership layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field add_form.seat layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -264,6 +279,9 @@
|
||||
{% if add_formset.empty_form.subevent %}
|
||||
{% bootstrap_field add_formset.empty_form.subevent layout="control" %}
|
||||
{% endif %}
|
||||
{% if add_formset.empty_form.used_membership %}
|
||||
{% bootstrap_field add_formset.empty_form.used_membership layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field add_formset.empty_form.seat layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -182,7 +182,20 @@
|
||||
<dt>{% trans "Expiry date" %}</dt>
|
||||
<dd>{{ order.expires|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
{% endif %}
|
||||
<dt>{% trans "User" %}</dt>
|
||||
{% if request.organizer.settings.customer_accounts %}
|
||||
<dt>{% trans "Customer account" %}</dt>
|
||||
<dd>
|
||||
{% if order.customer %}
|
||||
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=order.customer.identifier %}">
|
||||
{{ order.customer.identifier }} – {{ order.customer.email }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
</dd>
|
||||
{% endif %}
|
||||
<dt>{% trans "Contact email" %}</dt>
|
||||
<dd>
|
||||
{{ order.email|default_if_none:"" }}
|
||||
{% if order.email and order.email_known_to_work %}
|
||||
@@ -197,7 +210,7 @@
|
||||
</a>
|
||||
{% if order.status != "c" %}
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-default btn-xs">
|
||||
{% trans "Resend link" %}
|
||||
@@ -336,7 +349,7 @@
|
||||
{{ line.seat }}
|
||||
{% endif %}
|
||||
{% if line.voucher %}
|
||||
<br/><span class="fa fa-tags"></span> {% trans "Voucher code used:" %}
|
||||
<br/><span class="fa fa-tags fa-fw"></span> {% trans "Voucher code used:" %}
|
||||
<a
|
||||
{% if line.price_before_voucher|default_if_none:"NONE" != "NONE" %}data-toggle="tooltip" title="{% blocktrans trimmed with price=line.price_before_voucher|money:request.event.currency %}Original price: {{ price }}{% endblocktrans %}"{% endif %}
|
||||
href="{% url "control:event.voucher" event=request.event.slug organizer=request.event.organizer.slug voucher=line.voucher.pk %}">
|
||||
@@ -345,12 +358,18 @@
|
||||
{% endif %}
|
||||
{% if line.subevent %}
|
||||
<br/>
|
||||
<span class="fa fa-calendar"></span> {{ line.subevent.name }} · {{ line.subevent.get_date_range_display }}
|
||||
<span class="fa fa-calendar fa-fw"></span> {{ line.subevent.name }} · {{ line.subevent.get_date_range_display }}
|
||||
{% if event.settings.show_times %}
|
||||
<span class="fa fa-clock-o"></span>
|
||||
{{ line.subevent.date_from|date:"TIME_FORMAT" }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if line.used_membership %}
|
||||
<br /><span class="fa fa-id-card fa-fw" aria-hidden="true"></span>
|
||||
<a href="{% url "control:organizer.customer.membership.edit" organizer=request.organizer.slug customer=order.customer.identifier id=line.used_membership.pk %}">
|
||||
{{ line.used_membership }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if not line.canceled %}
|
||||
<div class="position-buttons">
|
||||
{% if line.generate_ticket %}
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
<a href="?{% url_replace request 'ordering' '-datetime' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'datetime' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th class="text-right flip">{% trans "Order paid / total" %}
|
||||
<th class="text-right flip">{% trans "Order paid / total" %}
|
||||
<a href="?{% url_replace request 'ordering' '-total' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'total' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th class="text-right flip">{% trans "Positions" %}</th>
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% block title %}
|
||||
{% blocktrans trimmed with id=customer.identifier %}
|
||||
Customer #{{ id }}
|
||||
{% endblocktrans %}
|
||||
{% endblock %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% blocktrans trimmed with id=customer.identifier %}
|
||||
Customer #{{ id }}
|
||||
{% endblocktrans %}
|
||||
</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-xs-12">
|
||||
<div class="panel panel-primary items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Details" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Customer ID" %}</dt>
|
||||
<dd>#{{ customer.identifier }}</dd>
|
||||
<dt>{% trans "Status" %}</dt>
|
||||
<dd>
|
||||
{% if not customer.is_active %}
|
||||
{% trans "disabled" %}
|
||||
{% elif not customer.is_verified %}
|
||||
{% trans "not yet activated" %}
|
||||
{% else %}
|
||||
{% trans "active" %}
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>{% trans "E-mail" %}</dt>
|
||||
<dd>{{ customer.email|default_if_none:"" }}</dd>
|
||||
<dt>{% trans "Name" %}</dt>
|
||||
<dd>{{ customer.name }}</dd>
|
||||
<dt>{% trans "Locale" %}</dt>
|
||||
<dd>{{ display_locale }}</dd>
|
||||
<dt>{% trans "Registration date" %}</dt>
|
||||
<dd>{{ customer.date_joined|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
<dt>{% trans "Last login" %}</dt>
|
||||
<dd>{% if customer.last_login %}{{ customer.last_login|date:"SHORT_DATETIME_FORMAT" }}{% else %}
|
||||
–{% endif %}</dd>
|
||||
</dl>
|
||||
<div class="text-right">
|
||||
<a href="{% url "control:organizer.customer.edit" organizer=request.organizer.slug customer=customer.identifier %}"
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-edit"></i> {% trans "Edit" %}
|
||||
</a>
|
||||
<a href="{% url "control:organizer.customer.anonymize" organizer=request.organizer.slug customer=customer.identifier %}"
|
||||
class="btn btn-danger">
|
||||
<i class="fa fa-trash"></i> {% trans "Anonymize" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Memberships" %}
|
||||
</h3>
|
||||
</div>
|
||||
<table class="panel-body table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Membership type" %}</th>
|
||||
<th>{% trans "Valid from" %}</th>
|
||||
<th>{% trans "Valid until" %}</th>
|
||||
<th>{% trans "Order" %}</th>
|
||||
<th>{% trans "Attendee name" %}</th>
|
||||
<th>{% trans "Usages" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in memberships %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ m.membership_type.name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ m.date_start|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</td>
|
||||
<td>
|
||||
{{ m.date_end|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</td>
|
||||
<td>
|
||||
{% if m.granted_in %}
|
||||
<a href="{% url "control:event.order" event=m.granted_in.order.event.slug organizer=customer.organizer.slug code=m.granted_in.order.code %}">
|
||||
{{ m.granted_in.order.code }}-{{ m.granted_in.positionid }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ m.attendee_name }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="quotabox">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success progress-bar-{{ m.percent }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="numbers">
|
||||
{{ m.usages }} /
|
||||
{{ m.membership_type.max_usages|default_if_none:"∞" }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% url "control:organizer.customer.membership.edit" organizer=request.organizer.slug customer=customer.identifier id=m.pk %}"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "Edit" %}"
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-edit"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<a href="{% url "control:organizer.customer.membership.add" organizer=request.organizer.slug customer=customer.identifier %}"
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-plus"></i>
|
||||
{% trans "Add membership" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Orders" %}
|
||||
</h3>
|
||||
</div>
|
||||
<table class="panel-body table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Order code" %}</th>
|
||||
<th>{% trans "Event" %}</th>
|
||||
<th>{% trans "Order date" %}</th>
|
||||
<th class="text-right">{% trans "Order paid / total" %}</th>
|
||||
<th class="text-right">{% trans "Positions" %}</th>
|
||||
<th class="text-right">{% trans "Status" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for o in orders %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
<a href="{% url "control:event.order" event=o.event.slug organizer=customer.organizer.slug code=o.code %}">
|
||||
{{ o.code }}
|
||||
</a>
|
||||
</strong>
|
||||
{% if o.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ o.event }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="fa fa-{{ o.sales_channel_obj.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{% trans o.sales_channel_obj.verbose_name %}"></span>
|
||||
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% if o.customer_id != customer.pk %}
|
||||
<span class="fa fa-link text-muted"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "Matched to the account based on the email address." %}"
|
||||
></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{% if o.has_cancellation_request %}
|
||||
<span class="label label-warning">{% trans "CANCELLATION REQUESTED" %}</span>
|
||||
{% endif %}
|
||||
{% if o.has_external_refund or o.has_pending_refund %}
|
||||
<span class="label label-danger">{% trans "REFUND PENDING" %}</span>
|
||||
{% elif o.has_pending_refund %}
|
||||
<span class="label label-warning">{% trans "REFUND PENDING" %}</span>
|
||||
{% endif %}
|
||||
{% if o.is_overpaid %}
|
||||
<span class="label label-warning">{% trans "OVERPAID" %}</span>
|
||||
{% elif o.is_underpaid %}
|
||||
<span class="label label-danger">{% trans "UNDERPAID" %}</span>
|
||||
{% elif o.is_pending_with_full_payment %}
|
||||
<span class="label label-danger">{% trans "FULLY PAID" %}</span>
|
||||
{% endif %}
|
||||
{% if o.computed_payment_refund_sum == o.total or o.computed_payment_refund_sum == 0 %}
|
||||
<span class="text-muted">
|
||||
{% endif %}
|
||||
{{ o.computed_payment_refund_sum|money:o.event.currency }} /
|
||||
{% if o.computed_payment_refund_sum == o.total or o.computed_payment_refund_sum == 0 %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{{ o.total|money:o.event.currency }}
|
||||
{% if o.status == "c" and o.icnt %}
|
||||
<br>
|
||||
<span class="label label-warning">{% trans "INVOICE NOT CANCELED" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">{{ o.pcnt|default_if_none:"0" }}</td>
|
||||
<td class="text-right flip">{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Customer history" %}
|
||||
</h3>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=customer %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,43 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}
|
||||
{% blocktrans trimmed with id=customer.identifier %}
|
||||
Anonymize customer #{{ id }}
|
||||
{% endblocktrans %}
|
||||
{% endblock %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% blocktrans trimmed with id=customer.identifier %}
|
||||
Anonymize customer #{{ id }}
|
||||
{% endblocktrans %}
|
||||
</h1>
|
||||
<p>
|
||||
{% trans "Are you sure you want to anonymize this customer account?" %}
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
{% trans "All orders will be disconnected from this customer account." %}
|
||||
<strong>
|
||||
{% trans "The orders themselves will not be anonymized and can still contain personal information!" %}
|
||||
</strong>
|
||||
</li>
|
||||
<li>
|
||||
{% trans "The customer will no longer be ble to log in and will lose access to any membership benefits." %}
|
||||
</li>
|
||||
<li>
|
||||
{% trans "This action is irreversible." %}
|
||||
</li>
|
||||
</ul>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=customer.identifier %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
{% trans "Anonymize" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}
|
||||
{% blocktrans trimmed with id=customer.identifier %}
|
||||
Customer #{{ id }}
|
||||
{% endblocktrans %}
|
||||
{% endblock %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% blocktrans trimmed with id=customer.identifier %}
|
||||
Customer #{{ id }}
|
||||
{% endblocktrans %}
|
||||
</h1>
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,88 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}
|
||||
{% trans "Membership" %}
|
||||
{% endblock %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% trans "Membership" %}
|
||||
</h1>
|
||||
<div class="panel panel-primary items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Details" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout="control" %}
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-3 col-md-9">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Usages" %}
|
||||
</h3>
|
||||
</div>
|
||||
<table class="panel-body table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Order code" %}</th>
|
||||
<th>{% trans "Event" %}</th>
|
||||
<th>{% trans "Date" context "subevent" %}</th>
|
||||
<th>{% trans "Product" %}</th>
|
||||
<th>{% trans "Order date" %}</th>
|
||||
<th class="text-right">{% trans "Status" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for op in usages %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
<a href="{% url "control:event.order" event=op.order.event.slug organizer=membership.customer.organizer.slug code=op.order.code %}">
|
||||
{{ op.order.code }}</a>-{{ op.positionid }}
|
||||
</strong>
|
||||
{% if op.order.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ op.order.event }}
|
||||
</td>
|
||||
<td>
|
||||
{{ op.subevent|default:"" }}
|
||||
</td>
|
||||
<td>
|
||||
{{ op.item }}
|
||||
{% if op.variation %}– {{ op.variation }}{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ op.order.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{% if op.canceled %}
|
||||
<span class="label label-danger">
|
||||
<span class="fa fa-times"></span>
|
||||
{% trans "Canceled" %}
|
||||
</span>
|
||||
{% else %}
|
||||
{% include "pretixcontrol/orders/fragment_order_status.html" with order=op.order %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,80 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load urlreplace %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% block title %}{% trans "Customers" %}{% endblock %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% trans "Customers" %}
|
||||
</h1>
|
||||
{% if customers|length == 0 and not filter_form.filtered %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
No customer accounts have been created yet.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<form class="row filter-form" action="" method="get">
|
||||
<div class="col-md-10 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.query layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-6 col-xs-12">
|
||||
<button class="btn btn-primary btn-block" type="submit">
|
||||
<span class="fa fa-filter"></span>
|
||||
<span class="hidden-md">
|
||||
{% trans "Filter" %}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Customer ID" %}
|
||||
<a href="?{% url_replace request 'ordering' '-identifier' %}"><i
|
||||
class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'identifier' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>{% trans "Email" %}
|
||||
<a href="?{% url_replace request 'ordering' '-email' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'email' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "Name" %}
|
||||
<a href="?{% url_replace request 'ordering' '-name' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'name' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in customers %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=c.identifier %}">
|
||||
{% if not c.is_active %}<strike>{% endif %}
|
||||
<strong>#{{ c.identifier }}</strong>
|
||||
{% if not c.is_active %}</strike>{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if not c.is_verified %}<strike>{% endif %}
|
||||
{{ c.email|default_if_none:"" }}
|
||||
{% if not c.is_verified %}</strike>{% endif %}
|
||||
</td>
|
||||
<td>{{ c.name }}</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=c.identifier %}"
|
||||
class="btn btn-default btn-sm" data-toggle="tooltip" title="{% trans "Details" %}">
|
||||
<i class="fa fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -50,6 +50,13 @@
|
||||
<legend>{% trans "Localization" %}</legend>
|
||||
{% bootstrap_field sform.locales layout="control" %}
|
||||
{% bootstrap_field sform.region layout="control" %}
|
||||
{% bootstrap_field sform.timezone layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Customer accounts" %}</legend>
|
||||
{% bootstrap_field sform.customer_accounts layout="control" %}
|
||||
{% bootstrap_field sform.name_scheme layout="control" %}
|
||||
{% bootstrap_field sform.name_scheme_titles layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Shop design" %}</legend>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load formset_tags %}
|
||||
{% block custom_header %}
|
||||
{{ block.super }}
|
||||
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" %}">
|
||||
{% endblock %}
|
||||
{% block title %}{% trans "Organizer" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "E-mail settings" %}</h1>
|
||||
|
||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data"
|
||||
mail-preview-url="{% url "control:organizer.settings.mail.preview" organizer=request.organizer.slug %}">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<div class="tabbed-form">
|
||||
<fieldset>
|
||||
<legend>{% trans "General" %}</legend>
|
||||
{% bootstrap_field form.mail_from layout="control" %}
|
||||
{% bootstrap_field form.mail_from_name layout="control" %}
|
||||
{% bootstrap_field form.mail_text_signature layout="control" %}
|
||||
{% bootstrap_field form.mail_bcc layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "E-mail content" %}</legend>
|
||||
<div class="panel-group" id="questions_group">
|
||||
{% blocktrans asvar title_customer_registration %}Customer account registration{% endblocktrans %}
|
||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="customer_registration" title=title_customer_registration items="mail_text_customer_registration" %}
|
||||
|
||||
{% blocktrans asvar title_email_change %}Customer account email change{% endblocktrans %}
|
||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="email_change" title=title_email_change items="mail_text_customer_email_change" %}
|
||||
|
||||
{% blocktrans asvar title_reset %}Customer account password reset{% endblocktrans %}
|
||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="reset" title=title_reset items="mail_text_customer_reset" %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "SMTP settings" %}</legend>
|
||||
{% bootstrap_field form.smtp_use_custom layout="control" %}
|
||||
{% bootstrap_field form.smtp_host layout="control" %}
|
||||
{% bootstrap_field form.smtp_port layout="control" %}
|
||||
{% bootstrap_field form.smtp_username layout="control" %}
|
||||
{% bootstrap_field form.smtp_password layout="control" %}
|
||||
{% bootstrap_field form.smtp_use_tls layout="control" %}
|
||||
{% bootstrap_field form.smtp_use_ssl layout="control" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-default btn-save pull-left" name="test" value="1">
|
||||
{% trans "Save and test custom SMTP connection" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,25 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<h1>{% trans "Delete membership type:" %} {{ type.name }}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% if is_allowed %}
|
||||
<p>{% blocktrans %}Are you sure you want to delete this membership type?{% endblocktrans %}
|
||||
{% else %}
|
||||
<p>{% blocktrans %}This membership type cannot be deleted since it has already been used.{% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:organizer.membershiptypes" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
{% if is_allowed %}
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
{% if type %}
|
||||
<h1>{% trans "Membership type:" %} {{ type.name }}</h1>
|
||||
{% else %}
|
||||
<h1>{% trans "Create a new membership type" %}</h1>
|
||||
{% endif %}
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,48 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Membership types" %}{% endblock %}
|
||||
{% block inner %}
|
||||
<h1>{% trans "Membership types" %}</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can define membership types. These allow you to link products from different events
|
||||
together. You can sell a membership as part of a a product in one event, and require valid
|
||||
memberships to allow purchases in another event.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
This can be used to enable products like year passes, tickets of ten, etc.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<a href="{% url "control:organizer.membershiptype.add" organizer=request.organizer.slug %}" class="btn btn-default">
|
||||
<span class="fa fa-plus"></span>
|
||||
{% trans "Create a new membership type" %}
|
||||
</a>
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in types %}
|
||||
<tr>
|
||||
<td><strong>
|
||||
<a href="{% url "control:organizer.membershiptype.edit" organizer=request.organizer.slug type=t.id %}">
|
||||
{{ t.name }}
|
||||
</a>
|
||||
</strong></td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% url "control:organizer.membershiptype.edit" organizer=request.organizer.slug type=t.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:organizer.membershiptype.delete" organizer=request.organizer.slug type=t.id %}"
|
||||
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<h1>{% trans "Delete property:" %} {{ gate.name }}</h1>
|
||||
<h1>{% trans "Delete property:" %} {{ type.name }}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans %}Are you sure you want to delete the property?{% endblocktrans %}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
{% if gate %}
|
||||
{% if property %}
|
||||
<h1>{% trans "Property:" %} {{ property.name }}</h1>
|
||||
{% else %}
|
||||
<h1>{% trans "Create a new property" %}</h1>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<legend>{% trans "Organizer permissions" %}</legend>
|
||||
{% bootstrap_field form.can_create_events layout="control" %}
|
||||
{% bootstrap_field form.can_manage_gift_cards layout="control" %}
|
||||
{% bootstrap_field form.can_manage_customers layout="control" %}
|
||||
{% bootstrap_field form.can_change_teams layout="control" %}
|
||||
{% bootstrap_field form.can_change_organizer_settings layout="control" %}
|
||||
</fieldset>
|
||||
|
||||
@@ -42,39 +42,41 @@ class PropagatedNode(Node):
|
||||
|
||||
if all([fn not in event.settings._cache() for fn in self.field_names]):
|
||||
body = """
|
||||
<div class="propagated-settings-box">
|
||||
<input type="hidden" name="_settings_ignore" value="{fnames}">
|
||||
<div class="propagated-settings-form blurred">
|
||||
{body}
|
||||
</div>
|
||||
<div class="propagated-settings-overlay">
|
||||
<h4><span class="fa fa-link"></span> {text_inh}</h4>
|
||||
<p>
|
||||
{text_expl}
|
||||
</p>
|
||||
<button class="btn btn-default" name="decouple" value="{fnames}" data-action="unlink">
|
||||
<span class="fa fa-unlink"></span> {text_unlink}
|
||||
<div class="propagated-settings-box locked panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<input type="hidden" name="_settings_ignore" value="{fnames}">
|
||||
<button class="btn btn-default pull-right btn-xs" name="decouple" value="{fnames}" data-action="unlink">
|
||||
<span class="fa fa-unlock"></span> {text_unlink}
|
||||
</button>
|
||||
<a class="btn btn-default" href="{url}" target="_blank">
|
||||
<span class="fa fa-group"></span> {text_orga}
|
||||
<h4 class="panel-title">
|
||||
<span class="fa fa-lock"></span> {text_inh}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="panel-body help-text">
|
||||
{text_expl}<br>
|
||||
<a href="{url}" target="_blank">
|
||||
{text_orga}
|
||||
</a>
|
||||
</div>
|
||||
<div class="panel-body propagated-settings-form">
|
||||
{body}
|
||||
</div>
|
||||
</div>
|
||||
""".format(
|
||||
body=body,
|
||||
text_inh=_("Organizer-level settings") if isinstance(event, Event) else _('Site-level settings'),
|
||||
text_inh=_("Currently set on organizer level") if isinstance(event, Event) else _('Currently set on global level'),
|
||||
fnames=','.join(self.field_names),
|
||||
text_expl=_(
|
||||
'These settings are currently set on organizer level. This way, you can easily change them for '
|
||||
'all of your events at the same time. You can either go to the organizer settings to change them '
|
||||
'or decouple them from the organizer account to change them for this event individually.'
|
||||
'all of your events at the same time. You can either go to the organizer settings to change them for all your events '
|
||||
'or you can unlock them to change them for this event individually.'
|
||||
) if isinstance(event, Event) else _(
|
||||
'These settings are currently set on global level. This way, you can easily change them for '
|
||||
'all organizers at the same time. You can either go to the global settings to change them '
|
||||
'or decouple them from the global settings to change them for this event individually.'
|
||||
'all organizers at the same time. You can either go to the global settings to change them for all your organizers '
|
||||
'or you can unlock them to change them for this event individually.'
|
||||
),
|
||||
text_unlink=_('Change only for this event') if isinstance(event, Event) else _('Change only for this organizer'),
|
||||
text_orga=_('Change for all events') if isinstance(event, Event) else _('Change for all organizers'),
|
||||
text_unlink=_('Unlock'),
|
||||
text_orga=_('Go to organizer settings') if isinstance(event, Event) else _('Go to global settings'),
|
||||
url=url
|
||||
)
|
||||
|
||||
|
||||
@@ -110,6 +110,10 @@ urlpatterns = [
|
||||
url(r'^organizers/select2$', typeahead.organizer_select2, name='organizers.select2'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/$', organizer.OrganizerDetail.as_view(), name='organizer'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/settings/email$',
|
||||
organizer.OrganizerMailSettings.as_view(), name='organizer.settings.mail'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/settings/email/preview$',
|
||||
organizer.MailSettingsPreview.as_view(), name='organizer.settings.mail.preview'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/delete$', organizer.OrganizerDelete.as_view(), name='organizer.delete'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/settings/display$', organizer.OrganizerDisplaySettings.as_view(),
|
||||
name='organizer.display'),
|
||||
@@ -120,6 +124,25 @@ urlpatterns = [
|
||||
name='organizer.property.edit'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/property/(?P<property>[^/]+)/delete$', organizer.EventMetaPropertyDeleteView.as_view(),
|
||||
name='organizer.property.delete'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/membershiptypes$', organizer.MembershipTypeListView.as_view(), name='organizer.membershiptypes'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/membershiptype/add$', organizer.MembershipTypeCreateView.as_view(),
|
||||
name='organizer.membershiptype.add'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/membershiptype/(?P<type>[^/]+)/edit$', organizer.MembershipTypeUpdateView.as_view(),
|
||||
name='organizer.membershiptype.edit'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/membershiptype/(?P<type>[^/]+)/delete$', organizer.MembershipTypeDeleteView.as_view(),
|
||||
name='organizer.membershiptype.delete'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/customers$', organizer.CustomerListView.as_view(), name='organizer.customers'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/customers/select2$', typeahead.customer_select2, name='organizer.customers.select2'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/customer/(?P<customer>[^/]+)/$',
|
||||
organizer.CustomerDetailView.as_view(), name='organizer.customer'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/customer/(?P<customer>[^/]+)/edit$',
|
||||
organizer.CustomerUpdateView.as_view(), name='organizer.customer.edit'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/customer/(?P<customer>[^/]+)/membership/add$',
|
||||
organizer.MembershipCreateView.as_view(), name='organizer.customer.membership.add'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/customer/(?P<customer>[^/]+)/membership/(?P<id>[^/]+)/edit$',
|
||||
organizer.MembershipUpdateView.as_view(), name='organizer.customer.membership.edit'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/customer/(?P<customer>[^/]+)/anonymize$',
|
||||
organizer.CustomerAnonymizeView.as_view(), name='organizer.customer.anonymize'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/giftcards$', organizer.GiftCardListView.as_view(), name='organizer.giftcards'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/giftcard/add$', organizer.GiftCardCreateView.as_view(), name='organizer.giftcard.add'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/giftcard/(?P<giftcard>[^/]+)/$', organizer.GiftCardDetailView.as_view(), name='organizer.giftcard'),
|
||||
|
||||
@@ -277,7 +277,7 @@ class Forgot(TemplateView):
|
||||
rc.setex('pretix_pwreset_%s' % (user.id), 3600 * 24, '1')
|
||||
|
||||
except User.DoesNotExist:
|
||||
logger.warning('Password reset for unregistered e-mail \"' + email + '\"requested.')
|
||||
logger.warning('Password reset for unregistered e-mail \"' + email + '\" requested.')
|
||||
|
||||
except SendMailException:
|
||||
logger.exception('Sending password reset e-mail to \"' + email + '\" failed.')
|
||||
|
||||
@@ -336,7 +336,7 @@ class OrderDetail(OrderView):
|
||||
cartpos = queryset.order_by(
|
||||
'item', 'variation'
|
||||
).select_related(
|
||||
'item', 'variation', 'addon_to', 'tax_rule'
|
||||
'item', 'variation', 'addon_to', 'tax_rule', 'used_membership', 'used_membership__membership_type'
|
||||
).prefetch_related(
|
||||
'item__questions', 'issued_gift_cards',
|
||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options').select_related('question')),
|
||||
@@ -1568,7 +1568,10 @@ class OrderChange(OrderView):
|
||||
|
||||
@cached_property
|
||||
def positions(self):
|
||||
positions = list(self.order.positions.select_related('item', 'item__tax_rule'))
|
||||
positions = list(self.order.positions.select_related(
|
||||
'item', 'item__tax_rule', 'used_membership', 'used_membership__membership_type', 'tax_rule',
|
||||
'seat', 'subevent',
|
||||
))
|
||||
for p in positions:
|
||||
p.form = OrderPositionChangeForm(prefix='op-{}'.format(p.pk), instance=p, items=self.items,
|
||||
initial={'seat': p.seat.seat_guid if p.seat else None},
|
||||
@@ -1616,7 +1619,8 @@ class OrderChange(OrderView):
|
||||
f.cleaned_data['price'],
|
||||
f.cleaned_data.get('addon_to'),
|
||||
f.cleaned_data.get('subevent'),
|
||||
f.cleaned_data.get('seat'))
|
||||
f.cleaned_data.get('seat'),
|
||||
f.cleaned_data.get('used_membership'))
|
||||
except OrderError as e:
|
||||
f.custom_error = str(e)
|
||||
return False
|
||||
@@ -1685,6 +1689,12 @@ class OrderChange(OrderView):
|
||||
if p.form.cleaned_data['price'] is not None and p.form.cleaned_data['price'] != p.price:
|
||||
ocm.change_price(p, p.form.cleaned_data['price'])
|
||||
|
||||
if p.form.cleaned_data['used_membership'] is not None and p.form.cleaned_data['used_membership'] != (p.used_membership or 'CLEAR'):
|
||||
if p.form.cleaned_data['used_membership'] == 'CLEAR':
|
||||
ocm.change_membership(p, None)
|
||||
else:
|
||||
ocm.change_membership(p, p.form.cleaned_data['used_membership'])
|
||||
|
||||
if p.form.cleaned_data['tax_rule'] and p.form.cleaned_data['tax_rule'] != p.tax_rule:
|
||||
ocm.change_tax_rule(p, p.form.cleaned_data['tax_rule'])
|
||||
|
||||
@@ -1792,12 +1802,18 @@ class OrderContactChange(OrderView):
|
||||
def form(self):
|
||||
return OrderContactForm(
|
||||
instance=self.order,
|
||||
data=self.request.POST if self.request.method == "POST" else None
|
||||
data=self.request.POST if self.request.method == "POST" else None,
|
||||
customers=self.request.organizer.settings.customer_accounts and (
|
||||
self.request.user.has_organizer_permission(
|
||||
self.request.organizer, 'can_manage_customers', request=self.request
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
old_email = self.order.email
|
||||
old_phone = self.order.phone
|
||||
old_customer = self.order.customer
|
||||
changed = False
|
||||
if self.form.is_valid():
|
||||
new_email = self.form.cleaned_data['email']
|
||||
@@ -1824,6 +1840,18 @@ class OrderContactChange(OrderView):
|
||||
user=self.request.user,
|
||||
)
|
||||
|
||||
new_customer = self.form.cleaned_data.get('customer')
|
||||
if new_customer != old_customer:
|
||||
changed = True
|
||||
self.order.log_action(
|
||||
'pretix.event.order.customer.changed',
|
||||
data={
|
||||
'old_customer': old_customer,
|
||||
'new_customer': self.form.cleaned_data['customer'],
|
||||
},
|
||||
user=self.request.user,
|
||||
)
|
||||
|
||||
if self.form.cleaned_data['regenerate_secrets']:
|
||||
changed = True
|
||||
self.order.secret = generate_secret()
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import json
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -43,11 +44,12 @@ from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
Count, Max, Min, OuterRef, Prefetch, ProtectedError, Subquery, Sum,
|
||||
Count, Exists, IntegerField, Max, Min, OuterRef, Prefetch, ProtectedError,
|
||||
Q, Subquery, Sum,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Greatest
|
||||
from django.forms import DecimalField
|
||||
from django.http import JsonResponse
|
||||
from django.http import HttpResponseBadRequest, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
@@ -61,29 +63,37 @@ from django.views.generic import (
|
||||
|
||||
from pretix.api.models import WebHook
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Device, Gate, GiftCard, LogEntry, OrderPayment, Organizer,
|
||||
CachedFile, Customer, Device, Gate, GiftCard, Invoice, LogEntry,
|
||||
Membership, MembershipType, Order, OrderPayment, OrderPosition, Organizer,
|
||||
Team, TeamInvite, User,
|
||||
)
|
||||
from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue
|
||||
from pretix.base.models.giftcards import (
|
||||
GiftCardTransaction, gen_giftcard_secret,
|
||||
)
|
||||
from pretix.base.models.orders import CancellationRequest
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.services.export import multiexport
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.base.settings import SETTINGS_AFFECTING_CSS
|
||||
from pretix.base.signals import register_multievent_data_exporters
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.control.forms.filter import (
|
||||
EventFilterForm, GiftCardFilterForm, OrganizerFilterForm, TeamFilterForm,
|
||||
CustomerFilterForm, EventFilterForm, GiftCardFilterForm,
|
||||
OrganizerFilterForm, TeamFilterForm,
|
||||
)
|
||||
from pretix.control.forms.orders import ExporterForm
|
||||
from pretix.control.forms.organizer import (
|
||||
DeviceForm, EventMetaPropertyForm, GateForm, GiftCardCreateForm,
|
||||
GiftCardUpdateForm, OrganizerDeleteForm, OrganizerForm,
|
||||
OrganizerSettingsForm, OrganizerUpdateForm, TeamForm, WebHookForm,
|
||||
CustomerUpdateForm, DeviceForm, EventMetaPropertyForm, GateForm,
|
||||
GiftCardCreateForm, GiftCardUpdateForm, MailSettingsForm,
|
||||
MembershipTypeForm, MembershipUpdateForm, OrganizerDeleteForm,
|
||||
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
|
||||
WebHookForm,
|
||||
)
|
||||
from pretix.control.logdisplay import OVERVIEW_BANLIST
|
||||
from pretix.control.permissions import (
|
||||
@@ -228,6 +238,104 @@ class OrganizerSettingsFormView(OrganizerDetailViewMixin, OrganizerPermissionReq
|
||||
return self.get(request)
|
||||
|
||||
|
||||
class OrganizerMailSettings(OrganizerSettingsFormView):
|
||||
form_class = MailSettingsForm
|
||||
template_name = 'pretixcontrol/organizers/mail.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.settings.mail', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
if form.has_changed():
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.settings', user=self.request.user, data={
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
)
|
||||
|
||||
if request.POST.get('test', '0').strip() == '1':
|
||||
backend = self.request.organizer.get_mail_backend(force_custom=True, timeout=10)
|
||||
try:
|
||||
backend.test(self.request.organizer.settings.mail_from)
|
||||
except Exception as e:
|
||||
messages.warning(self.request, _('An error occurred while contacting the SMTP server: %s') % str(e))
|
||||
else:
|
||||
if form.cleaned_data.get('smtp_use_custom'):
|
||||
messages.success(self.request, _('Your changes have been saved and the connection attempt to '
|
||||
'your SMTP server was successful.'))
|
||||
else:
|
||||
messages.success(self.request, _('We\'ve been able to contact the SMTP server you configured. '
|
||||
'Remember to check the "use custom SMTP server" checkbox, '
|
||||
'otherwise your SMTP server will not be used.'))
|
||||
else:
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||
return self.get(request)
|
||||
|
||||
|
||||
class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
|
||||
permission = 'can_change_organizer_settings'
|
||||
|
||||
# return the origin text if key is missing in dict
|
||||
class SafeDict(dict):
|
||||
def __missing__(self, key):
|
||||
return '{' + key + '}'
|
||||
|
||||
# create index-language mapping
|
||||
@cached_property
|
||||
def supported_locale(self):
|
||||
locales = {}
|
||||
for idx, val in enumerate(settings.LANGUAGES):
|
||||
if val[0] in self.request.organizer.settings.locales:
|
||||
locales[str(idx)] = val[0]
|
||||
return locales
|
||||
|
||||
# get all supported placeholders with dummy values
|
||||
def placeholders(self, item):
|
||||
ctx = {}
|
||||
for p, s in MailSettingsForm(obj=self.request.organizer)._get_sample_context(MailSettingsForm.base_context[item]).items():
|
||||
if s.strip().startswith('*'):
|
||||
ctx[p] = s
|
||||
else:
|
||||
ctx[p] = '<span class="placeholder" title="{}">{}</span>'.format(
|
||||
_('This value will be replaced based on dynamic parameters.'),
|
||||
s
|
||||
)
|
||||
return self.SafeDict(ctx)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
preview_item = request.POST.get('item', '')
|
||||
if preview_item not in MailSettingsForm.base_context:
|
||||
return HttpResponseBadRequest(_('invalid item'))
|
||||
|
||||
regex = r"^" + re.escape(preview_item) + r"_(?P<idx>[\d+])$"
|
||||
msgs = {}
|
||||
for k, v in request.POST.items():
|
||||
# only accept allowed fields
|
||||
matched = re.search(regex, k)
|
||||
if matched is not None:
|
||||
idx = matched.group('idx')
|
||||
if idx in self.supported_locale:
|
||||
with language(self.supported_locale[idx], self.request.organizer.settings.region):
|
||||
msgs[self.supported_locale[idx]] = markdown_compile_email(
|
||||
v.format_map(self.placeholders(preview_item))
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'item': preview_item,
|
||||
'msgs': msgs
|
||||
})
|
||||
|
||||
|
||||
class OrganizerDisplaySettings(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, View):
|
||||
permission = None
|
||||
|
||||
@@ -1502,3 +1610,339 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
return ctx
|
||||
|
||||
|
||||
class MembershipTypeListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = MembershipType
|
||||
template_name = 'pretixcontrol/organizers/membershiptypes.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'types'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.membership_types.all()
|
||||
|
||||
|
||||
class MembershipTypeCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
||||
model = MembershipType
|
||||
template_name = 'pretixcontrol/organizers/membershiptype_edit.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
form_class = MembershipTypeForm
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(MembershipType, organizer=self.request.organizer, pk=self.kwargs.get('type'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.membershiptypes', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['event'] = self.request.organizer
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('The membership type has been created.'))
|
||||
form.instance.organizer = self.request.organizer
|
||||
ret = super().form_valid(form)
|
||||
form.instance.log_action('pretix.membershiptype.created', user=self.request.user, data={
|
||||
k: getattr(self.object, k) for k in form.changed_data
|
||||
})
|
||||
return ret
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class MembershipTypeUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
||||
model = MembershipType
|
||||
template_name = 'pretixcontrol/organizers/membershiptype_edit.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'type'
|
||||
form_class = MembershipTypeForm
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(MembershipType, organizer=self.request.organizer, pk=self.kwargs.get('type'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.membershiptypes', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['event'] = self.request.organizer
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.has_changed():
|
||||
self.object.log_action('pretix.membershiptype.changed', user=self.request.user, data={
|
||||
k: getattr(self.object, k)
|
||||
for k in form.changed_data
|
||||
})
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class MembershipTypeDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
|
||||
model = MembershipType
|
||||
template_name = 'pretixcontrol/organizers/membershiptype_delete.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'type'
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(MembershipType, organizer=self.request.organizer, pk=self.kwargs.get('type'))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['is_allowed'] = self.object.allow_delete()
|
||||
return ctx
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.membershiptypes', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
@transaction.atomic
|
||||
def delete(self, request, *args, **kwargs):
|
||||
success_url = self.get_success_url()
|
||||
self.object = self.get_object()
|
||||
if self.object.allow_delete():
|
||||
self.object.log_action('pretix.membershiptype.deleted', user=self.request.user)
|
||||
self.object.delete()
|
||||
messages.success(request, _('The selected object has been deleted.'))
|
||||
return redirect(success_url)
|
||||
|
||||
|
||||
class CustomerListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
model = Customer
|
||||
template_name = 'pretixcontrol/organizers/customers.html'
|
||||
permission = 'can_manage_customers'
|
||||
context_object_name = 'customers'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.organizer.customers.all()
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['filter_form'] = self.filter_form
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return CustomerFilterForm(data=self.request.GET, request=self.request)
|
||||
|
||||
|
||||
class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
template_name = 'pretixcontrol/organizers/customer.html'
|
||||
permission = 'can_manage_customers'
|
||||
context_object_name = 'orders'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = Order.objects.filter(
|
||||
Q(customer=self.customer)
|
||||
| Q(email__iexact=self.customer.email)
|
||||
).select_related('event').order_by('-datetime')
|
||||
return qs
|
||||
|
||||
@cached_property
|
||||
def customer(self):
|
||||
return get_object_or_404(
|
||||
self.request.organizer.customers,
|
||||
identifier=self.kwargs.get('customer')
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['customer'] = self.customer
|
||||
ctx['display_locale'] = dict(settings.LANGUAGES)[self.customer.locale or self.request.organizer.settings.locale]
|
||||
|
||||
ctx['memberships'] = self.customer.memberships.with_usages().select_related(
|
||||
'membership_type', 'granted_in', 'granted_in__order', 'granted_in__order__event'
|
||||
)
|
||||
|
||||
for m in ctx['memberships']:
|
||||
if m.membership_type.max_usages:
|
||||
m.percent = int(m.usages / m.membership_type.max_usages * 100)
|
||||
else:
|
||||
m.percent = 0
|
||||
|
||||
# Only compute this annotations for this page (query optimization)
|
||||
s = OrderPosition.objects.filter(
|
||||
order=OuterRef('pk')
|
||||
).order_by().values('order').annotate(k=Count('id')).values('k')
|
||||
i = Invoice.objects.filter(
|
||||
order=OuterRef('pk'),
|
||||
is_cancellation=False,
|
||||
refered__isnull=True,
|
||||
).order_by().values('order').annotate(k=Count('id')).values('k')
|
||||
annotated = {
|
||||
o['pk']: o
|
||||
for o in
|
||||
Order.annotate_overpayments(Order.objects, sums=True).filter(
|
||||
pk__in=[o.pk for o in ctx['orders']]
|
||||
).annotate(
|
||||
pcnt=Subquery(s, output_field=IntegerField()),
|
||||
icnt=Subquery(i, output_field=IntegerField()),
|
||||
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef('pk')))
|
||||
).values(
|
||||
'pk', 'pcnt', 'is_overpaid', 'is_underpaid', 'is_pending_with_full_payment', 'has_external_refund',
|
||||
'has_pending_refund', 'has_cancellation_request', 'computed_payment_refund_sum', 'icnt'
|
||||
)
|
||||
}
|
||||
|
||||
scs = get_all_sales_channels()
|
||||
for o in ctx['orders']:
|
||||
if o.pk not in annotated:
|
||||
continue
|
||||
o.pcnt = annotated.get(o.pk)['pcnt']
|
||||
o.is_overpaid = annotated.get(o.pk)['is_overpaid']
|
||||
o.is_underpaid = annotated.get(o.pk)['is_underpaid']
|
||||
o.is_pending_with_full_payment = annotated.get(o.pk)['is_pending_with_full_payment']
|
||||
o.has_external_refund = annotated.get(o.pk)['has_external_refund']
|
||||
o.has_pending_refund = annotated.get(o.pk)['has_pending_refund']
|
||||
o.has_cancellation_request = annotated.get(o.pk)['has_cancellation_request']
|
||||
o.computed_payment_refund_sum = annotated.get(o.pk)['computed_payment_refund_sum']
|
||||
o.icnt = annotated.get(o.pk)['icnt']
|
||||
o.sales_channel_obj = scs[o.sales_channel]
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class CustomerUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
||||
template_name = 'pretixcontrol/organizers/customer_edit.html'
|
||||
permission = 'can_manage_customers'
|
||||
context_object_name = 'customer'
|
||||
form_class = CustomerUpdateForm
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(
|
||||
self.request.organizer.customers,
|
||||
identifier=self.kwargs.get('customer')
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.has_changed():
|
||||
self.object.log_action('pretix.customer.changed', user=self.request.user, data={
|
||||
k: getattr(self.object, k)
|
||||
for k in form.changed_data
|
||||
})
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.customer', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'customer': self.object.identifier,
|
||||
})
|
||||
|
||||
|
||||
class MembershipUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
||||
template_name = 'pretixcontrol/organizers/customer_membership.html'
|
||||
permission = 'can_manage_customers'
|
||||
context_object_name = 'membership'
|
||||
form_class = MembershipUpdateForm
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(
|
||||
Membership,
|
||||
customer__organizer=self.request.organizer,
|
||||
customer__identifier=self.kwargs.get('customer'),
|
||||
pk=self.kwargs.get('id')
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['usages'] = self.object.orderposition_set.select_related(
|
||||
'order', 'order__event', 'subevent', 'item', 'variation',
|
||||
)
|
||||
return ctx
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.has_changed():
|
||||
d = {
|
||||
k: getattr(self.object, k)
|
||||
for k in form.changed_data
|
||||
}
|
||||
d['id'] = self.object.pk
|
||||
self.object.customer.log_action('pretix.customer.membership.changed', user=self.request.user, data=d)
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.customer', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'customer': self.object.customer.identifier,
|
||||
})
|
||||
|
||||
|
||||
class MembershipCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
||||
template_name = 'pretixcontrol/organizers/customer_membership.html'
|
||||
permission = 'can_manage_customers'
|
||||
context_object_name = 'membership'
|
||||
form_class = MembershipUpdateForm
|
||||
|
||||
@cached_property
|
||||
def customer(self):
|
||||
return get_object_or_404(
|
||||
self.request.organizer.customers,
|
||||
identifier=self.kwargs.get('customer')
|
||||
)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['instance'] = Membership(
|
||||
customer=self.customer,
|
||||
)
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
r = super().form_valid(form)
|
||||
d = {
|
||||
k: getattr(self.object, k)
|
||||
for k in form.changed_data
|
||||
}
|
||||
d['id'] = self.object.pk
|
||||
self.customer.log_action('pretix.customer.membership.created', user=self.request.user, data=d)
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return r
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.customer', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'customer': self.object.customer.identifier,
|
||||
})
|
||||
|
||||
|
||||
class CustomerAnonymizeView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
||||
template_name = 'pretixcontrol/organizers/customer_anonymize.html'
|
||||
permission = 'can_manage_customers'
|
||||
context_object_name = 'customer'
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(
|
||||
self.request.organizer.customers,
|
||||
identifier=self.kwargs.get('customer')
|
||||
)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
with transaction.atomic():
|
||||
self.object.anonymize()
|
||||
self.object.log_action('pretix.customer.anonymized', user=self.request.user)
|
||||
messages.success(self.request, _('The customer account has been anonymized.'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.customer', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'customer': self.object.identifier,
|
||||
})
|
||||
|
||||
@@ -51,7 +51,9 @@ from pretix.base.models import (
|
||||
ItemVariation, Order, Organizer, User, Voucher,
|
||||
)
|
||||
from pretix.control.forms.event import EventWizardCopyForm
|
||||
from pretix.control.permissions import event_permission_required
|
||||
from pretix.control.permissions import (
|
||||
event_permission_required, organizer_permission_required,
|
||||
)
|
||||
from pretix.helpers.daterange import daterange
|
||||
from pretix.helpers.i18n import i18ncomp
|
||||
|
||||
@@ -169,6 +171,36 @@ def event_list(request):
|
||||
return JsonResponse(doc)
|
||||
|
||||
|
||||
@organizer_permission_required("can_manage_customers")
|
||||
def customer_select2(request, **kwargs):
|
||||
query = request.GET.get('query', '')
|
||||
try:
|
||||
page = int(request.GET.get('page', '1'))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
qs = request.organizer.customers.filter(
|
||||
Q(email__icontains=query) | Q(name_cached__icontains=query) | Q(identifier__istartswith=query)
|
||||
).order_by('name_cached')
|
||||
|
||||
total = qs.count()
|
||||
pagesize = 20
|
||||
offset = (page - 1) * pagesize
|
||||
doc = {
|
||||
'results': [
|
||||
{
|
||||
'id': e.pk,
|
||||
'text': str(e),
|
||||
}
|
||||
for e in qs[offset:offset + pagesize]
|
||||
],
|
||||
'pagination': {
|
||||
"more": total >= (offset + pagesize)
|
||||
}
|
||||
}
|
||||
return JsonResponse(doc)
|
||||
|
||||
|
||||
def nav_context_list(request):
|
||||
query = request.GET.get('query', '').strip()
|
||||
organizer = request.GET.get('organizer', None)
|
||||
|
||||
Reference in New Issue
Block a user