Files
pretix_original/src/pretix/control/views/subevents.py
2021-03-05 13:05:04 +01:00

1455 lines
58 KiB
Python

import copy
from collections import defaultdict
from datetime import datetime, time, timedelta
from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rruleset
from django.contrib import messages
from django.core.files import File
from django.db import connections, transaction
from django.db.models import (
Count, F, IntegerField, OuterRef, Prefetch, Subquery, Sum,
)
from django.db.models.functions import Coalesce, TruncDate, TruncTime
from django.forms import inlineformset_factory
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.formats import get_format
from django.utils.functional import cached_property
from django.utils.timezone import make_aware
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.views import View
from django.views.generic import (
CreateView, DeleteView, FormView, ListView, UpdateView,
)
from pretix.base.models import CartPosition, LogEntry
from pretix.base.models.checkin import CheckinList
from pretix.base.models.event import SubEvent, SubEventMetaValue
from pretix.base.models.items import (
ItemVariation, Quota, SubEventItem, SubEventItemVariation,
)
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
from pretix.base.services import tickets
from pretix.base.services.quotas import QuotaAvailability
from pretix.control.forms.checkin import SimpleCheckinListForm
from pretix.control.forms.filter import SubEventFilterForm
from pretix.control.forms.item import QuotaForm
from pretix.control.forms.subevents import (
CheckinListFormSet, QuotaFormSet, RRuleFormSet, SubEventBulkEditForm,
SubEventBulkForm, SubEventForm, SubEventItemForm,
SubEventItemVariationForm, SubEventMetaValueForm, TimeFormSet,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import subevent_forms
from pretix.control.views import PaginationMixin
from pretix.control.views.event import MetaDataEditorMixin
from pretix.helpers import GroupConcat
from pretix.helpers.models import modelcopy
class SubEventQueryMixin:
@cached_property
def request_data(self):
if self.request.method == "POST":
return self.request.POST
return self.request.GET
def get_queryset(self, list=False):
sum_tickets_paid = Quota.objects.filter(
subevent=OuterRef('pk')
).order_by().values('subevent').annotate(
s=Sum('cached_availability_paid_orders')
).values(
's'
)
qs = self.request.event.subevents
if list:
qs = qs.annotate(
sum_tickets_paid=Subquery(sum_tickets_paid, output_field=IntegerField())
).prefetch_related(
Prefetch('quotas',
queryset=Quota.objects.annotate(s=Coalesce(F('size'), 0)).order_by('-s'),
to_attr='first_quotas')
)
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
if 'subevent' in self.request_data and '__ALL' not in self.request_data:
qs = qs.filter(
id__in=self.request_data.getlist('subevent')
)
return qs
@cached_property
def filter_form(self):
return SubEventFilterForm(data=self.request_data, prefix='filter')
class SubEventList(EventPermissionRequiredMixin, PaginationMixin, SubEventQueryMixin, ListView):
model = SubEvent
context_object_name = 'subevents'
template_name = 'pretixcontrol/subevents/index.html'
permission = 'can_change_settings'
def get_queryset(self):
return super().get_queryset(True)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['filter_form'] = self.filter_form
quotas = []
for s in ctx['subevents']:
s.first_quotas = s.first_quotas[:4]
quotas += list(s.first_quotas)
qa = QuotaAvailability(early_out=False)
for q in quotas:
if q.cached_availability_time is None or q.cached_availability_paid_orders is None:
qa.queue(q)
qa.compute()
for q in quotas:
q.cached_avail = (
qa.results[q] if q in qa.results
else (q.cached_availability_state, q.cached_availability_number)
)
if q.size is not None:
q.percent_paid = min(
100,
round(q.cached_availability_paid_orders / q.size * 100) if q.size > 0 else 100
)
return ctx
class SubEventDelete(EventPermissionRequiredMixin, DeleteView):
model = SubEvent
template_name = 'pretixcontrol/subevents/delete.html'
permission = 'can_change_settings'
context_object_name = 'subevents'
def get_object(self, queryset=None) -> SubEvent:
try:
return self.request.event.subevents.get(
id=self.kwargs['subevent']
)
except SubEvent.DoesNotExist:
raise Http404(pgettext_lazy("subevent", "The requested date does not exist."))
def get(self, request, *args, **kwargs):
if not self.get_object().allow_delete():
messages.error(request, pgettext_lazy('subevent', 'A date can not be deleted if orders already have been '
'placed.'))
return HttpResponseRedirect(self.get_success_url())
return super().get(request, *args, **kwargs)
@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(request, pgettext_lazy('subevent', 'A date can not be deleted if orders already have been '
'placed.'))
return HttpResponseRedirect(self.get_success_url())
else:
self.object.log_action('pretix.subevent.deleted', user=self.request.user)
CartPosition.objects.filter(addon_to__subevent=self.object).delete()
self.object.cartposition_set.all().delete()
self.object.delete()
messages.success(request, pgettext_lazy('subevent', 'The selected date has been deleted.'))
return HttpResponseRedirect(success_url)
def get_success_url(self) -> str:
return reverse('control:event.subevents', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
class SubEventEditorMixin(MetaDataEditorMixin):
meta_form = SubEventMetaValueForm
meta_model = SubEventMetaValue
@cached_property
def plugin_forms(self):
forms = []
for rec, resp in subevent_forms.send(sender=self.request.event, subevent=self.object, request=self.request,
copy_from=self.copy_from):
if isinstance(resp, (list, tuple)):
forms.extend(resp)
else:
forms.append(resp)
return forms
def _make_meta_form(self, p, val_instances):
if not hasattr(self, '_default_meta'):
self._default_meta = self.request.event.meta_data
return self.meta_form(
prefix='prop-{}'.format(p.pk),
property=p,
disabled=(
p.protected and
not self.request.user.has_organizer_permission(self.request.organizer, 'can_change_organizer_settings', request=self.request)
),
default=self._default_meta.get(p.name, ''),
instance=val_instances.get(p.pk, self.meta_model(property=p, subevent=self.object)),
data=(self.request.POST if self.request.method == "POST" else None)
)
@cached_property
def cl_formset(self):
extra = 0
kwargs = {}
if self.copy_from and self.request.method != "POST":
kwargs['initial'] = [
{
'name': cl.name,
'all_products': cl.all_products,
'limit_products': cl.limit_products.all(),
'include_pending': cl.include_pending,
} for cl in self.copy_from.checkinlist_set.prefetch_related('limit_products')
]
extra = len(kwargs['initial'])
elif not self.object and self.request.method != "POST":
kwargs['initial'] = [
{
'name': '',
'all_products': True,
'include_pending': False,
}
]
extra = 0
formsetclass = inlineformset_factory(
SubEvent, CheckinList,
form=SimpleCheckinListForm, formset=CheckinListFormSet,
can_order=False, can_delete=True, extra=extra,
)
if self.object:
kwargs['queryset'] = self.object.checkinlist_set.prefetch_related('limit_products')
return formsetclass(self.request.POST if self.request.method == "POST" else None,
instance=self.object,
event=self.request.event, **kwargs)
@cached_property
def formset(self):
extra = 0
kwargs = {}
if self.copy_from and self.request.method != "POST":
kwargs['initial'] = [
{
'size': q.size,
'name': q.name,
'release_after_exit': q.release_after_exit,
'itemvars': [str(i.pk) for i in q.items.all()] + [
'{}-{}'.format(v.item_id, v.pk) for v in q.variations.all()
]
} for q in self.copy_from.quotas.prefetch_related('items', 'variations')
]
extra = len(kwargs['initial'])
formsetclass = inlineformset_factory(
SubEvent, Quota,
form=QuotaForm, formset=QuotaFormSet, min_num=1, validate_min=True,
can_order=False, can_delete=True, extra=extra,
)
if self.object:
kwargs['queryset'] = self.object.quotas.prefetch_related('items', 'variations')
return formsetclass(
self.request.POST if self.request.method == "POST" else None,
instance=self.object,
event=self.request.event, **kwargs
)
def save_cl_formset(self, obj):
for form in self.cl_formset.initial_forms:
if form in self.cl_formset.deleted_forms:
if not form.instance.pk:
continue
form.instance.checkins.all().delete()
form.instance.log_action(action='pretix.event.checkinlist.deleted', user=self.request.user)
form.instance.delete()
form.instance.pk = None
elif form.has_changed():
form.instance.subevent = obj
form.instance.event = obj.event
form.save()
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
change_data['id'] = form.instance.pk
form.instance.log_action(
'pretix.event.checkinlist.changed', user=self.request.user, data={
k: form.cleaned_data.get(k) for k in form.changed_data
}
)
for form in self.cl_formset.extra_forms:
if not form.has_changed():
continue
if self.formset._should_delete_form(form):
continue
form.instance.subevent = obj
form.instance.event = obj.event
form.save()
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
change_data['id'] = form.instance.pk
form.instance.log_action(action='pretix.event.checkinlist.added', user=self.request.user, data=change_data)
def save_formset(self, obj):
for form in self.formset.initial_forms:
if form in self.formset.deleted_forms:
if not form.instance.pk:
continue
form.instance.log_action(action='pretix.event.quota.deleted', user=self.request.user)
obj.log_action('pretix.subevent.quota.deleted', user=self.request.user, data={
'id': form.instance.pk
})
form.instance.delete()
form.instance.pk = None
elif form.has_changed():
form.instance.question = obj
form.save()
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
change_data['id'] = form.instance.pk
obj.log_action(
'pretix.subevent.quota.changed', user=self.request.user, data={
k: form.cleaned_data.get(k) for k in form.changed_data
}
)
form.instance.log_action(
'pretix.event.quota.changed', user=self.request.user, data={
k: form.cleaned_data.get(k) for k in form.changed_data
}
)
for form in self.formset.extra_forms:
if not form.has_changed():
continue
if self.formset._should_delete_form(form):
continue
form.instance.subevent = obj
form.instance.event = obj.event
form.save()
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
change_data['id'] = form.instance.pk
form.instance.log_action(action='pretix.event.quota.added', user=self.request.user, data=change_data)
obj.log_action('pretix.subevent.quota.added', user=self.request.user, data=change_data)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['formset'] = self.formset
ctx['cl_formset'] = self.cl_formset
ctx['itemvar_forms'] = self.itemvar_forms
ctx['meta_forms'] = self.meta_forms
ctx['plugin_forms'] = self.plugin_forms
return ctx
@cached_property
def copy_from(self):
if self.request.GET.get("copy_from") and (not getattr(self, 'object', None) or not self.object.pk):
try:
return self.request.event.subevents.get(pk=self.request.GET.get("copy_from"))
except SubEvent.DoesNotExist:
pass
@cached_property
def itemvar_forms(self):
se_item_instances = {
sei.item_id: sei for sei in SubEventItem.objects.filter(subevent=self.object)
}
se_var_instances = {
sei.variation_id: sei for sei in SubEventItemVariation.objects.filter(subevent=self.object)
}
if self.copy_from:
se_item_instances = {
sei.item_id: SubEventItem(item=sei.item, price=sei.price, disabled=sei.disabled)
for sei in SubEventItem.objects.filter(subevent=self.copy_from).select_related('item')
}
se_var_instances = {
sei.variation_id: SubEventItemVariation(variation=sei.variation, price=sei.price, disabled=sei.disabled)
for sei in SubEventItemVariation.objects.filter(subevent=self.copy_from).select_related('variation')
}
formlist = []
for i in self.request.event.items.filter(active=True).prefetch_related('variations'):
if i.has_variations:
for v in i.variations.all():
inst = se_var_instances.get(v.pk) or SubEventItemVariation(subevent=self.object, variation=v)
formlist.append(SubEventItemVariationForm(
prefix='itemvar-{}'.format(v.pk),
item=i, variation=v,
instance=inst,
data=(self.request.POST if self.request.method == "POST" else None)
))
else:
inst = se_item_instances.get(i.pk) or SubEventItem(subevent=self.object, item=i)
formlist.append(SubEventItemForm(
prefix='item-{}'.format(i.pk),
item=i,
instance=inst,
data=(self.request.POST if self.request.method == "POST" else None)
))
return formlist
def is_valid(self, form):
return form.is_valid() and all([f.is_valid() for f in self.itemvar_forms]) and self.formset.is_valid() and (
all([f.is_valid() for f in self.meta_forms])
) and self.cl_formset.is_valid() and all(f.is_valid() for f in self.plugin_forms)
class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateView):
model = SubEvent
template_name = 'pretixcontrol/subevents/detail.html'
permission = 'can_change_settings'
context_object_name = 'subevent'
form_class = SubEventForm
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
if self.is_valid(form):
r = self.form_valid(form)
return r
messages.error(self.request, _('We could not save your changes. See below for details.'))
return self.form_invalid(form)
def get_object(self, queryset=None) -> SubEvent:
try:
return self.request.event.subevents.get(
id=self.kwargs['subevent']
)
except SubEvent.DoesNotExist:
raise Http404(pgettext_lazy("subevent", "The requested date does not exist."))
@transaction.atomic
def form_valid(self, form):
self.save_formset(self.object)
self.save_cl_formset(self.object)
self.save_meta()
for f in self.itemvar_forms:
f.save()
# TODO: LogEntry?
messages.success(self.request, _('Your changes have been saved.'))
if form.has_changed() or any(f.has_changed() for f in self.plugin_forms):
data = {
k: form.cleaned_data.get(k) for k in form.changed_data
}
for f in self.plugin_forms:
data.update({
k: (f.cleaned_data.get(k).name
if isinstance(f.cleaned_data.get(k), File)
else f.cleaned_data.get(k))
for k in f.changed_data
})
self.object.log_action(
'pretix.subevent.changed', user=self.request.user, data=data
)
for f in self.plugin_forms:
f.subevent = self.object
f.save()
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk})
return super().form_valid(form)
def get_success_url(self) -> str:
return reverse('control:event.subevents', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + ('?' + self.request.GET.get('returnto') if 'returnto' in self.request.GET else '')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['event'] = self.request.event
return kwargs
class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateView):
model = SubEvent
template_name = 'pretixcontrol/subevents/detail.html'
permission = 'can_change_settings'
context_object_name = 'subevent'
form_class = SubEventForm
def post(self, request, *args, **kwargs):
self.object = SubEvent(event=self.request.event)
form = self.get_form()
if self.is_valid(form):
return self.form_valid(form)
return self.form_invalid(form)
def get_success_url(self) -> str:
return reverse('control:event.subevents', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['event'] = self.request.event
initial = kwargs.get('initial', {})
if self.copy_from:
i = modelcopy(self.copy_from)
i.pk = None
kwargs['instance'] = i
else:
kwargs['instance'] = SubEvent(event=self.request.event)
initial['location'] = self.request.event.location
initial['geo_lat'] = self.request.event.geo_lat
initial['geo_lon'] = self.request.event.geo_lon
kwargs['initial'] = initial
return kwargs
@transaction.atomic
def form_valid(self, form):
form.instance.event = self.request.event
messages.success(self.request, pgettext_lazy('subevent', 'The new date has been created.'))
ret = super().form_valid(form)
self.object = form.instance
data = dict(form.cleaned_data)
for f in self.plugin_forms:
data.update({
k: (f.cleaned_data.get(k).name
if isinstance(f.cleaned_data.get(k), File)
else f.cleaned_data.get(k))
for k in f.cleaned_data
})
form.instance.log_action('pretix.subevent.added', data=dict(data), user=self.request.user)
self.save_formset(form.instance)
self.save_cl_formset(form.instance)
for f in self.itemvar_forms:
f.instance.subevent = form.instance
f.save()
for f in self.meta_forms:
f.instance.subevent = form.instance
self.save_meta()
for f in self.plugin_forms:
f.subevent = form.instance
f.save()
return ret
@cached_property
def meta_forms(self):
def clone(o):
o = copy.copy(o)
o.pk = None
return o
if self.copy_from:
val_instances = {
v.property_id: clone(v) for v in self.copy_from.meta_values.all()
}
else:
val_instances = {}
formlist = []
for p in self.request.organizer.meta_properties.all():
formlist.append(self._make_meta_form(p, val_instances))
return formlist
class SubEventBulkAction(SubEventQueryMixin, EventPermissionRequiredMixin, View):
permission = 'can_change_settings'
@transaction.atomic
def post(self, request, *args, **kwargs):
if request.POST.get('action') == 'disable':
for obj in self.get_queryset():
obj.log_action(
'pretix.subevent.changed', user=self.request.user, data={
'active': False
}
)
obj.active = False
obj.save(update_fields=['active'])
messages.success(request, pgettext_lazy('subevent', 'The selected dates have been disabled.'))
elif request.POST.get('action') == 'enable':
for obj in self.get_queryset():
obj.log_action(
'pretix.subevent.changed', user=self.request.user, data={
'active': True
}
)
obj.active = True
obj.save(update_fields=['active'])
messages.success(request, pgettext_lazy('subevent', 'The selected dates have been enabled.'))
elif request.POST.get('action') == 'delete':
return render(request, 'pretixcontrol/subevents/delete_bulk.html', {
'allowed': self.get_queryset().filter(orderposition__isnull=True),
'forbidden': self.get_queryset().filter(orderposition__isnull=False).distinct(),
})
elif request.POST.get('action') == 'delete_confirm':
for obj in self.get_queryset():
if obj.allow_delete():
CartPosition.objects.filter(addon_to__subevent=obj).delete()
obj.cartposition_set.all().delete()
obj.log_action('pretix.subevent.deleted', user=self.request.user)
obj.delete()
else:
obj.log_action(
'pretix.subevent.changed', user=self.request.user, data={
'active': False
}
)
obj.active = False
obj.save(update_fields=['active'])
messages.success(request, pgettext_lazy('subevent', 'The selected dates have been deleted or disabled.'))
return redirect(self.get_success_url())
def get_success_url(self) -> str:
return reverse('control:event.subevents', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateView):
model = SubEvent
template_name = 'pretixcontrol/subevents/bulk.html'
permission = 'can_change_settings'
context_object_name = 'subevent'
form_class = SubEventBulkForm
def is_valid(self, form):
return self.rrule_formset.is_valid() and self.time_formset.is_valid() and super().is_valid(form)
def get_success_url(self) -> str:
return reverse('control:event.subevents', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
@cached_property
def rrule_formset(self):
return RRuleFormSet(
data=self.request.POST if self.request.method == "POST" else None,
prefix='rruleformset'
)
@cached_property
def time_formset(self):
return TimeFormSet(
data=self.request.POST if self.request.method == "POST" else None,
prefix='timeformset'
)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['rrule_formset'] = self.rrule_formset
ctx['time_formset'] = self.time_formset
tf = get_format('TIME_INPUT_FORMATS')[0]
ctx['time_begin_sample'] = time(9, 0, 0).strftime(tf)
ctx['time_end_sample'] = time(18, 0, 0).strftime(tf)
return ctx
@cached_property
def meta_forms(self):
def clone(o):
o = copy.copy(o)
o.pk = None
return o
if self.copy_from:
val_instances = {
v.property_id: clone(v) for v in self.copy_from.meta_values.all()
}
else:
val_instances = {}
formlist = []
for p in self.request.organizer.meta_properties.all():
formlist.append(self._make_meta_form(p, val_instances))
return formlist
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
initial = {
'active': True,
}
kwargs['event'] = self.request.event
tz = self.request.event.timezone
if self.copy_from:
i = copy.copy(self.copy_from)
i.pk = None
kwargs['instance'] = i
initial['time_from'] = i.date_from.astimezone(tz).time()
initial['time_to'] = i.date_to.astimezone(tz).time() if i.date_to else None
initial['time_admission'] = i.date_admission.astimezone(tz).time() if i.date_admission else None
initial['rel_presale_start'] = RelativeDateWrapper(RelativeDate(
days_before=(i.date_from.astimezone(tz).date() - i.presale_start.astimezone(tz).date()).days,
base_date_name='date_from',
time=i.presale_start.astimezone(tz).time(),
minutes_before=None
)) if i.presale_start else None
initial['rel_presale_end'] = RelativeDateWrapper(RelativeDate(
days_before=(i.date_from.astimezone(tz).date() - i.presale_end.astimezone(tz).date()).days,
base_date_name='date_from',
time=i.presale_end.astimezone(tz).time(),
minutes_before=None
)) if i.presale_end else None
else:
kwargs['instance'] = SubEvent(event=self.request.event)
initial['location'] = self.request.event.location
initial['geo_lat'] = self.request.event.geo_lat
initial['geo_lon'] = self.request.event.geo_lon
kwargs['initial'] = initial
return kwargs
def get_times(self):
times = []
for f in self.time_formset:
if f in self.time_formset.deleted_forms or not f.cleaned_data.get('time_from'):
continue
times.append(f.cleaned_data)
return times
def get_rrule_set(self):
s = rruleset()
for f in self.rrule_formset:
if f in self.rrule_formset.deleted_forms:
continue
rule_kwargs = {}
rule_kwargs['dtstart'] = f.cleaned_data['dtstart']
rule_kwargs['interval'] = f.cleaned_data['interval']
if f.cleaned_data['freq'] == 'yearly':
freq = YEARLY
if f.cleaned_data['yearly_same'] == "off":
rule_kwargs['bysetpos'] = int(f.cleaned_data['yearly_bysetpos'])
rule_kwargs['byweekday'] = f.parse_weekdays(f.cleaned_data['yearly_byweekday'])
rule_kwargs['bymonth'] = int(f.cleaned_data['yearly_bymonth'])
elif f.cleaned_data['freq'] == 'monthly':
freq = MONTHLY
if f.cleaned_data['monthly_same'] == "off":
rule_kwargs['bysetpos'] = int(f.cleaned_data['monthly_bysetpos'])
rule_kwargs['byweekday'] = f.parse_weekdays(f.cleaned_data['monthly_byweekday'])
elif f.cleaned_data['freq'] == 'weekly':
freq = WEEKLY
if f.cleaned_data['weekly_byweekday']:
rule_kwargs['byweekday'] = [f.parse_weekdays(a) for a in f.cleaned_data['weekly_byweekday']]
elif f.cleaned_data['freq'] == 'daily':
freq = DAILY
if f.cleaned_data['end'] == 'count':
rule_kwargs['count'] = f.cleaned_data['count']
else:
rule_kwargs['until'] = f.cleaned_data['until']
if f.cleaned_data['exclude']:
s.exrule(rrule(freq, **rule_kwargs))
else:
s.rrule(rrule(freq, **rule_kwargs))
return s
@transaction.atomic
def form_valid(self, form):
tz = self.request.event.timezone
subevents = []
for rdate in self.get_rrule_set():
for t in self.get_times():
se = copy.copy(form.instance)
se.date_from = make_aware(datetime.combine(rdate, t['time_from']), tz)
if t.get('time_to'):
se.date_to = (
make_aware(datetime.combine(rdate, t['time_to']), tz)
if t.get('time_to') > t.get('time_from')
else make_aware(datetime.combine(rdate + timedelta(days=1), t['time_to']), tz)
)
else:
se.date_to = None
se.date_admission = (
make_aware(datetime.combine(rdate, t['time_admission']), tz)
if t.get('time_admission')
else None
)
se.presale_start = (
form.cleaned_data['rel_presale_start'].datetime(se)
if form.cleaned_data.get('rel_presale_start')
else None
)
se.presale_end = (
form.cleaned_data['rel_presale_end'].datetime(se)
if form.cleaned_data.get('rel_presale_end')
else None
)
se.save(clear_cache=False)
subevents.append(se)
data = dict(form.cleaned_data)
for f in self.plugin_forms:
data.update({
k: (f.cleaned_data.get(k).name
if isinstance(f.cleaned_data.get(k), File)
else f.cleaned_data.get(k))
for k in f.cleaned_data
})
log_entries = []
for se in subevents:
log_entries.append(se.log_action('pretix.subevent.added', data=data, user=self.request.user, save=False))
to_save = []
for f in self.meta_forms:
if f.cleaned_data.get('value'):
for se in subevents:
i = copy.copy(f.instance)
i.pk = None
i.subevent = se
to_save.append(i)
SubEventMetaValue.objects.bulk_create(to_save)
to_save_items = []
to_save_variations = []
for f in self.itemvar_forms:
for se in subevents:
i = copy.copy(f.instance)
i.pk = None
i.subevent = se
if isinstance(i, SubEventItem):
to_save_items.append(i)
else:
to_save_variations.append(i)
SubEventItem.objects.bulk_create(to_save_items)
SubEventItemVariation.objects.bulk_create(to_save_variations)
to_save_items = []
to_save_variations = []
for f in self.formset.forms:
if self.formset._should_delete_form(f) or not f.has_changed():
continue
change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
for se in subevents:
i = copy.copy(f.instance)
i.pk = None
i.subevent = se
i.event = se.event
i.save(clear_cache=False)
selected_items = set(list(self.request.event.items.filter(id__in=[
i.split('-')[0] for i in f.cleaned_data.get('itemvars', [])
])))
selected_variations = list(ItemVariation.objects.filter(item__event=self.request.event, id__in=[
i.split('-')[1] for i in f.cleaned_data.get('itemvars', []) if '-' in i
]))
for _i in selected_items:
to_save_items.append(Quota.items.through(quota_id=i.pk, item_id=_i.pk))
for _i in selected_variations:
to_save_variations.append(Quota.variations.through(quota_id=i.pk, itemvariation_id=_i.pk))
change_data['id'] = i.pk
log_entries.append(
i.log_action(action='pretix.event.quota.added', user=self.request.user,
data=change_data, save=False)
)
log_entries.append(
se.log_action('pretix.subevent.quota.added', user=self.request.user, data=change_data, save=False)
)
Quota.items.through.objects.bulk_create(to_save_items)
Quota.variations.through.objects.bulk_create(to_save_variations)
to_save_products = []
for f in self.cl_formset.forms:
if self.cl_formset._should_delete_form(f) or not f.has_changed():
continue
change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
for se in subevents:
i = copy.copy(f.instance)
i.subevent = se
i.event = se.event
i.save()
for _i in f.cleaned_data.get('limit_products', []):
to_save_products.append(CheckinList.limit_products.through(checkinlist_id=i.pk, item_id=_i.pk))
change_data['id'] = i.pk
log_entries.append(
i.log_action(action='pretix.event.checkinlist.added', user=self.request.user, data=change_data,
save=False)
)
CheckinList.limit_products.through.objects.bulk_create(to_save_products)
for f in self.plugin_forms:
f.is_valid()
for se in subevents:
f.subevent = se
f.save()
if connections['default'].features.can_return_rows_from_bulk_insert:
LogEntry.objects.bulk_create(log_entries)
LogEntry.bulk_postprocess(log_entries)
else:
for le in log_entries:
le.save()
LogEntry.bulk_postprocess(log_entries)
self.request.event.cache.clear()
messages.success(self.request, pgettext_lazy('subevent', '{} new dates have been created.').format(len(subevents)))
return redirect(reverse('control:event.subevents', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}))
def post(self, request, *args, **kwargs):
form = self.get_form()
self.object = SubEvent(event=self.request.event)
if self.is_valid(form):
return self.form_valid(form)
messages.error(self.request, _('We could not save your changes. See below for details.'))
return self.form_invalid(form)
class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormView):
permission = 'can_change_settings'
form_class = SubEventBulkEditForm
template_name = 'pretixcontrol/subevents/bulk_edit.html'
context_object_name = 'subevent'
def get_queryset(self):
return super().get_queryset().prefetch_related(None).order_by()
def get_success_url(self) -> str:
return reverse('control:event.subevents', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
def get(self, request, *args, **kwargs):
return HttpResponse(status=405)
@cached_property
def cached_num(self):
return self.get_queryset().count()
@cached_property
def itemvar_forms(self):
matches = defaultdict(list)
for sei in SubEventItem.objects.filter(
subevent__in=self.get_queryset()
).order_by().values('item', 'price', 'disabled').annotate(c=Count('*')):
matches['item', sei['item']].append(sei)
for sei in SubEventItemVariation.objects.filter(
subevent__in=self.get_queryset()
).order_by().values('variation', 'price', 'disabled').annotate(c=Count('*')):
matches['variation', sei['variation']].append(sei)
total = self.cached_num
formlist = []
for i in self.request.event.items.filter(active=True).prefetch_related('variations'):
if i.has_variations:
for v in i.variations.all():
m = matches['variation', v.pk]
if m and len(m) == 1 and m[0]['c'] == total:
inst = SubEventItemVariation(variation=v, disabled=m[0]['disabled'], price=m[0]['price'])
else:
inst = SubEventItemVariation(variation=v)
formlist.append(SubEventItemVariationForm(
prefix='itemvar-{}'.format(v.pk),
item=i, variation=v,
instance=inst,
data=(self.request.POST if self.is_submitted else None)
))
else:
m = matches['item', i.pk]
if m and len(m) == 1 and m[0]['c'] == total:
inst = SubEventItem(item=i, disabled=m[0]['disabled'], price=m[0]['price'])
else:
inst = SubEventItem(item=i)
formlist.append(SubEventItemForm(
prefix='item-{}'.format(i.pk),
item=i,
instance=inst,
data=(self.request.POST if self.is_submitted else None)
))
return formlist
@cached_property
def meta_forms(self):
matches = defaultdict(list)
for smv in SubEventMetaValue.objects.filter(
subevent__in=self.get_queryset()
).order_by().values('property', 'value').annotate(c=Count('*')):
matches[smv['property']].append(smv)
total = self.cached_num
formlist = []
if not hasattr(self, '_default_meta'):
self._default_meta = self.request.event.meta_data
for p in self.request.organizer.meta_properties.all():
inst = SubEventMetaValue(property=p)
if len(matches[p.id]) == 1 and matches[p.id][0]['c'] == total:
inst.value = matches[p.id][0]['value']
formlist.append(SubEventMetaValueForm(
prefix='prop-{}'.format(p.pk),
property=p,
default=self._default_meta.get(p.name, ''),
instance=inst,
data=(self.request.POST if self.is_submitted else None)
))
return formlist
@cached_property
def quota_formset(self):
extra = 0
kwargs = {}
if self.sampled_quotas is not None:
kwargs['instance'] = self.get_queryset()[0]
formsetclass = inlineformset_factory(
SubEvent, Quota,
form=QuotaForm, formset=QuotaFormSet, min_num=0, validate_min=False,
can_order=False, can_delete=True, extra=extra,
)
return formsetclass(
self.request.POST if self.is_submitted else None,
event=self.request.event, **kwargs
)
@cached_property
def list_formset(self):
extra = 0
kwargs = {}
if self.sampled_lists is not None:
kwargs['instance'] = self.get_queryset()[0]
else:
return None
formsetclass = inlineformset_factory(
SubEvent, CheckinList,
form=SimpleCheckinListForm, formset=CheckinListFormSet, min_num=0, validate_min=False,
can_order=False, can_delete=True, extra=extra,
)
return formsetclass(
self.request.POST if self.is_submitted else None,
event=self.request.event, **kwargs
)
def save_list_formset(self, log_entries):
if not self.list_formset.has_changed() or self.sampled_lists is None:
return
qidx = 0
subevents = list(self.get_queryset().prefetch_related('checkinlist_set'))
to_save_products = []
to_save_gates = []
for f in self.list_formset.forms:
if self.list_formset._should_delete_form(f) and f in self.list_formset.extra_forms:
continue
if self.list_formset._should_delete_form(f):
for se in subevents:
q = list(se.checkinlist_set.all())[qidx]
log_entries += [
q.log_action(action='pretix.event.checkinlist.deleted', user=self.request.user, save=False),
]
q.delete()
elif f in self.list_formset.extra_forms:
change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
for se in subevents:
q = copy.copy(f.instance)
q.pk = None
q.subevent = se
q.event = self.request.event
q.save()
for _i in f.cleaned_data.get('limit_products', []):
to_save_products.append(CheckinList.limit_products.through(checkinlist_id=q.pk, item_id=_i.pk))
for _i in f.cleaned_data.get('gates', []):
to_save_gates.append(CheckinList.gates.through(checkinlist_id=q.pk, gate_id=_i.pk))
change_data['id'] = q.pk
log_entries.append(
q.log_action(action='pretix.event.checkinlist.added', user=self.request.user,
data=change_data, save=False)
)
else:
if f.changed_data:
change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
for se in subevents:
q = list(se.checkinlist_set.all())[qidx]
for fname in ('name', 'all_products', 'include_pending', 'allow_entry_after_exit'):
setattr(q, fname, f.cleaned_data.get(fname))
q.save()
if 'limit_products' in f.changed_data:
q.limit_products.set(f.cleaned_data.get('limit_products', []))
if 'gates' in f.changed_data:
q.gates.set(f.cleaned_data.get('limit_products', []))
log_entries.append(
q.log_action(action='pretix.event.checkinlist.changed', user=self.request.user,
data=change_data, save=False)
)
qidx += 1
if to_save_products:
CheckinList.limit_products.through.objects.bulk_create(to_save_products)
if to_save_gates:
CheckinList.gates.through.objects.bulk_create(to_save_gates)
def save_quota_formset(self, log_entries):
if not self.quota_formset.has_changed():
return
qidx = 0
subevents = list(self.get_queryset().prefetch_related('quotas'))
to_save_items = []
to_save_variations = []
to_delete_quota_ids = []
if self.sampled_quotas is None:
if len(self.quota_formset.forms) == 0:
return
else:
for se in subevents:
for q in se.quotas.all():
to_delete_quota_ids.append(q.pk)
log_entries += [
q.log_action(action='pretix.event.quota.deleted', user=self.request.user, save=False),
se.log_action('pretix.subevent.quota.deleted', user=self.request.user, data={
'id': q.pk
}, save=False)
]
if to_delete_quota_ids:
Quota.objects.filter(id__in=to_delete_quota_ids).delete()
for f in self.quota_formset.forms:
if self.quota_formset._should_delete_form(f) and f in self.quota_formset.extra_forms:
continue
selected_items = set(list(self.request.event.items.filter(id__in=[
i.split('-')[0] for i in f.cleaned_data.get('itemvars', [])
])))
selected_variations = list(ItemVariation.objects.filter(item__event=self.request.event, id__in=[
i.split('-')[1] for i in f.cleaned_data.get('itemvars', []) if '-' in i
]))
if self.quota_formset._should_delete_form(f):
for se in subevents:
q = list(se.quotas.all())[qidx]
log_entries += [
q.log_action(action='pretix.event.quota.deleted', user=self.request.user, save=False),
se.log_action('pretix.subevent.quota.deleted', user=self.request.user, data={
'id': q.pk
}, save=False)
]
q.delete()
elif f in self.quota_formset.extra_forms:
change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
for se in subevents:
q = copy.copy(f.instance)
q.pk = None
q.subevent = se
q.event = self.request.event
q.save(clear_cache=False)
for _i in selected_items:
to_save_items.append(Quota.items.through(quota_id=q.pk, item_id=_i.pk))
for _i in selected_variations:
to_save_variations.append(Quota.variations.through(quota_id=q.pk, itemvariation_id=_i.pk))
change_data['id'] = q.pk
log_entries.append(
q.log_action(action='pretix.event.quota.added', user=self.request.user,
data=change_data, save=False)
)
log_entries.append(
se.log_action('pretix.subevent.quota.added', user=self.request.user, data=change_data,
save=False)
)
else:
if f.changed_data:
change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
for se in subevents:
q = list(se.quotas.all())[qidx]
for fname in ('size', 'name', 'release_after_exit'):
setattr(q, fname, f.cleaned_data.get(fname))
q.save(clear_cache=False)
if 'itemvars' in f.changed_data:
q.items.set(selected_items)
q.variations.set(selected_variations)
log_entries.append(
q.log_action(action='pretix.event.quota.added', user=self.request.user,
data=change_data, save=False)
)
qidx += 1
if to_save_items:
Quota.items.through.objects.bulk_create(to_save_items)
if to_save_variations:
Quota.variations.through.objects.bulk_create(to_save_variations)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['subevents'] = self.get_queryset()
ctx['filter_form'] = self.filter_form
ctx['sampled_quotas'] = self.sampled_quotas
ctx['sampled_lists'] = self.sampled_lists
ctx['formset'] = self.quota_formset
ctx['cl_formset'] = self.list_formset
ctx['itemvar_forms'] = self.itemvar_forms
ctx['bulk_selected'] = self.request.POST.getlist("_bulk")
ctx['meta_forms'] = self.meta_forms
return ctx
@cached_property
def sampled_quotas(self):
all_quotas = Quota.objects.filter(
subevent__in=self.get_queryset()
).annotate(
item_list=GroupConcat('items__id'),
var_list=GroupConcat('variations__id'),
).values(
'item_list', 'var_list',
*(f.name for f in Quota._meta.fields if f.name not in (
'id', 'event', 'items', 'variations', 'cached_availability_state', 'cached_availability_number',
'cached_availability_paid_orders', 'cached_availability_time', 'closed',
))
).order_by('subevent_id')
if not all_quotas:
return Quota.objects.none()
quotas_by_subevent = defaultdict(list)
for q in all_quotas:
quotas_by_subevent[q.pop('subevent')].append(q)
prev = None
for se in self.get_queryset():
if se.pk not in quotas_by_subevent:
return None
if prev is None:
prev = quotas_by_subevent[se.pk]
if quotas_by_subevent[se.pk] != prev:
return None
return se.quotas.all()
@cached_property
def sampled_lists(self):
all_lists = CheckinList.objects.filter(
subevent__in=self.get_queryset()
).annotate(
item_list=GroupConcat('limit_products__id'),
gates_list=GroupConcat('gates__id'),
).values(
'item_list', 'gates_list',
*(f.name for f in CheckinList._meta.fields if f.name not in (
'id', 'event', 'limit_products', 'gates',
))
).order_by('subevent_id')
if not all_lists:
return SubEvent.objects.none()
lists_by_subevent = defaultdict(list)
for cl in all_lists:
lists_by_subevent[cl.pop('subevent')].append(cl)
prev = None
for se in self.get_queryset():
if se.pk not in lists_by_subevent:
return None
if prev is None:
prev = lists_by_subevent[se.pk]
if lists_by_subevent[se.pk] != prev:
return None
return se.checkinlist_set.all()
@cached_property
def is_submitted(self):
# Usually, django considers a form "bound" / "submitted" on every POST request. However, this view is always
# called with POST method, even if just to pass the selection of objects to work on, so we want to modify
# that behaviour
return '_bulk' in self.request.POST
def get_form_kwargs(self):
initial = {}
mixed_values = set()
qs = self.get_queryset()
qs = qs.annotate(
**{
# TODO: Once we're on Django 3.2, pass a tzinfo parameter
# Before Django 3.2, it uses the current timezone, which is hopefully fine
# as well in all cases we are concerned about
# See also: https://code.djangoproject.com/ticket/31948
k + '_day': TruncDate(k)
for k in ('date_from', 'date_to', 'date_admission', 'presale_start', 'presale_end')
},
**{
k + '_time': TruncTime(k)
for k in ('date_from', 'date_to', 'date_admission', 'presale_start', 'presale_end')
},
)
fields = {
'name',
'location',
'frontpage_text',
'geo_lat',
'geo_lon',
'is_public',
'active',
'date_from_day',
'date_from_time',
'date_to_day',
'date_to_time',
'date_admission_day',
'date_admission_time',
'presale_start_day',
'presale_start_time',
'presale_end_day',
'presale_end_time',
}
for k in fields:
existing_values = list(qs.order_by(k).values(k).annotate(c=Count('*')))
if len(existing_values) == 1:
initial[k] = existing_values[0][k]
elif len(existing_values) > 1:
mixed_values.add(k)
initial[k] = None
kwargs = super().get_form_kwargs()
kwargs['event'] = self.request.event
kwargs['prefix'] = 'bulkedit'
kwargs['initial'] = initial
kwargs['queryset'] = self.get_queryset()
kwargs['mixed_values'] = mixed_values
if not self.is_submitted:
kwargs['data'] = None
kwargs['files'] = None
return kwargs
def post(self, request, *args, **kwargs):
form = self.get_form()
is_valid = (
self.is_submitted and
form.is_valid() and
self.quota_formset.is_valid() and
(not self.list_formset or self.list_formset.is_valid()) and
all(f.is_valid() for f in self.itemvar_forms)and
all(f.is_valid() for f in self.meta_forms)
)
if is_valid:
return self.form_valid(form)
else:
if self.is_submitted:
messages.error(self.request, _('We could not save your changes. See below for details.'))
return self.form_invalid(form)
def save_meta(self):
for f in self.meta_forms:
if f.prefix + 'value' not in self.request.POST.getlist('_bulk'):
continue
if f.cleaned_data.get('value'):
for obj in self.get_queryset():
SubEventMetaValue.objects.update_or_create(
property=f.instance.property,
subevent=obj,
defaults={
'value': f.cleaned_data['value']
}
)
else:
SubEventMetaValue.objects.filter(
property=f.instance.property,
subevent__in=self.get_queryset()
).delete()
def save_itemvars(self):
for f in self.itemvar_forms:
u = {}
if f.prefix + 'price' in self.request.POST.getlist('_bulk'):
u['price'] = f.cleaned_data.get('price')
if f.prefix + 'disabled' in self.request.POST.getlist('_bulk'):
u['disabled'] = f.cleaned_data.get('disabled')
if not u:
continue
if isinstance(f, SubEventItemForm):
if u.get('price') is None and not u.get('disabled'):
SubEventItem.objects.filter(
subevent__in=self.get_queryset(),
item=f.instance.item,
).delete()
else:
for obj in self.get_queryset():
SubEventItem.objects.update_or_create(
subevent=obj,
item=f.instance.item,
defaults=u
)
elif isinstance(f, SubEventItemVariationForm):
if u.get('price') is None and not u.get('disabled'):
SubEventItemVariation.objects.filter(
subevent__in=self.get_queryset(),
variation=f.instance.variation,
).delete()
else:
for obj in self.get_queryset():
SubEventItemVariation.objects.update_or_create(
subevent=obj,
variation=f.instance.variation,
defaults=u
)
@transaction.atomic()
def form_valid(self, form):
log_entries = []
# Main form
form.save()
data = {
k: v for k, v in form.cleaned_data.items() if k in form.changed_data
}
data['_raw_bulk_data'] = self.request.POST.dict()
for obj in self.get_queryset():
log_entries.append(
obj.log_action('pretix.subevent.changed', data=data, user=self.request.user, save=False)
)
# Formsets
if '__quotas' in self.request.POST.getlist('_bulk'):
self.save_quota_formset(log_entries)
if '__checkinlists' in self.request.POST.getlist('_bulk'):
self.save_list_formset(log_entries)
self.save_itemvars()
self.save_meta()
if connections['default'].features.can_return_rows_from_bulk_insert:
LogEntry.objects.bulk_create(log_entries, batch_size=200)
LogEntry.bulk_postprocess(log_entries)
else:
for le in log_entries:
le.save()
LogEntry.bulk_postprocess(log_entries)
self.request.event.cache.clear()
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)