forked from CGM_Public/pretix_original
Add sub-events and relative date settings (#503)
* Data model * little crud * SubEventItemForm etc * Drop SubEventItem.active, quota editor * Fix failing tests * First frontend stuff * Addons form stuff * Quota calculation * net price display on EventIndex * Add tests, solve some bugs * Correct quota selection in more places, consolidate pricing logic * Fix failing quota tests * Fix TypeError * Add tests for checkout * Fixed a bug in QuotaForm * Prevent immutable cart if a quota was removed from an item * Add tests for pricing * Handle waiting list * Filter in check-in list * Fixed import lost in rebase * Fix waiting list widget * Voucher management * Voucher redemption * Fix broken tests * Add subevents to OrderChangeManager * Create a subevent during event creation * Fix bulk voucher creation * Introduce subevent.active * Copy from for subevents * Show active in list * ICal download for subevents * Check start and end of presale * Failing tests / show cart logic * Test * Rebase migrations * REST API integration of sub-events * Integrate quota calculation into the traditional quota form * Make subevent argument to add_position optional * Log-display foo * pretixdroid and subevents * Filter by subevent * Add more tests * Some mor tests * Rebase fixes * More tests * Relative dates * Restrict selection in relative datetime widgets * Filter subevent list * Re-label has_subevents * Rebase fixes, subevents in calendar view * Performance and caching issues * Refactor calendar templates * Permission tests * Calendar fixes and month selection * subevent selection * Rename subevents to dates * Add tests for calendar views
This commit is contained in:
@@ -10,6 +10,7 @@ from pytz import common_timezones, timezone
|
||||
|
||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
|
||||
from pretix.base.models import Event, Organizer
|
||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||
from pretix.control.forms import ExtFileField
|
||||
|
||||
|
||||
@@ -20,6 +21,15 @@ class EventWizardFoundationForm(forms.Form):
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
help_text=_('Choose all languages that your event should be available in.')
|
||||
)
|
||||
has_subevents = forms.BooleanField(
|
||||
label=_("This is an event series"),
|
||||
help_text=_('Only recommended for advanced users. If this feature is enabled, this will not only be a '
|
||||
'single event but a series of very similar events that are handled within a single shop. '
|
||||
'The single events inside the series can only differ in date, time, location, prices and '
|
||||
'quotas, but not in other settings, and buying tickets across multiple of these events at '
|
||||
'the same time is possible. You cannot change this setting for this event later.'),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user = kwargs.pop('user')
|
||||
@@ -72,10 +82,15 @@ class EventWizardBasicsForm(I18nModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.organizer = kwargs.pop('organizer')
|
||||
self.locales = kwargs.get('locales')
|
||||
self.has_subevents = kwargs.pop('has_subevents')
|
||||
kwargs.pop('user')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.initial['timezone'] = get_current_timezone_name()
|
||||
self.fields['locale'].choices = [(a, b) for a, b in settings.LANGUAGES if a in self.locales]
|
||||
self.fields['location'].widget.attrs['rows'] = '3'
|
||||
if self.has_subevents:
|
||||
del self.fields['presale_start']
|
||||
del self.fields['presale_end']
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
@@ -125,11 +140,12 @@ class EventWizardCopyForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.pop('organizer')
|
||||
kwargs.pop('locales')
|
||||
has_subevents = kwargs.pop('has_subevents')
|
||||
self.user = kwargs.pop('user')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['copy_from_event'] = forms.ModelChoiceField(
|
||||
label=_("Copy configuration from"),
|
||||
queryset=EventWizardCopyForm.copy_from_queryset(self.user),
|
||||
queryset=EventWizardCopyForm.copy_from_queryset(self.user).filter(has_subevents=has_subevents),
|
||||
widget=forms.RadioSelect,
|
||||
empty_label=_('Do not copy'),
|
||||
required=False
|
||||
@@ -195,15 +211,15 @@ class EventSettingsForm(SettingsForm):
|
||||
presale_start_show_date = forms.BooleanField(
|
||||
label=_("Show start date"),
|
||||
help_text=_("Show the presale start date before presale has started."),
|
||||
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_presale_start'}),
|
||||
widget=forms.CheckboxInput,
|
||||
required=False
|
||||
)
|
||||
last_order_modification_date = forms.DateTimeField(
|
||||
last_order_modification_date = RelativeDateTimeField(
|
||||
label=_('Last date of modifications'),
|
||||
help_text=_("The last date users can modify details of their orders, such as attendee names or "
|
||||
"answers to questions."),
|
||||
"answers to questions. If you use the event series feature and an order contains tickest for "
|
||||
"multiple event dates, the earliest date will be used."),
|
||||
required=False,
|
||||
widget=forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
)
|
||||
timezone = forms.ChoiceField(
|
||||
choices=((a, a) for a in common_timezones),
|
||||
@@ -327,12 +343,12 @@ class PaymentSettingsForm(SettingsForm):
|
||||
label=_('Payment term in days'),
|
||||
help_text=_("The number of days after placing an order the user has to pay to preserve his reservation."),
|
||||
)
|
||||
payment_term_last = forms.DateField(
|
||||
payment_term_last = RelativeDateField(
|
||||
label=_('Last date of payments'),
|
||||
help_text=_("The last date any payments are accepted. This has precedence over the number of "
|
||||
"days configured above."),
|
||||
"days configured above. If you use the event series feature and an order contains tickets for "
|
||||
"multiple dates, the earliest date will be used."),
|
||||
required=False,
|
||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'})
|
||||
)
|
||||
payment_term_weekdays = forms.BooleanField(
|
||||
label=_('Only end payment terms on weekdays'),
|
||||
@@ -364,8 +380,10 @@ class PaymentSettingsForm(SettingsForm):
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
payment_term_last = cleaned_data.get('payment_term_last')
|
||||
print(payment_term_last)
|
||||
if payment_term_last and self.obj.presale_end:
|
||||
if payment_term_last < self.obj.presale_end.date():
|
||||
print(payment_term_last, payment_term_last.datetime(self.obj), self.obj.presale_end.date())
|
||||
if payment_term_last.datetime(self.obj) < self.obj.presale_end.date():
|
||||
self.add_error(
|
||||
'payment_term_last',
|
||||
_('The last payment date cannot be before the end of presale.'),
|
||||
@@ -392,6 +410,8 @@ class ProviderForm(SettingsForm):
|
||||
v._required = v.one_required
|
||||
v.one_required = False
|
||||
v.widget.enabled_locales = self.locales
|
||||
elif isinstance(v, (RelativeDateTimeField, RelativeDateField)):
|
||||
v.set_event(self.obj)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
@@ -658,12 +678,12 @@ class TicketSettingsForm(SettingsForm):
|
||||
help_text=_("Use pretix to generate tickets for the user to download and print out."),
|
||||
required=False
|
||||
)
|
||||
ticket_download_date = forms.DateTimeField(
|
||||
ticket_download_date = RelativeDateTimeField(
|
||||
label=_("Download date"),
|
||||
help_text=_("Ticket download will be offered after this date."),
|
||||
required=True,
|
||||
widget=forms.DateTimeInput(attrs={'class': 'datetimepicker',
|
||||
'data-display-dependency': '#id_ticket_download'}),
|
||||
help_text=_("Ticket download will be offered after this date. If you use the event series feature and an order "
|
||||
"contains tickets for multiple event dates, download of all tickets will be available if at least "
|
||||
"one of the event dates allows it."),
|
||||
required=False,
|
||||
)
|
||||
ticket_download_addons = forms.BooleanField(
|
||||
label=_("Offer to download tickets separately for add-on products"),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Item, Order, Organizer
|
||||
from pretix.base.models import Item, Order, Organizer, SubEvent
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.control.utils.i18n import i18ncomp
|
||||
|
||||
@@ -86,6 +86,12 @@ class EventOrderFilterForm(OrderFilterForm):
|
||||
],
|
||||
required=False,
|
||||
)
|
||||
subevent = forms.ModelChoiceField(
|
||||
label=pgettext_lazy('subevent', 'Date'),
|
||||
queryset=SubEvent.objects.none(),
|
||||
required=False,
|
||||
empty_label=pgettext_lazy('subevent', 'All dates')
|
||||
)
|
||||
|
||||
def get_payment_providers(self):
|
||||
providers = []
|
||||
@@ -105,12 +111,20 @@ class EventOrderFilterForm(OrderFilterForm):
|
||||
self.fields['provider'].choices += [(k, v.verbose_name) for k, v
|
||||
in self.event.get_payment_providers().items()]
|
||||
|
||||
if self.event.has_subevents:
|
||||
self.fields['subevent'].queryset = self.event.subevents.all()
|
||||
elif 'subevent':
|
||||
del self.fields['subevent']
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
qs = super().filter_qs(qs)
|
||||
|
||||
if fdata.get('item'):
|
||||
qs = qs.filter(positions__item_id__in=(fdata.get('item'),))
|
||||
qs = qs.filter(positions__item=fdata.get('item'))
|
||||
|
||||
if fdata.get('subevent'):
|
||||
qs = qs.filter(positions__subevent=fdata.get('subevent'))
|
||||
|
||||
if fdata.get('provider'):
|
||||
qs = qs.filter(payment_provider=fdata.get('provider'))
|
||||
@@ -146,6 +160,57 @@ class OrderSearchFilterForm(OrderFilterForm):
|
||||
return qs
|
||||
|
||||
|
||||
class SubEventFilterForm(FilterForm):
|
||||
status = forms.ChoiceField(
|
||||
label=_('Status'),
|
||||
choices=(
|
||||
('', _('All')),
|
||||
('active', _('Active')),
|
||||
('running', _('Shop live and presale running')),
|
||||
('inactive', _('Inactive')),
|
||||
('future', _('Presale not started')),
|
||||
('past', _('Presale over')),
|
||||
),
|
||||
required=False
|
||||
)
|
||||
query = forms.CharField(
|
||||
label=_('Event name'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Event name'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
|
||||
if fdata.get('status') == 'active':
|
||||
qs = qs.filter(active=True)
|
||||
elif fdata.get('status') == 'running':
|
||||
qs = qs.filter(
|
||||
active=True
|
||||
).filter(
|
||||
Q(presale_start__isnull=True) | Q(presale_start__lte=now())
|
||||
).filter(
|
||||
Q(presale_end__isnull=True) | Q(presale_end__gte=now())
|
||||
)
|
||||
elif fdata.get('status') == 'inactive':
|
||||
qs = qs.filter(active=False)
|
||||
elif fdata.get('status') == 'future':
|
||||
qs = qs.filter(presale_start__gte=now())
|
||||
elif fdata.get('status') == 'past':
|
||||
qs = qs.filter(presale_end__lte=now())
|
||||
|
||||
if fdata.get('query'):
|
||||
query = fdata.get('query')
|
||||
qs = qs.filter(
|
||||
Q(name__icontains=i18ncomp(query)) | Q(location__icontains=query)
|
||||
)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class EventFilterForm(FilterForm):
|
||||
status = forms.ChoiceField(
|
||||
label=_('Status'),
|
||||
|
||||
@@ -3,7 +3,6 @@ import copy
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Max
|
||||
from django.forms import BooleanField, ModelMultipleChoiceField
|
||||
from django.forms.formsets import DELETION_FIELD_NAME
|
||||
from django.utils.translation import ugettext as __, ugettext_lazy as _
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
@@ -52,7 +51,6 @@ class QuestionForm(I18nModelForm):
|
||||
|
||||
|
||||
class QuestionOptionForm(I18nModelForm):
|
||||
|
||||
class Meta:
|
||||
model = QuestionOption
|
||||
localized_fields = '__all__'
|
||||
@@ -62,36 +60,38 @@ class QuestionOptionForm(I18nModelForm):
|
||||
|
||||
|
||||
class QuotaForm(I18nModelForm):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
items = kwargs['items']
|
||||
del kwargs['items']
|
||||
instance = kwargs.get('instance', None)
|
||||
self.original_instance = copy.copy(instance) if instance else None
|
||||
self.instance = kwargs.get('instance', None)
|
||||
self.event = kwargs.get('event')
|
||||
items = kwargs.pop('items', None) or self.event.items.prefetch_related('variations')
|
||||
self.original_instance = copy.copy(self.instance) if self.instance else None
|
||||
initial = kwargs.get('initial', {})
|
||||
if self.instance and self.instance.pk:
|
||||
initial['itemvars'] = [str(i.pk) for i in self.instance.items.all()] + [
|
||||
'{}-{}'.format(v.item_id, v.pk) for v in self.instance.variations.all()
|
||||
]
|
||||
kwargs['initial'] = initial
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if hasattr(self, 'instance') and self.instance.pk:
|
||||
active_items = set(self.instance.items.all())
|
||||
active_variations = set(self.instance.variations.all())
|
||||
else:
|
||||
active_items = set()
|
||||
active_variations = set()
|
||||
|
||||
choices = []
|
||||
for item in items:
|
||||
if len(item.variations.all()) > 0:
|
||||
self.fields['item_%s' % item.id] = ModelMultipleChoiceField(
|
||||
label=_("Activate for"),
|
||||
required=False,
|
||||
initial=active_variations,
|
||||
queryset=item.variations.all(),
|
||||
widget=forms.CheckboxSelectMultiple
|
||||
)
|
||||
for v in item.variations.all():
|
||||
choices.append(('{}-{}'.format(item.pk, v.pk), '{} – {}'.format(item.name, v.value)))
|
||||
else:
|
||||
self.fields['item_%s' % item.id] = BooleanField(
|
||||
label=_("Activate"),
|
||||
required=False,
|
||||
initial=(item in active_items)
|
||||
)
|
||||
choices.append(('{}'.format(item.pk), item.name))
|
||||
|
||||
self.fields['itemvars'] = forms.MultipleChoiceField(
|
||||
label=_('Products'),
|
||||
required=False,
|
||||
choices=choices,
|
||||
widget=forms.CheckboxSelectMultiple
|
||||
)
|
||||
|
||||
if self.event.has_subevents:
|
||||
self.fields['subevent'].queryset = self.event.subevents.all()
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
|
||||
class Meta:
|
||||
model = Quota
|
||||
@@ -99,8 +99,29 @@ class QuotaForm(I18nModelForm):
|
||||
fields = [
|
||||
'name',
|
||||
'size',
|
||||
'subevent'
|
||||
]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
creating = not self.instance.pk
|
||||
inst = super().save(*args, **kwargs)
|
||||
|
||||
selected_items = set(list(self.event.items.filter(id__in=[
|
||||
i.split('-')[0] for i in self.cleaned_data['itemvars']
|
||||
])))
|
||||
selected_variations = list(ItemVariation.objects.filter(item__event=self.event, id__in=[
|
||||
i.split('-')[1] for i in self.cleaned_data['itemvars'] if '-' in i
|
||||
]))
|
||||
|
||||
current_items = [] if creating else self.instance.items.all()
|
||||
current_variations = [] if creating else self.instance.variations.all()
|
||||
|
||||
self.instance.items.remove(*[i for i in current_items if i not in selected_items])
|
||||
self.instance.items.add(*[i for i in selected_items if i not in current_items])
|
||||
self.instance.variations.remove(*[i for i in current_variations if i not in selected_variations])
|
||||
self.instance.variations.add(*[i for i in selected_variations if i not in current_variations])
|
||||
return inst
|
||||
|
||||
|
||||
class ItemCreateForm(I18nModelForm):
|
||||
has_variations = forms.BooleanField(label=_('The product should exist in multiple variations'),
|
||||
@@ -197,7 +218,6 @@ class ItemUpdateForm(I18nModelForm):
|
||||
|
||||
|
||||
class ItemVariationsFormSet(I18nFormSet):
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
for f in self.forms:
|
||||
|
||||
@@ -4,10 +4,12 @@ from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.formats import localize
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models import Item, ItemAddOn, Order, OrderPosition
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.pricing import get_price
|
||||
|
||||
|
||||
class ExtendForm(I18nModelForm):
|
||||
@@ -55,6 +57,15 @@ class CommentForm(I18nModelForm):
|
||||
}
|
||||
|
||||
|
||||
class SubEventChoiceField(forms.ModelChoiceField):
|
||||
def label_from_instance(self, obj):
|
||||
p = get_price(self.instance.item, self.instance.variation,
|
||||
voucher=self.instance.voucher,
|
||||
subevent=obj)
|
||||
return '{} – {} ({} {})'.format(obj.name, obj.get_date_range_display(),
|
||||
p, self.instance.order.event.currency)
|
||||
|
||||
|
||||
class OrderPositionAddForm(forms.Form):
|
||||
do = forms.BooleanField(
|
||||
label=_('Add a new product to the order'),
|
||||
@@ -74,6 +85,12 @@ class OrderPositionAddForm(forms.Form):
|
||||
label=_('Gross price'),
|
||||
help_text=_("Keep empty for the product's default price")
|
||||
)
|
||||
subevent = forms.ModelChoiceField(
|
||||
SubEvent.objects.none(),
|
||||
label=pgettext_lazy('subevent', 'Date'),
|
||||
required=True,
|
||||
empty_label=None
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
order = kwargs.pop('order')
|
||||
@@ -100,9 +117,20 @@ class OrderPositionAddForm(forms.Form):
|
||||
else:
|
||||
del self.fields['addon_to']
|
||||
|
||||
if order.event.has_subevents:
|
||||
self.fields['subevent'].queryset = order.event.subevents.all()
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
|
||||
|
||||
class OrderPositionChangeForm(forms.Form):
|
||||
itemvar = forms.ChoiceField()
|
||||
subevent = SubEventChoiceField(
|
||||
SubEvent.objects.none(),
|
||||
label=pgettext_lazy('subevent', 'New date'),
|
||||
required=True,
|
||||
empty_label=None
|
||||
)
|
||||
price = forms.DecimalField(
|
||||
required=False,
|
||||
max_digits=10, decimal_places=2,
|
||||
@@ -114,6 +142,7 @@ class OrderPositionChangeForm(forms.Form):
|
||||
choices=(
|
||||
('product', 'Change product'),
|
||||
('price', 'Change price'),
|
||||
('subevent', 'Change event date'),
|
||||
('cancel', 'Remove product')
|
||||
)
|
||||
)
|
||||
@@ -131,9 +160,15 @@ class OrderPositionChangeForm(forms.Form):
|
||||
pass
|
||||
|
||||
initial['price'] = instance.price
|
||||
initial['subevent'] = instance.subevent
|
||||
|
||||
kwargs['initial'] = initial
|
||||
super().__init__(*args, **kwargs)
|
||||
if instance.order.event.has_subevents:
|
||||
self.fields['subevent'].instance = instance
|
||||
self.fields['subevent'].queryset = instance.order.event.subevents.all()
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
choices = []
|
||||
for i in instance.order.event.items.prefetch_related('variations').all():
|
||||
pname = str(i.name)
|
||||
@@ -142,11 +177,13 @@ class OrderPositionChangeForm(forms.Form):
|
||||
variations = list(i.variations.all())
|
||||
if variations:
|
||||
for v in variations:
|
||||
p = get_price(i, v, voucher=instance.voucher, subevent=instance.subevent)
|
||||
choices.append(('%d-%d' % (i.pk, v.pk),
|
||||
'%s – %s (%s %s)' % (pname, v.value, localize(v.price),
|
||||
'%s – %s (%s %s)' % (pname, v.value, localize(p),
|
||||
instance.order.event.currency)))
|
||||
else:
|
||||
choices.append((str(i.pk), '%s (%s %s)' % (pname, localize(i.default_price),
|
||||
p = get_price(i, None, voucher=instance.voucher, subevent=instance.subevent)
|
||||
choices.append((str(i.pk), '%s (%s %s)' % (pname, localize(p),
|
||||
instance.order.event.currency)))
|
||||
self.fields['itemvar'].choices = choices
|
||||
|
||||
|
||||
98
src/pretix/control/forms/subevents.py
Normal file
98
src/pretix/control/forms/subevents.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from django import forms
|
||||
from django.utils.functional import cached_property
|
||||
from i18nfield.forms import I18nInlineFormSet
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import SubEventItem
|
||||
|
||||
|
||||
class SubEventForm(I18nModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs['event']
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['location'].widget.attrs['rows'] = '3'
|
||||
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'name',
|
||||
'active',
|
||||
'date_from',
|
||||
'date_to',
|
||||
'date_admission',
|
||||
'presale_start',
|
||||
'presale_end',
|
||||
'location',
|
||||
]
|
||||
widgets = {
|
||||
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'date_to': forms.DateTimeInput(attrs={'class': 'datetimepicker', 'data-date-after': '#id_date_from'}),
|
||||
'date_admission': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'presale_start': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'presale_end': forms.DateTimeInput(attrs={'class': 'datetimepicker',
|
||||
'data-date-after': '#id_presale_start'}),
|
||||
}
|
||||
|
||||
|
||||
class SubEventItemOrVariationFormMixin:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.item = kwargs.pop('item')
|
||||
self.variation = kwargs.pop('variation', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class SubEventItemForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['price'].widget.attrs['placeholder'] = '{} {}'.format(
|
||||
self.item.default_price, self.item.event.currency
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SubEventItem
|
||||
fields = ['price']
|
||||
|
||||
|
||||
class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['price'].widget.attrs['placeholder'] = '{} {}'.format(
|
||||
self.variation.price, self.item.event.currency
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SubEventItem
|
||||
fields = ['price']
|
||||
|
||||
|
||||
class QuotaFormSet(I18nInlineFormSet):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event', None)
|
||||
self.locales = self.event.settings.get('locales')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@cached_property
|
||||
def items(self):
|
||||
return self.event.items.prefetch_related('variations').all()
|
||||
|
||||
def _construct_form(self, i, **kwargs):
|
||||
kwargs['locales'] = self.locales
|
||||
kwargs['event'] = self.event
|
||||
kwargs['items'] = self.items
|
||||
return super()._construct_form(i, **kwargs)
|
||||
|
||||
@property
|
||||
def empty_form(self):
|
||||
form = self.form(
|
||||
auto_id=self.auto_id,
|
||||
prefix=self.add_prefix('__prefix__'),
|
||||
empty_permitted=True,
|
||||
locales=self.locales,
|
||||
event=self.event,
|
||||
items=self.items
|
||||
)
|
||||
self.add_fields(form, None)
|
||||
return form
|
||||
@@ -4,7 +4,7 @@ from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models import Item, ItemVariation, Quota, Voucher
|
||||
@@ -24,7 +24,7 @@ class VoucherForm(I18nModelForm):
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
|
||||
'comment', 'max_usages', 'price_mode'
|
||||
'comment', 'max_usages', 'price_mode', 'subevent'
|
||||
]
|
||||
widgets = {
|
||||
'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
@@ -47,6 +47,12 @@ class VoucherForm(I18nModelForm):
|
||||
else:
|
||||
self.initial_instance_data = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if instance.event.has_subevents:
|
||||
self.fields['subevent'].queryset = instance.event.subevents.all()
|
||||
elif 'subevent':
|
||||
del self.fields['subevent']
|
||||
|
||||
choices = []
|
||||
for i in self.instance.event.items.prefetch_related('variations').all():
|
||||
variations = list(i.variations.all())
|
||||
@@ -103,6 +109,12 @@ class VoucherForm(I18nModelForm):
|
||||
else:
|
||||
cnt = data['max_usages']
|
||||
|
||||
if self.instance.event.has_subevents and data['block_quota'] and not data.get('subevent'):
|
||||
raise ValidationError(pgettext_lazy(
|
||||
'subevent',
|
||||
'If you want this voucher to block quota, you need to select a specific date.'
|
||||
))
|
||||
|
||||
if self._clean_quota_needs_checking(data):
|
||||
self._clean_quota_check(data, cnt)
|
||||
|
||||
@@ -136,6 +148,10 @@ class VoucherForm(I18nModelForm):
|
||||
# The voucher has been reassigned to a different item, variation or quota
|
||||
return True
|
||||
|
||||
if data.get('subevent') != self.initial.get('subevent'):
|
||||
# The voucher has been reassigned to a different subevent
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _clean_was_valid(self):
|
||||
@@ -147,9 +163,11 @@ class VoucherForm(I18nModelForm):
|
||||
if self.initial_instance_data.quota:
|
||||
quotas.add(self.initial_instance_data.quota)
|
||||
elif self.initial_instance_data.variation:
|
||||
quotas |= set(self.initial_instance_data.variation.quotas.all())
|
||||
quotas |= set(self.initial_instance_data.variation.quotas.filter(
|
||||
subevent=self.initial_instance_data.subevent))
|
||||
elif self.initial_instance_data.item:
|
||||
quotas |= set(self.initial_instance_data.item.quotas.all())
|
||||
quotas |= set(self.initial_instance_data.item.quotas.filter(
|
||||
subevent=self.initial_instance_data.subevent))
|
||||
return quotas
|
||||
|
||||
def _clean_quota_check(self, data, cnt):
|
||||
@@ -164,9 +182,9 @@ class VoucherForm(I18nModelForm):
|
||||
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
|
||||
'Otherwise it might be unclear which quotas to block.'))
|
||||
elif self.instance.item and self.instance.variation:
|
||||
avail = self.instance.variation.check_quotas(ignored_quotas=old_quotas)
|
||||
avail = self.instance.variation.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
|
||||
elif self.instance.item and not self.instance.item.has_variations:
|
||||
avail = self.instance.item.check_quotas(ignored_quotas=old_quotas)
|
||||
avail = self.instance.item.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
|
||||
else:
|
||||
raise ValidationError(_('You need to specify either a quota or a product.'))
|
||||
|
||||
@@ -195,7 +213,7 @@ class VoucherBulkForm(VoucherForm):
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
|
||||
'max_usages', 'price_mode'
|
||||
'max_usages', 'price_mode', 'subevent'
|
||||
]
|
||||
widgets = {
|
||||
'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
|
||||
Reference in New Issue
Block a user