mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
New implementation of sales channels (#4111)
Co-authored-by: Martin Gross <gross@rami.io>
This commit is contained in:
@@ -438,3 +438,20 @@ class ButtonGroupRadioSelect(forms.RadioSelect):
|
||||
attrs['icon'] = self.option_icons[value]
|
||||
opt = super().create_option(name, value, label, selected, index, subindex, attrs)
|
||||
return opt
|
||||
|
||||
|
||||
class SalesChannelCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
|
||||
option_template_name = 'pretixbase/forms/widgets/checkbox_sales_channel_option.html'
|
||||
|
||||
def __init__(self, event, attrs=None, choices=()):
|
||||
self.event = event
|
||||
super().__init__(attrs, choices)
|
||||
|
||||
def create_option(
|
||||
self, name, value, label, selected, index, subindex=None, attrs=None
|
||||
):
|
||||
plugin = value.instance.type_instance.required_event_plugin
|
||||
return {
|
||||
**super().create_option(name, value, label, selected, index, subindex, attrs),
|
||||
"plugin_missing": plugin and plugin not in self.event.get_plugins(),
|
||||
}
|
||||
|
||||
@@ -30,11 +30,12 @@ from django_scopes.forms import (
|
||||
SafeModelChoiceField, SafeModelMultipleChoiceField,
|
||||
)
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||
from pretix.base.models import Gate
|
||||
from pretix.base.models.checkin import Checkin, CheckinList
|
||||
from pretix.control.forms import ItemMultipleChoiceField
|
||||
from pretix.control.forms import (
|
||||
ItemMultipleChoiceField, SalesChannelCheckboxSelectMultiple,
|
||||
)
|
||||
from pretix.control.forms.widgets import Select2
|
||||
|
||||
|
||||
@@ -66,14 +67,9 @@ class CheckinListForm(forms.ModelForm):
|
||||
kwargs.pop('locales', None)
|
||||
super().__init__(**kwargs)
|
||||
self.fields['limit_products'].queryset = self.event.items.all()
|
||||
self.fields['auto_checkin_sales_channels'] = forms.MultipleChoiceField(
|
||||
label=self.fields['auto_checkin_sales_channels'].label,
|
||||
help_text=self.fields['auto_checkin_sales_channels'].help_text,
|
||||
required=self.fields['auto_checkin_sales_channels'].required,
|
||||
choices=(
|
||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
||||
),
|
||||
widget=forms.CheckboxSelectMultiple
|
||||
self.fields['auto_checkin_sales_channels'].queryset = self.event.organizer.sales_channels.all()
|
||||
self.fields['auto_checkin_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(
|
||||
self.event, choices=self.fields['auto_checkin_sales_channels'].widget.choices
|
||||
)
|
||||
|
||||
if not self.event.organizer.gates.exists():
|
||||
@@ -123,13 +119,13 @@ class CheckinListForm(forms.ModelForm):
|
||||
'gates': forms.CheckboxSelectMultiple(attrs={
|
||||
'class': 'scrolling-multiple-choice'
|
||||
}),
|
||||
'auto_checkin_sales_channels': forms.CheckboxSelectMultiple(),
|
||||
'exit_all_at': NextTimeInput(attrs={'class': 'timepickerfield'}),
|
||||
}
|
||||
field_classes = {
|
||||
'limit_products': ItemMultipleChoiceField,
|
||||
'gates': SafeModelMultipleChoiceField,
|
||||
'subevent': SafeModelChoiceField,
|
||||
'auto_checkin_sales_channels': SafeModelMultipleChoiceField,
|
||||
'exit_all_at': NextTimeField,
|
||||
}
|
||||
|
||||
|
||||
@@ -22,13 +22,16 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.channels import get_all_sales_channel_types
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||
from pretix.base.models import Discount
|
||||
from pretix.control.forms import ItemMultipleChoiceField, SplitDateTimeField
|
||||
from pretix.control.forms import (
|
||||
ItemMultipleChoiceField, SalesChannelCheckboxSelectMultiple,
|
||||
SplitDateTimeField,
|
||||
)
|
||||
|
||||
|
||||
class DiscountForm(I18nModelForm):
|
||||
@@ -38,7 +41,8 @@ class DiscountForm(I18nModelForm):
|
||||
fields = [
|
||||
'active',
|
||||
'internal_name',
|
||||
'sales_channels',
|
||||
'all_sales_channels',
|
||||
'limit_sales_channels',
|
||||
'available_from',
|
||||
'available_until',
|
||||
'subevent_mode',
|
||||
@@ -60,6 +64,7 @@ class DiscountForm(I18nModelForm):
|
||||
'available_until': SplitDateTimeField,
|
||||
'condition_limit_products': ItemMultipleChoiceField,
|
||||
'benefit_limit_products': ItemMultipleChoiceField,
|
||||
'limit_sales_channels': SafeModelMultipleChoiceField,
|
||||
}
|
||||
widgets = {
|
||||
'subevent_mode': forms.RadioSelect,
|
||||
@@ -83,15 +88,12 @@ class DiscountForm(I18nModelForm):
|
||||
self.event = kwargs['event']
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['sales_channels'] = forms.MultipleChoiceField(
|
||||
label=_('Sales channels'),
|
||||
required=True,
|
||||
choices=(
|
||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
||||
if c.discounts_supported
|
||||
),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.filter(
|
||||
type__in=[k for k, v in get_all_sales_channel_types().items() if v.discounts_supported]
|
||||
)
|
||||
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
|
||||
'data-inverse-dependency': '<[name$=all_sales_channels]',
|
||||
}, choices=self.fields['limit_sales_channels'].widget.choices)
|
||||
self.fields['condition_limit_products'].queryset = self.event.items.all()
|
||||
self.fields['benefit_limit_products'].queryset = self.event.items.all()
|
||||
self.fields['condition_min_count'].required = False
|
||||
|
||||
@@ -44,9 +44,7 @@ from django.conf import settings
|
||||
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
|
||||
from django.core.validators import MaxValueValidator
|
||||
from django.db.models import Prefetch, Q, prefetch_related_objects
|
||||
from django.forms import (
|
||||
CheckboxSelectMultiple, formset_factory, inlineformset_factory,
|
||||
)
|
||||
from django.forms import formset_factory, inlineformset_factory
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import escape, format_html
|
||||
@@ -54,12 +52,12 @@ from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import get_current_timezone_name
|
||||
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
|
||||
from django_countries.fields import LazyTypedChoiceField
|
||||
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||
from i18nfield.forms import (
|
||||
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
|
||||
)
|
||||
from pytz import common_timezones
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.forms import (
|
||||
I18nMarkdownTextarea, I18nModelForm, PlaceholderValidator, SettingsForm,
|
||||
)
|
||||
@@ -73,8 +71,8 @@ from pretix.base.settings import (
|
||||
)
|
||||
from pretix.base.validators import multimail_validate
|
||||
from pretix.control.forms import (
|
||||
MultipleLanguagesWidget, SlugWidget, SplitDateTimeField,
|
||||
SplitDateTimePickerWidget,
|
||||
MultipleLanguagesWidget, SalesChannelCheckboxSelectMultiple, SlugWidget,
|
||||
SplitDateTimeField, SplitDateTimePickerWidget,
|
||||
)
|
||||
from pretix.control.forms.widgets import Select2
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
@@ -378,16 +376,10 @@ class EventUpdateForm(I18nModelForm):
|
||||
required=False,
|
||||
help_text=_('You need to configure the custom domain in the webserver beforehand.')
|
||||
)
|
||||
self.fields['sales_channels'] = forms.MultipleChoiceField(
|
||||
label=self.fields['sales_channels'].label,
|
||||
help_text=self.fields['sales_channels'].help_text,
|
||||
required=self.fields['sales_channels'].required,
|
||||
initial=self.fields['sales_channels'].initial,
|
||||
choices=(
|
||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
||||
),
|
||||
widget=forms.CheckboxSelectMultiple
|
||||
)
|
||||
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
|
||||
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
|
||||
'data-inverse-dependency': '<[name$=all_sales_channels]',
|
||||
}, choices=self.fields['limit_sales_channels'].widget.choices)
|
||||
|
||||
def clean_domain(self):
|
||||
d = self.cleaned_data['domain']
|
||||
@@ -444,7 +436,8 @@ class EventUpdateForm(I18nModelForm):
|
||||
'location',
|
||||
'geo_lat',
|
||||
'geo_lon',
|
||||
'sales_channels'
|
||||
'all_sales_channels',
|
||||
'limit_sales_channels',
|
||||
]
|
||||
field_classes = {
|
||||
'date_from': SplitDateTimeField,
|
||||
@@ -452,6 +445,7 @@ class EventUpdateForm(I18nModelForm):
|
||||
'date_admission': SplitDateTimeField,
|
||||
'presale_start': SplitDateTimeField,
|
||||
'presale_end': SplitDateTimeField,
|
||||
'limit_sales_channels': SafeModelMultipleChoiceField,
|
||||
}
|
||||
widgets = {
|
||||
'date_from': SplitDateTimePickerWidget(),
|
||||
@@ -459,7 +453,6 @@ class EventUpdateForm(I18nModelForm):
|
||||
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}),
|
||||
'presale_start': SplitDateTimePickerWidget(),
|
||||
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
|
||||
'sales_channels': CheckboxSelectMultiple(),
|
||||
}
|
||||
|
||||
|
||||
@@ -915,7 +908,7 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
||||
locale_names = dict(settings.LANGUAGES)
|
||||
self.fields['invoice_language'].choices = [('__user__', _('The user\'s language'))] + [(a, locale_names[a]) for a in event.settings.locales]
|
||||
self.fields['invoice_generate_sales_channels'].choices = (
|
||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
||||
(c.identifier, c.label) for c in event.organizer.sales_channels.all()
|
||||
)
|
||||
self.fields['invoice_numbers_counter_length'].validators.append(MaxValueValidator(15))
|
||||
|
||||
@@ -961,7 +954,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
||||
]
|
||||
|
||||
mail_sales_channel_placed_paid = forms.MultipleChoiceField(
|
||||
choices=lambda: [(ident, sc.verbose_name) for ident, sc in get_all_sales_channels().items()],
|
||||
choices=[],
|
||||
label=_('Sales channels for checkout emails'),
|
||||
help_text=_('The order placed and paid emails will only be send to orders from these sales channels. '
|
||||
'The online shop must be enabled.'),
|
||||
@@ -972,7 +965,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
||||
)
|
||||
|
||||
mail_sales_channel_download_reminder = forms.MultipleChoiceField(
|
||||
choices=lambda: [(ident, sc.verbose_name) for ident, sc in get_all_sales_channels().items()],
|
||||
choices=[],
|
||||
label=_('Sales channels'),
|
||||
help_text=_('This email will only be send to orders from these sales channels. The online shop must be enabled.'),
|
||||
widget=forms.CheckboxSelectMultiple(
|
||||
@@ -1367,6 +1360,12 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
||||
self.fields['mail_html_renderer'].choices = [
|
||||
(r.identifier, r.verbose_name) for r in event.get_html_mail_renderers().values()
|
||||
]
|
||||
self.fields['mail_sales_channel_placed_paid'].choices = (
|
||||
(c.identifier, c.label) for c in event.organizer.sales_channels.all()
|
||||
)
|
||||
self.fields['mail_sales_channel_download_reminder'].choices = (
|
||||
(c.identifier, c.label) for c in event.organizer.sales_channels.all()
|
||||
)
|
||||
|
||||
prefetch_related_objects([self.event.organizer], Prefetch('meta_properties'))
|
||||
self.event.meta_values_cached = self.event.meta_values.select_related('property').all()
|
||||
|
||||
@@ -50,15 +50,14 @@ from django.utils.timezone import get_current_timezone, make_aware, now
|
||||
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes.forms import SafeModelChoiceField
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.forms.widgets import (
|
||||
DatePickerWidget, SplitDateTimePickerWidget, TimePickerWidget,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
Checkin, CheckinList, Device, Event, EventMetaProperty, EventMetaValue,
|
||||
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
|
||||
OrderRefund, Organizer, Question, QuestionAnswer, Quota, SubEvent,
|
||||
SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
|
||||
OrderRefund, Organizer, Question, QuestionAnswer, Quota, SalesChannel,
|
||||
SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
|
||||
)
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
||||
@@ -579,9 +578,11 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
||||
required=False,
|
||||
label=_('Maximal sum of payments and refunds'),
|
||||
)
|
||||
sales_channel = forms.ChoiceField(
|
||||
sales_channel = SafeModelChoiceField(
|
||||
label=_('Sales channel'),
|
||||
required=False,
|
||||
queryset=SalesChannel.objects.none(),
|
||||
to_field_name="identifier",
|
||||
)
|
||||
has_checkin = forms.NullBooleanField(
|
||||
required=False,
|
||||
@@ -604,9 +605,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
||||
del self.fields['subevents_from']
|
||||
del self.fields['subevents_to']
|
||||
|
||||
self.fields['sales_channel'].choices = [('', '')] + [
|
||||
(k, v.verbose_name) for k, v in get_all_sales_channels().items()
|
||||
]
|
||||
self.fields['sales_channel'].queryset = self.event.organizer.sales_channels.all()
|
||||
|
||||
locale_names = dict(settings.LANGUAGES)
|
||||
self.fields['locale'].choices = [('', '')] + [(a, locale_names[a]) for a in self.event.settings.locales]
|
||||
@@ -719,7 +718,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
||||
if fdata.get('comment'):
|
||||
qs = qs.filter(comment__icontains=fdata.get('comment'))
|
||||
if fdata.get('sales_channel'):
|
||||
qs = qs.filter(sales_channel=fdata.get('sales_channel'))
|
||||
qs = qs.filter(sales_channel__identifier=fdata.get('sales_channel').identifier)
|
||||
if fdata.get('total'):
|
||||
qs = qs.filter(total=fdata.get('total'))
|
||||
if fdata.get('email_known_to_work') is not None:
|
||||
|
||||
@@ -55,7 +55,6 @@ from django_scopes.forms import (
|
||||
)
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.forms import I18nFormSet, I18nMarkdownTextarea, I18nModelForm
|
||||
from pretix.base.forms.widgets import DatePickerWidget
|
||||
from pretix.base.models import (
|
||||
@@ -64,7 +63,8 @@ from pretix.base.models import (
|
||||
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
|
||||
from pretix.base.signals import item_copy_data
|
||||
from pretix.control.forms import (
|
||||
ButtonGroupRadioSelect, ItemMultipleChoiceField, SizeValidationMixin,
|
||||
ButtonGroupRadioSelect, ItemMultipleChoiceField,
|
||||
SalesChannelCheckboxSelectMultiple, SizeValidationMixin,
|
||||
SplitDateTimeField, SplitDateTimePickerWidget,
|
||||
)
|
||||
from pretix.control.forms.widgets import Select2, Select2ItemVarMulti
|
||||
@@ -413,7 +413,7 @@ class ItemCreateForm(I18nModelForm):
|
||||
'checkin_text',
|
||||
'free_price',
|
||||
'original_price',
|
||||
'sales_channels',
|
||||
'all_sales_channels',
|
||||
'issue_giftcard',
|
||||
'require_approval',
|
||||
'allow_waitinglist',
|
||||
@@ -443,9 +443,6 @@ class ItemCreateForm(I18nModelForm):
|
||||
|
||||
if src.picture:
|
||||
self.instance.picture.save(os.path.basename(src.picture.name), src.picture)
|
||||
else:
|
||||
# Add to all sales channels by default
|
||||
self.instance.sales_channels = list(get_all_sales_channels().keys())
|
||||
|
||||
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
|
||||
if not self.instance.admission:
|
||||
@@ -474,6 +471,8 @@ class ItemCreateForm(I18nModelForm):
|
||||
})
|
||||
|
||||
if self.cleaned_data.get('copy_from'):
|
||||
if not self.instance.all_sales_channels:
|
||||
self.instance.limit_sales_channels.set(self.cleaned_data['copy_from'].limit_sales_channels.all())
|
||||
self.instance.require_membership_types.set(
|
||||
self.cleaned_data['copy_from'].require_membership_types.all()
|
||||
)
|
||||
@@ -574,14 +573,10 @@ class ItemUpdateForm(I18nModelForm):
|
||||
if self.event.tax_rules.exists():
|
||||
self.fields['tax_rule'].required = True
|
||||
self.fields['description'].widget.attrs['rows'] = '4'
|
||||
self.fields['sales_channels'] = forms.MultipleChoiceField(
|
||||
label=_('Sales channels'),
|
||||
required=False,
|
||||
choices=(
|
||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
||||
),
|
||||
widget=forms.CheckboxSelectMultiple
|
||||
)
|
||||
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
|
||||
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
|
||||
'data-inverse-dependency': '<[name$=all_sales_channels]',
|
||||
}, choices=self.fields['limit_sales_channels'].widget.choices)
|
||||
change_decimal_field(self.fields['default_price'], self.event.currency)
|
||||
|
||||
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
|
||||
@@ -772,7 +767,8 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'name',
|
||||
'internal_name',
|
||||
'active',
|
||||
'sales_channels',
|
||||
'all_sales_channels',
|
||||
'limit_sales_channels',
|
||||
'admission',
|
||||
'personalized',
|
||||
'description',
|
||||
@@ -829,6 +825,7 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'hidden_if_item_available': SafeModelChoiceField,
|
||||
'grant_membership_type': SafeModelChoiceField,
|
||||
'require_membership_types': SafeModelMultipleChoiceField,
|
||||
'limit_sales_channels': SafeModelMultipleChoiceField,
|
||||
}
|
||||
widgets = {
|
||||
'available_from': SplitDateTimePickerWidget(),
|
||||
@@ -900,18 +897,10 @@ class ItemVariationForm(I18nModelForm):
|
||||
qs = kwargs.pop('membership_types')
|
||||
super().__init__(*args, **kwargs)
|
||||
change_decimal_field(self.fields['default_price'], self.event.currency)
|
||||
self.fields['sales_channels'] = forms.MultipleChoiceField(
|
||||
label=_('Sales channels'),
|
||||
required=False,
|
||||
choices=(
|
||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
||||
),
|
||||
help_text=_('The sales channel selection for the product as a whole takes precedence, so if a sales channel is '
|
||||
'selected here but not on product level, the variation will not be available.'),
|
||||
widget=forms.CheckboxSelectMultiple
|
||||
)
|
||||
if not self.instance.pk:
|
||||
self.initial.setdefault('sales_channels', list(get_all_sales_channels().keys()))
|
||||
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
|
||||
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
|
||||
'data-inverse-dependency': '<[name$=all_sales_channels]',
|
||||
}, choices=self.fields['limit_sales_channels'].widget.choices)
|
||||
|
||||
self.fields['description'].widget.attrs['rows'] = 3
|
||||
if qs:
|
||||
@@ -983,12 +972,14 @@ class ItemVariationForm(I18nModelForm):
|
||||
'available_from_mode',
|
||||
'available_until',
|
||||
'available_until_mode',
|
||||
'sales_channels',
|
||||
'all_sales_channels',
|
||||
'limit_sales_channels',
|
||||
'hide_without_voucher',
|
||||
]
|
||||
field_classes = {
|
||||
'available_from': SplitDateTimeField,
|
||||
'available_until': SplitDateTimeField,
|
||||
'limit_sales_channels': SafeModelMultipleChoiceField,
|
||||
}
|
||||
widgets = {
|
||||
'available_from': SplitDateTimePickerWidget(),
|
||||
|
||||
@@ -50,6 +50,7 @@ from django_scopes.forms import SafeModelChoiceField
|
||||
from i18nfield.forms import (
|
||||
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
|
||||
)
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
from pytz import common_timezones
|
||||
|
||||
@@ -68,7 +69,8 @@ from pretix.base.forms.widgets import (
|
||||
)
|
||||
from pretix.base.models import (
|
||||
Customer, Device, EventMetaProperty, Gate, GiftCard, GiftCardAcceptance,
|
||||
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium, Team,
|
||||
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
|
||||
SalesChannel, Team,
|
||||
)
|
||||
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
||||
from pretix.base.models.organizer import OrganizerFooterLink
|
||||
@@ -1090,3 +1092,40 @@ class GiftCardAcceptanceInviteForm(forms.Form):
|
||||
if self.organizer.gift_card_acceptor_acceptance.filter(acceptor=acceptor).exists():
|
||||
raise ValidationError(_('The selected organizer has already been invited.'))
|
||||
return acceptor
|
||||
|
||||
|
||||
class SalesChannelForm(I18nModelForm):
|
||||
class Meta:
|
||||
model = SalesChannel
|
||||
fields = ['label', 'identifier']
|
||||
widgets = {
|
||||
'default': forms.TextInput(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.type = kwargs.pop("type")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if not self.type.multiple_allowed or (self.instance and self.instance.pk):
|
||||
self.fields["identifier"].initial = self.type.identifier
|
||||
self.fields["identifier"].disabled = True
|
||||
self.fields["label"].initial = LazyI18nString.from_gettext(self.type.verbose_name)
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
|
||||
if self.instance.pk:
|
||||
d["identifier"] = self.instance.identifier
|
||||
elif self.type.multiple_allowed:
|
||||
d["identifier"] = self.type.identifier + "." + d["identifier"]
|
||||
else:
|
||||
d["identifier"] = self.type.identifier
|
||||
|
||||
if not self.instance.pk:
|
||||
# self.event is actually the organizer, sorry I18nModelForm!
|
||||
if self.event.sales_channels.filter(identifier=d["identifier"]).exists():
|
||||
raise ValidationError(
|
||||
_("A sales channel with the same identifier already exists.")
|
||||
)
|
||||
|
||||
return d
|
||||
|
||||
@@ -357,6 +357,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'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.saleschannel.created': _('The sales channel has been created.'),
|
||||
'pretix.saleschannel.changed': _('The sales channel has been changed.'),
|
||||
'pretix.saleschannel.deleted': _('The sales channel 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.'),
|
||||
|
||||
@@ -502,6 +502,13 @@ def get_organizer_navigation(request):
|
||||
}),
|
||||
'active': url.url_name == 'organizer.settings.mail',
|
||||
},
|
||||
{
|
||||
'label': _('Sales channels'),
|
||||
'url': reverse('control:organizer.channels', kwargs={
|
||||
'organizer': request.organizer.slug
|
||||
}),
|
||||
'active': url.url_name.startswith('organizer.channel'),
|
||||
},
|
||||
{
|
||||
'label': _('Webhooks'),
|
||||
'url': reverse('control:organizer.webhooks', kwargs={
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
{% load urlreplace %}
|
||||
{% block title %}{% trans "Check-in lists" %}{% endblock %}
|
||||
{% block inside %}
|
||||
@@ -137,9 +138,14 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<td>
|
||||
{% for channel in cl.auto_checkin_sales_channels %}
|
||||
<span class="fa fa-{{ channel.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{% trans channel.verbose_name %}"></span>
|
||||
{% for channel in cl.auto_checkin_sales_channels.all %}
|
||||
{% if "." in channel.icon %}
|
||||
<img src="{% static channel.icon %}" class="fa-like-image"
|
||||
data-toggle="tooltip" title="{{ channel.label }}">
|
||||
{% else %}
|
||||
<span class="fa fa-{{ channel.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{{ channel.label }}"></span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "pretixcontrol/event/settings_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Payment settings" %}</h1>
|
||||
@@ -30,8 +31,13 @@
|
||||
</td>
|
||||
<td class="iconcol">
|
||||
{% for channel in provider.sales_channels %}
|
||||
<span class="fa fa-{{ channel.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{% trans channel.verbose_name %}"></span>
|
||||
{% if "." in channel.icon %}
|
||||
<img src="{% static channel.icon %}" class="fa-like-image"
|
||||
data-toggle="tooltip" title="{{ channel.label }}">
|
||||
{% else %}
|
||||
<span class="fa fa-{{ channel.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{{ channel.label }}"></span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
{% bootstrap_field sform.contact_mail layout="control" %}
|
||||
{% bootstrap_field sform.imprint_url layout="control" %}
|
||||
{% bootstrap_field form.is_public layout="control" %}
|
||||
{% bootstrap_field form.sales_channels layout="control" %}
|
||||
{% bootstrap_field form.all_sales_channels layout="control" %}
|
||||
{% bootstrap_field form.limit_sales_channels layout="control" %}
|
||||
|
||||
{% if meta_forms %}
|
||||
<div class="form-group metadata-group">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load formset_tags %}
|
||||
{% load static %}
|
||||
{% load getitem %}
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}" id="item_variations">
|
||||
{{ formset.management_form }}
|
||||
@@ -40,9 +41,14 @@
|
||||
title="{% trans "Require a valid membership" %}"></span>
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6">
|
||||
{% for k, c in sales_channels.items %}
|
||||
<span class="fa fa-fw fa-{{ c.icon }} text-muted variation-channel-{{ k }} variation-icon-hidden"
|
||||
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
|
||||
{% for c in sales_channels %}
|
||||
{% if "." in c.icon %}
|
||||
<img src="{% static c.icon %}" class="fa-like-image variation-channel-{{ c.id }} variation-icon-hidden"
|
||||
data-toggle="tooltip" title="{{ c.label }}">
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-{{ c.icon }} text-muted variation-channel-{{ c.id }} variation-icon-hidden"
|
||||
data-toggle="tooltip" title="{{ c.label }}"></span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="col-md-1 col-xs-6 text-right flip variation-price">
|
||||
@@ -97,7 +103,8 @@
|
||||
{% endif %}
|
||||
{% bootstrap_field form.available_from visibility_field=form.available_from_mode layout="control_with_visibility" %}
|
||||
{% bootstrap_field form.available_until visibility_field=form.available_until_mode layout="control_with_visibility" %}
|
||||
{% bootstrap_field form.sales_channels layout="control" %}
|
||||
{% bootstrap_field form.all_sales_channels layout="control" %}
|
||||
{% bootstrap_field form.limit_sales_channels layout="control" %}
|
||||
{% bootstrap_field form.hide_without_voucher layout="control" %}
|
||||
{% bootstrap_field form.require_approval layout="control" %}
|
||||
{% if form.require_membership %}
|
||||
@@ -148,9 +155,14 @@
|
||||
title="{% trans "Require a valid membership" %}"></span>
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6">
|
||||
{% for k, c in sales_channels.items %}
|
||||
<span class="fa fa-fw fa-{{ c.icon }} text-muted variation-channel-{{ k }} variation-icon-hidden"
|
||||
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
|
||||
{% for c in sales_channels %}
|
||||
{% if "." in c.icon %}
|
||||
<img src="{% static c.icon %}" class="fa-like-image variation-channel-{{ c.id }} variation-icon-hidden"
|
||||
data-toggle="tooltip" title="{{ c.label }}">
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-{{ c.icon }} text-muted variation-channel-{{ c.id }} variation-icon-hidden"
|
||||
data-toggle="tooltip" title="{{ c.label }}"></span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="col-md-1 col-xs-6 text-right flip variation-price">
|
||||
@@ -197,7 +209,8 @@
|
||||
{% bootstrap_field formset.empty_form.available_from visibility_field=formset.empty_form.available_from_mode layout="control_with_visibility" %}
|
||||
{% bootstrap_field formset.empty_form.available_until visibility_field=formset.empty_form.available_until_mode layout="control_with_visibility" %}
|
||||
{% bootstrap_field formset.empty_form.available_until layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.sales_channels layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.all_sales_channels layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.limit_sales_channels layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.hide_without_voucher layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.require_approval layout="control" %}
|
||||
{% if formset.empty_form.require_membership %}
|
||||
|
||||
@@ -152,9 +152,8 @@
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Availability" %}</legend>
|
||||
|
||||
|
||||
{% bootstrap_field form.sales_channels layout="control" horizontal_field_class="col-md-7" %}
|
||||
{% bootstrap_field form.all_sales_channels layout="control" horizontal_field_class="col-md-7" %}
|
||||
{% bootstrap_field form.limit_sales_channels layout="control" horizontal_field_class="col-md-7" %}
|
||||
{% bootstrap_field form.available_from visibility_field=form.available_from_mode layout="control_with_visibility" %}
|
||||
{% bootstrap_field form.available_until visibility_field=form.available_until_mode layout="control_with_visibility" %}
|
||||
{% bootstrap_field form.max_per_order layout="control" horizontal_field_class="col-md-7" %}
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
{% bootstrap_field form.internal_name layout="control" %}
|
||||
{% bootstrap_field form.available_from layout="control" %}
|
||||
{% bootstrap_field form.available_until layout="control" %}
|
||||
{% bootstrap_field form.sales_channels layout="control" %}
|
||||
{% bootstrap_field form.all_sales_channels layout="control" %}
|
||||
{% bootstrap_field form.limit_sales_channels layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Condition" context "discount" %}</legend>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Automatic discounts" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Automatic discounts" %}</h1>
|
||||
@@ -78,10 +79,15 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% for k, c in sales_channels.items %}
|
||||
{% if k in d.sales_channels %}
|
||||
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
|
||||
{% for c in sales_channels %}
|
||||
{% if d.all_sales_channels or c in d.limit_sales_channels.all %}
|
||||
{% if "." in c.icon %}
|
||||
<img src="{% static c.icon %}" class="fa-like-image"
|
||||
data-toggle="tooltip" title="{{ c.label }}">
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{{ c.label }}"></span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load money %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Products" %}{% endblock %}
|
||||
{% block inside %}
|
||||
{% blocktrans asvar s_taxes %}taxes{% endblocktrans %}
|
||||
@@ -66,10 +67,15 @@
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
#{{ i.pk }}
|
||||
{% for k, c in sales_channels.items %}
|
||||
{% if k in i.sales_channels %}
|
||||
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
|
||||
{% for c in sales_channels %}
|
||||
{% if i.all_sales_channels or c in i.limit_sales_channels.all %}
|
||||
{% if "." in c.icon %}
|
||||
<img src="{% static c.icon %}" class="fa-like-image"
|
||||
data-toggle="tooltip" title="{{ c.label }}">
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{{ c.label }}"></span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -185,9 +185,9 @@
|
||||
<dt>{% trans "Cancellation date" %}</dt>
|
||||
<dd>{{ order.cancellation_date|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
{% endif %}
|
||||
{% if sales_channel %}
|
||||
{% if order.sales_channel %}
|
||||
<dt>{% trans "Sales channel" %}</dt>
|
||||
<dd>{{ sales_channel.verbose_name }}</dd>
|
||||
<dd>{{ order.sales_channel.label }}</dd>
|
||||
{% endif %}
|
||||
<dt>{% trans "Order locale" %}</dt>
|
||||
<dd>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{% load urlreplace %}
|
||||
{% load money %}
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Orders" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Orders" %}</h1>
|
||||
@@ -201,8 +202,13 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="fa fa-fw fa-{{ o.sales_channel_obj.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{% trans o.sales_channel_obj.verbose_name %}"></span>
|
||||
{% if "." in o.sales_channel.icon %}
|
||||
<img src="{% static o.sales_channel.icon %}" class="fa-like-image"
|
||||
data-toggle="tooltip" title="{{ o.sales_channel.label }}">
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-{{ o.sales_channel.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{{ o.sales_channel.label }}"></span>
|
||||
{% endif %}
|
||||
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load formset_tags %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<h1>{% trans "Add sales channel" %}</h1>
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form layout="control" %}
|
||||
|
||||
{% bootstrap_field form.label layout="control" %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_identifier">{% trans "Channel type" %}</label>
|
||||
<div class="col-md-9">
|
||||
<input type="text" value="{{ type.verbose_name }}" class="form-control" disabled>
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.identifier addon_before=identifier_prefix 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,26 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<h1>{% trans "Add sales channel" %}</h1>
|
||||
<div class="list-group large-link-group">
|
||||
{% for t in types %}
|
||||
<a class="list-group-item" href="?type={{ t.identifier }}">
|
||||
<h4>
|
||||
{% if "." in t.icon %}
|
||||
<img class="fa-like-image" src="{% static t.icon %}" alt="">
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-{{ t.icon }} text-muted"></span>
|
||||
{% endif %}
|
||||
{{ t.verbose_name }}
|
||||
</h4>
|
||||
{% if t.description %}
|
||||
<p>
|
||||
{{ t.description }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<h1>{% trans "Delete sales channel:" %} {{ channel.label }}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% if is_allowed %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Are you sure you want to delete this sales channel?
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
<div class="alert alert-danger">
|
||||
{% blocktrans trimmed %}
|
||||
This sales channel cannot be deleted since it has already been used to sell orders or because it is
|
||||
a core element of the system.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:organizer.channels" 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,27 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load formset_tags %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<h1>{% trans "Sales channel:" %} {{ channel.label }}</h1>
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form layout="control" %}
|
||||
|
||||
{% bootstrap_field form.label layout="control" %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_identifier">{% trans "Channel type" %}</label>
|
||||
<div class="col-md-9">
|
||||
<input type="text" value="{{ type.verbose_name }}" class="form-control" disabled>
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.identifier 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,64 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
{% block inner %}
|
||||
<h1>{% trans "Sales channels" %}</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
On this page, you can manage the different channels your tickets can be sold through. This is useful
|
||||
to unlock new revenue streams or to separate revenue between different sources for reporting purchases.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<a href="{% url "control:organizer.channel.add" organizer=request.organizer.slug %}" class="btn btn-default">
|
||||
<span class="fa fa-plus"></span>
|
||||
{% trans "Add a new channel" %}
|
||||
</a>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Channel" %}</th>
|
||||
<th>{% trans "Identifier" %}</th>
|
||||
<th>{% trans "Channel type" %}</th>
|
||||
<th class="action-col-2"></th>
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-dnd-url="{% url "control:organizer.channels.reorder" organizer=request.organizer.slug %}">
|
||||
{% for c in channels %}
|
||||
<tr data-dnd-id="{{ c.pk }}">
|
||||
<td><strong>
|
||||
<a href="{% url "control:organizer.channel.edit" organizer=request.organizer.slug channel=c.identifier %}">
|
||||
{{ c.label }}
|
||||
</a>
|
||||
</strong></td>
|
||||
<td>
|
||||
<code>{{ c.identifier }}</code>
|
||||
</td>
|
||||
<td>
|
||||
{% if "." in c.type_instance.icon %}
|
||||
<img class="fa-like-image" src="{% static c.icon %}" alt="">
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-{{ c.type_instance.icon }} text-muted"></span>
|
||||
{% endif %}
|
||||
{{ c.type_instance.verbose_name }}
|
||||
</td>
|
||||
<td>
|
||||
<button formaction="{% url "control:organizer.channel.up" organizer=request.organizer.slug channel=c.identifier %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
|
||||
<button formaction="{% url "control:organizer.channel.down" organizer=request.organizer.slug channel=c.identifier %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
|
||||
<span class="dnd-container"></span>
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% url "control:organizer.channel.edit" organizer=request.organizer.slug channel=c.identifier %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:organizer.channel.delete" organizer=request.organizer.slug channel=c.identifier %}"
|
||||
class="btn btn-danger btn-sm {% if c.type_instance.default_created %}disabled{% endif %}"><i class="fa fa-trash"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -2,6 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% load static %}
|
||||
{% block title %}
|
||||
{% blocktrans trimmed with id=customer.identifier %}
|
||||
Customer #{{ id }}
|
||||
@@ -225,8 +226,13 @@
|
||||
{{ 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>
|
||||
{% if "." in o.sales_channel.icon %}
|
||||
<img src="{% static o.sales_channel.icon %}" class="fa-like-image"
|
||||
data-toggle="tooltip" title="{{ o.sales_channel.label }}">
|
||||
{% else %}
|
||||
<span class="fa fa-{{ o.sales_channel.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{{ o.sales_channel.label }}"></span>
|
||||
{% endif %}
|
||||
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% if o.customer_id != customer.pk %}
|
||||
<span class="fa fa-link text-muted"
|
||||
|
||||
@@ -136,6 +136,19 @@ urlpatterns = [
|
||||
name='organizer.property.down'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/property/reorder$', organizer.reorder_meta_properties,
|
||||
name='organizer.properties.reorder'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/channels$', organizer.ChannelListView.as_view(), name='organizer.channels'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/channel/add$', organizer.ChannelCreateView.as_view(),
|
||||
name='organizer.channel.add'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/channel/(?P<channel>[^/]+)/edit$', organizer.ChannelUpdateView.as_view(),
|
||||
name='organizer.channel.edit'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/channel/(?P<channel>[^/]+)/delete$', organizer.ChannelDeleteView.as_view(),
|
||||
name='organizer.channel.delete'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/channel/(?P<channel>[^/]+)/up$', organizer.channel_move_up,
|
||||
name='organizer.channel.up'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/channel/(?P<channel>[^/]+)/down$', organizer.channel_move_down,
|
||||
name='organizer.channel.down'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/channel/reorder$', organizer.reorder_channels,
|
||||
name='organizer.channels.reorder'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/membershiptypes$', organizer.MembershipTypeListView.as_view(), name='organizer.membershiptypes'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/membershiptype/add$', organizer.MembershipTypeCreateView.as_view(),
|
||||
name='organizer.membershiptype.add'),
|
||||
|
||||
@@ -49,7 +49,6 @@ from django.views.generic import FormView, ListView, TemplateView
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.api.views.checkin import _redeem_process
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.models import Checkin, Order, OrderPosition
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
from pretix.base.services.checkin import (
|
||||
@@ -296,7 +295,9 @@ class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
ordering = ('subevent__date_from', 'name', 'pk')
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.checkin_lists.select_related('subevent').prefetch_related("limit_products")
|
||||
qs = self.request.event.checkin_lists.select_related('subevent').prefetch_related(
|
||||
"limit_products", "auto_checkin_sales_channels"
|
||||
)
|
||||
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
@@ -305,12 +306,10 @@ class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
clists = list(ctx['checkinlists'])
|
||||
sales_channels = get_all_sales_channels()
|
||||
|
||||
for cl in clists:
|
||||
if cl.subevent:
|
||||
cl.subevent.event = self.request.event # re-use same event object to make sure settings are cached
|
||||
cl.auto_checkin_sales_channels = [sales_channels[channel] for channel in cl.auto_checkin_sales_channels]
|
||||
ctx['checkinlists'] = clists
|
||||
|
||||
ctx['can_change_organizer_settings'] = self.request.user.has_organizer_permission(
|
||||
|
||||
@@ -43,7 +43,6 @@ from pretix.control.permissions import (
|
||||
)
|
||||
from pretix.helpers.models import modelcopy
|
||||
|
||||
from ...base.channels import get_all_sales_channels
|
||||
from ...helpers.compat import CompatDeleteView
|
||||
from . import CreateView, PaginationMixin, UpdateView
|
||||
|
||||
@@ -190,11 +189,14 @@ class DiscountList(PaginationMixin, ListView):
|
||||
template_name = 'pretixcontrol/items/discounts.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.discounts.prefetch_related('condition_limit_products')
|
||||
return self.request.event.discounts.prefetch_related('condition_limit_products', 'limit_sales_channels')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['sales_channels'] = get_all_sales_channels()
|
||||
ctx['sales_channels'] = [
|
||||
c for c in self.request.organizer.sales_channels.all()
|
||||
if c.type_instance.discounts_supported
|
||||
]
|
||||
return ctx
|
||||
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ from django.views.generic.detail import SingleObjectMixin
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from i18nfield.utils import I18nJSONEncoder
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.email import get_available_placeholders
|
||||
from pretix.base.forms import PlaceholderValidator
|
||||
from pretix.base.models import Event, LogEntry, Order, TaxRule, Voucher
|
||||
@@ -559,7 +558,7 @@ class PaymentSettings(EventSettingsViewMixin, EventSettingsFormView):
|
||||
key=lambda s: s.verbose_name
|
||||
)
|
||||
|
||||
sales_channels = get_all_sales_channels()
|
||||
sales_channels = {s.identifier: s for s in self.request.organizer.sales_channels.all()}
|
||||
for p in context['providers']:
|
||||
p.show_enabled = p.is_enabled
|
||||
p.sales_channels = [sales_channels[channel] for channel in p.settings.get('_restrict_to_sales_channels', as_type=list, default=['web'])]
|
||||
@@ -1468,7 +1467,7 @@ class QuickSetupView(FormView):
|
||||
admission=True,
|
||||
personalized=True,
|
||||
position=i,
|
||||
sales_channels=list(get_all_sales_channels().keys())
|
||||
all_sales_channels=True,
|
||||
)
|
||||
item.log_action('pretix.event.item.added', user=self.request.user, data=dict(f.cleaned_data))
|
||||
if f.cleaned_data['quota'] or not form.cleaned_data['total_quota']:
|
||||
|
||||
@@ -84,7 +84,6 @@ from pretix.control.permissions import (
|
||||
from pretix.control.signals import item_forms, item_formsets
|
||||
from pretix.helpers.models import modelcopy
|
||||
|
||||
from ...base.channels import get_all_sales_channels
|
||||
from ...helpers.compat import CompatDeleteView
|
||||
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
|
||||
|
||||
@@ -106,14 +105,14 @@ class ItemList(ListView):
|
||||
event=self.request.event
|
||||
).annotate(
|
||||
var_count=Count('variations')
|
||||
).prefetch_related("category").order_by(
|
||||
).prefetch_related("category", "limit_sales_channels").order_by(
|
||||
F('category__position').asc(nulls_first=True),
|
||||
'category', 'position'
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['sales_channels'] = get_all_sales_channels()
|
||||
ctx['sales_channels'] = self.request.organizer.sales_channels.all()
|
||||
items_by_category = {cat: list(items) for cat, items in groupby(ctx['items'], lambda item: item.category)}
|
||||
ctx['cat_list'] = [(cat, items_by_category.get(cat, [])) for cat in [None, *self.request.event.categories.all()]]
|
||||
return ctx
|
||||
@@ -1503,7 +1502,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
|
||||
"Your participants won't be able to buy the bundle unless you remove this "
|
||||
"item from it."))
|
||||
|
||||
ctx['sales_channels'] = get_all_sales_channels()
|
||||
ctx['sales_channels'] = self.request.organizer.sales_channels.all()
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
@@ -1515,7 +1514,9 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
|
||||
can_order=True, can_delete=True, extra=0
|
||||
)(
|
||||
self.request.POST if self.request.method == "POST" else None,
|
||||
queryset=ItemVariation.objects.filter(item=self.get_object()).prefetch_related('meta_values', 'require_membership_types'),
|
||||
queryset=ItemVariation.objects.filter(item=self.get_object()).prefetch_related(
|
||||
'meta_values', 'limit_sales_channels', 'require_membership_types'
|
||||
),
|
||||
event=self.request.event, prefix="variations"
|
||||
)),
|
||||
('addons', inlineformset_factory(
|
||||
|
||||
@@ -71,7 +71,6 @@ from django.views.generic import (
|
||||
)
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.exporter import MultiSheetListExporter
|
||||
@@ -375,7 +374,7 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
|
||||
def get_queryset(self):
|
||||
qs = Order.objects.filter(
|
||||
event=self.request.event
|
||||
).select_related('invoice_address')
|
||||
).select_related('invoice_address').prefetch_related("sales_channel")
|
||||
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
@@ -420,7 +419,6 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
|
||||
)
|
||||
}
|
||||
|
||||
scs = get_all_sales_channels()
|
||||
for o in ctx['orders']:
|
||||
if o.pk not in annotated:
|
||||
continue
|
||||
@@ -433,7 +431,6 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
|
||||
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]
|
||||
|
||||
if ctx['page_obj'].paginator.count < 1000:
|
||||
# Performance safeguard: Only count positions if the data set is small
|
||||
@@ -520,7 +517,6 @@ class OrderDetail(OrderView):
|
||||
ctx['display_locale'] = dict(settings.LANGUAGES)[self.object.locale or self.request.event.settings.locale]
|
||||
|
||||
ctx['overpaid'] = self.order.pending_sum * -1
|
||||
ctx['sales_channel'] = get_all_sales_channels().get(self.order.sales_channel)
|
||||
ctx['download_buttons'] = self.download_buttons
|
||||
ctx['payment_refund_sum'] = self.order.payment_refund_sum
|
||||
ctx['pending_sum'] = self.order.pending_sum
|
||||
|
||||
@@ -56,7 +56,7 @@ from django.forms import DecimalField
|
||||
from django.http import (
|
||||
Http404, HttpResponse, HttpResponseBadRequest, JsonResponse,
|
||||
)
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import cached_property
|
||||
@@ -71,7 +71,7 @@ from django.views.generic import (
|
||||
from pretix.api.models import ApiCall, WebHook
|
||||
from pretix.api.webhooks import manually_retry_all_calls
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.channels import get_all_sales_channel_types
|
||||
from pretix.base.exporter import (
|
||||
MultiSheetListExporter, OrganizerLevelExportMixin,
|
||||
)
|
||||
@@ -87,7 +87,7 @@ from pretix.base.models.giftcards import (
|
||||
GiftCardAcceptance, GiftCardTransaction, gen_giftcard_secret,
|
||||
)
|
||||
from pretix.base.models.orders import CancellationRequest
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.models.organizer import SalesChannel, TeamAPIToken
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.services.export import multiexport, scheduled_organizer_export
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
@@ -107,8 +107,8 @@ from pretix.control.forms.organizer import (
|
||||
MailSettingsForm, MembershipTypeForm, MembershipUpdateForm,
|
||||
OrganizerDeleteForm, OrganizerFooterLinkFormset, OrganizerForm,
|
||||
OrganizerSettingsForm, OrganizerUpdateForm, ReusableMediumCreateForm,
|
||||
ReusableMediumUpdateForm, SSOClientForm, SSOProviderForm, TeamForm,
|
||||
WebHookForm,
|
||||
ReusableMediumUpdateForm, SalesChannelForm, SSOClientForm, SSOProviderForm,
|
||||
TeamForm, WebHookForm,
|
||||
)
|
||||
from pretix.control.forms.rrule import RRuleForm
|
||||
from pretix.control.logdisplay import OVERVIEW_BANLIST
|
||||
@@ -2206,7 +2206,7 @@ def meta_property_move_down(request, organizer, property):
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
@organizer_permission_required("can_change_items")
|
||||
@organizer_permission_required("can_change_organizer_settings")
|
||||
@require_http_methods(["POST"])
|
||||
def reorder_meta_properties(request, organizer):
|
||||
try:
|
||||
@@ -2643,7 +2643,7 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
||||
q |= Q(email__iexact=self.customer.email)
|
||||
qs = Order.objects.filter(
|
||||
q
|
||||
).select_related('event').order_by('-datetime', 'pk')
|
||||
).select_related('event').prefetch_related('sales_channel').order_by('-datetime', 'pk')
|
||||
return qs
|
||||
|
||||
@cached_property
|
||||
@@ -2720,7 +2720,6 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
||||
)
|
||||
}
|
||||
|
||||
scs = get_all_sales_channels()
|
||||
for o in ctx['orders']:
|
||||
if o.pk not in annotated:
|
||||
continue
|
||||
@@ -2733,7 +2732,6 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
||||
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]
|
||||
|
||||
ctx["lifetime_spending"] = (
|
||||
self.get_queryset()
|
||||
@@ -3040,3 +3038,242 @@ class ReusableMediumUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
|
||||
'organizer': self.request.organizer.slug,
|
||||
'pk': self.object.pk,
|
||||
})
|
||||
|
||||
|
||||
class ChannelListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = SalesChannel
|
||||
template_name = 'pretixcontrol/organizers/channels.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'channels'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.sales_channels.all()
|
||||
|
||||
|
||||
class ChannelEditorMixin:
|
||||
form_class = SalesChannelForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
return {
|
||||
**super().get_form_kwargs(),
|
||||
'event': self.request.organizer,
|
||||
}
|
||||
|
||||
|
||||
class ChannelCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ChannelEditorMixin, CreateView):
|
||||
model = SalesChannel
|
||||
permission = 'can_change_organizer_settings'
|
||||
template_name = 'pretixcontrol/organizers/channel_add.html'
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return SalesChannel()
|
||||
|
||||
@property
|
||||
def allowed_types(self):
|
||||
existing_types = set(self.request.organizer.sales_channels.values_list("type", flat=True))
|
||||
return {
|
||||
k: t for k, t in get_all_sales_channel_types().items()
|
||||
if t.multiple_allowed or t.identifier not in existing_types
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def selected_type(self):
|
||||
try:
|
||||
return self.allowed_types[self.request.GET.get("type")]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if not self.selected_type:
|
||||
return render(request, "pretixcontrol/organizers/channel_add_choice.html", {
|
||||
"types": self.allowed_types.values()
|
||||
})
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not self.selected_type:
|
||||
return render(request, "pretixcontrol/organizers/channel_add_choice.html", {
|
||||
"types": self.allowed_types.values()
|
||||
})
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.channels', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx["type"] = self.selected_type
|
||||
if self.selected_type.multiple_allowed:
|
||||
ctx["identifier_prefix"] = self.selected_type.identifier + "."
|
||||
return ctx
|
||||
|
||||
def get_form_kwargs(self):
|
||||
return {
|
||||
**super().get_form_kwargs(),
|
||||
"type": self.selected_type,
|
||||
}
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('The sales channel has been created.'))
|
||||
form.instance.organizer = self.request.organizer
|
||||
form.instance.type = self.selected_type.identifier
|
||||
form.instance.position = (self.request.organizer.sales_channels.aggregate(m=Max("position"))["m"] or 0) + 1
|
||||
ret = super().form_valid(form)
|
||||
form.instance.log_action('pretix.saleschannel.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 ChannelUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ChannelEditorMixin, UpdateView):
|
||||
model = SalesChannel
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'channel'
|
||||
template_name = 'pretixcontrol/organizers/channel_edit.html'
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(SalesChannel, organizer=self.request.organizer, identifier=self.kwargs.get('channel'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.channels', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
@cached_property
|
||||
def type(self):
|
||||
return get_all_sales_channel_types()[self.object.type]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx["type"] = self.type
|
||||
return ctx
|
||||
|
||||
def get_form_kwargs(self):
|
||||
return {
|
||||
**super().get_form_kwargs(),
|
||||
"type": self.type,
|
||||
}
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.has_changed() or self.formset.has_changed():
|
||||
self.object.log_action('pretix.saleschannel.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 ChannelDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
|
||||
model = SalesChannel
|
||||
template_name = 'pretixcontrol/organizers/channel_delete.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'channel'
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(SalesChannel, organizer=self.request.organizer, identifier=self.kwargs.get('channel'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.channels', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx["is_allowed"] = self.get_object().allow_delete
|
||||
return ctx
|
||||
|
||||
@transaction.atomic
|
||||
def delete(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
success_url = self.get_success_url()
|
||||
if not self.object.allow_delete():
|
||||
messages.error(self.request, _('This channel can not be deleted.'))
|
||||
return redirect(success_url)
|
||||
try:
|
||||
self.object.log_action('pretix.saleschannel.deleted', user=self.request.user)
|
||||
self.object.delete()
|
||||
messages.success(request, _('The selected sales channel has been deleted.'))
|
||||
except ProtectedError:
|
||||
messages.error(self.request, _('The channel could not be deleted as some constraints (e.g. data created by '
|
||||
'plug-ins) did not allow it.'))
|
||||
return redirect(success_url)
|
||||
|
||||
|
||||
def channel_move(request, channel, up=True):
|
||||
channel = get_object_or_404(request.organizer.sales_channels, identifier=channel)
|
||||
channels = list(request.organizer.sales_channels.order_by("position"))
|
||||
|
||||
index = channels.index(channel)
|
||||
if index != 0 and up:
|
||||
channels[index - 1], channels[index] = channels[index], channels[index - 1]
|
||||
elif index != len(channels) - 1 and not up:
|
||||
channels[index + 1], channels[index] = channels[index], channels[index + 1]
|
||||
|
||||
for i, prop in enumerate(channels):
|
||||
if prop.position != i:
|
||||
prop.position = i
|
||||
prop.save()
|
||||
prop.log_action(
|
||||
'pretix.saleschannel.reordered', user=request.user, data={
|
||||
'position': i,
|
||||
}
|
||||
)
|
||||
messages.success(request, _('The order of sales channels has been updated.'))
|
||||
|
||||
|
||||
@organizer_permission_required("can_change_organizer_settings")
|
||||
@require_http_methods(["POST"])
|
||||
def channel_move_up(request, organizer, channel):
|
||||
channel_move(request, channel, up=True)
|
||||
return redirect('control:organizer.channels',
|
||||
organizer=request.organizer.slug)
|
||||
|
||||
|
||||
@organizer_permission_required("can_change_organizer_settings")
|
||||
@require_http_methods(["POST"])
|
||||
def channel_move_down(request, organizer, channel):
|
||||
channel_move(request, channel, up=False)
|
||||
return redirect('control:organizer.channels',
|
||||
organizer=request.organizer.slug)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
@organizer_permission_required("can_change_organizer_settings")
|
||||
@require_http_methods(["POST"])
|
||||
def reorder_channels(request, organizer):
|
||||
try:
|
||||
ids = json.loads(request.body.decode('utf-8'))['ids']
|
||||
except (JSONDecodeError, KeyError, ValueError):
|
||||
return HttpResponseBadRequest("expected JSON: {ids:[]}")
|
||||
|
||||
input_channels = list(request.organizer.sales_channels.filter(id__in=[i for i in ids if i.isdigit()]))
|
||||
|
||||
if len(input_channels) != len(ids):
|
||||
raise Http404(_("Some of the provided object ids are invalid."))
|
||||
|
||||
if len(input_channels) != request.organizer.sales_channels.count():
|
||||
raise Http404(_("Not all objects have been selected."))
|
||||
|
||||
for c in input_channels:
|
||||
pos = ids.index(str(c.pk))
|
||||
if pos != c.position: # Save unneccessary UPDATE queries
|
||||
c.position = pos
|
||||
c.save(update_fields=['position'])
|
||||
c.log_action(
|
||||
'pretix.saleschannel.reordered', user=request.user, data={
|
||||
'position': pos,
|
||||
}
|
||||
)
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
@@ -98,6 +98,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
from pretix.base.models import Order
|
||||
order = self.request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
|
||||
email='sample@pretix.eu',
|
||||
sales_channel=self.request.event.organizer.sales_channels.get(identifier="web"),
|
||||
locale=self.request.event.settings.locale,
|
||||
expires=now(), code="PREVIEW1234", total=Decimal('119.00'))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user