mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Bulk creation for event series dates (#848)
* copy-from things * Some frontend * rrule UI * . * Fixes * UI improvements * First test * Tests
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import copy
|
||||
from datetime import datetime
|
||||
|
||||
from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rruleset
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import transaction
|
||||
@@ -7,19 +9,25 @@ from django.db.models import F, IntegerField, OuterRef, Prefetch, Subquery, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.forms import inlineformset_factory
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
|
||||
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
from pretix.base.models.event import SubEvent, SubEventMetaValue
|
||||
from pretix.base.models.items import Quota, SubEventItem, SubEventItemVariation
|
||||
from pretix.base.models.items import (
|
||||
ItemVariation, Quota, SubEventItem, SubEventItemVariation,
|
||||
)
|
||||
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
|
||||
from pretix.control.forms.checkin import CheckinListForm
|
||||
from pretix.control.forms.filter import SubEventFilterForm
|
||||
from pretix.control.forms.item import QuotaForm
|
||||
from pretix.control.forms.subevents import (
|
||||
CheckinListFormSet, QuotaFormSet, SubEventForm, SubEventItemForm,
|
||||
SubEventItemVariationForm, SubEventMetaValueForm,
|
||||
CheckinListFormSet, QuotaFormSet, RRuleFormSet, SubEventBulkForm,
|
||||
SubEventForm, SubEventItemForm, SubEventItemVariationForm,
|
||||
SubEventMetaValueForm,
|
||||
)
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views import PaginationMixin
|
||||
@@ -92,7 +100,7 @@ class SubEventDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
if self.get_object().orderposition_set.count() > 0:
|
||||
messages.error(request, pgettext_lazy('subevent', 'A date can not be deleted if orders already have been '
|
||||
'placed.'))
|
||||
'placed.'))
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
@@ -103,7 +111,7 @@ class SubEventDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
|
||||
if self.object.orderposition_set.count() > 0:
|
||||
messages.error(request, pgettext_lazy('subevent', 'A date can not be deleted if orders already have been '
|
||||
'placed.'))
|
||||
'placed.'))
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
elif not self.object.allow_delete(): # checking if this is the last date in the event series
|
||||
messages.error(request, pgettext_lazy('subevent', 'The last date of an event series can not be deleted.'))
|
||||
@@ -142,7 +150,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
extra = 0
|
||||
kwargs = {}
|
||||
|
||||
if self.copy_from:
|
||||
if self.copy_from and self.request.method != "POST":
|
||||
kwargs['initial'] = [
|
||||
{
|
||||
'name': cl.name,
|
||||
@@ -152,7 +160,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
} for cl in self.copy_from.checkinlist_set.prefetch_related('limit_products')
|
||||
]
|
||||
extra = len(kwargs['initial'])
|
||||
elif not self.object:
|
||||
elif not self.object and self.request.method != "POST":
|
||||
kwargs['initial'] = [
|
||||
{
|
||||
'name': '',
|
||||
@@ -179,7 +187,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
extra = 0
|
||||
kwargs = {}
|
||||
|
||||
if self.copy_from:
|
||||
if self.copy_from and self.request.method != "POST":
|
||||
kwargs['initial'] = [
|
||||
{
|
||||
'size': q.size,
|
||||
@@ -199,9 +207,11 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
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)
|
||||
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:
|
||||
@@ -285,7 +295,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
|
||||
@cached_property
|
||||
def copy_from(self):
|
||||
if self.request.GET.get("copy_from") and not getattr(self, 'object'):
|
||||
if self.request.GET.get("copy_from") and not getattr(self, 'object', None):
|
||||
try:
|
||||
return self.request.event.subevents.get(pk=self.request.GET.get("copy_from"))
|
||||
except SubEvent.DoesNotExist:
|
||||
@@ -428,6 +438,7 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi
|
||||
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
|
||||
form.instance.log_action('pretix.subevent.added', data=dict(form.cleaned_data), user=self.request.user)
|
||||
|
||||
self.save_formset(form.instance)
|
||||
@@ -435,6 +446,239 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi
|
||||
for f in self.itemvar_forms:
|
||||
f.instance.subevent = form.instance
|
||||
f.save()
|
||||
self.object = form.instance
|
||||
for f in self.meta_forms:
|
||||
f.instance.subevent = form.instance
|
||||
self.save_meta()
|
||||
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 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 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'
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['rrule_formset'] = self.rrule_formset
|
||||
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 = {}
|
||||
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()
|
||||
)) 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()
|
||||
)) if i.presale_start else None
|
||||
else:
|
||||
kwargs['instance'] = SubEvent(event=self.request.event)
|
||||
kwargs['initial'] = initial
|
||||
return kwargs
|
||||
|
||||
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
|
||||
cnt = 0
|
||||
for rdate in self.get_rrule_set():
|
||||
se = copy.copy(form.instance)
|
||||
|
||||
se.date_from = make_aware(datetime.combine(rdate, form.cleaned_data['time_from']), tz)
|
||||
se.date_to = (
|
||||
make_aware(datetime.combine(rdate, form.cleaned_data['time_to']), tz)
|
||||
if form.cleaned_data.get('time_to')
|
||||
else None
|
||||
)
|
||||
se.date_admission = (
|
||||
make_aware(datetime.combine(rdate, form.cleaned_data['time_admission']), tz)
|
||||
if form.cleaned_data.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()
|
||||
se.log_action('pretix.subevent.added', data=dict(form.cleaned_data), user=self.request.user)
|
||||
|
||||
for f in self.meta_forms:
|
||||
if f.cleaned_data.get('value'):
|
||||
i = copy.copy(f.instance)
|
||||
i.subevent = se
|
||||
i.save()
|
||||
|
||||
for f in self.formset.forms:
|
||||
if self.formset._should_delete_form(f):
|
||||
continue
|
||||
i = copy.copy(f.instance)
|
||||
i.subevent = se
|
||||
i.event = se.event
|
||||
i.save()
|
||||
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
|
||||
]))
|
||||
i.items.add(*[_i for _i in selected_items])
|
||||
i.variations.add(*[_i for _i in selected_variations])
|
||||
|
||||
change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
|
||||
change_data['id'] = i.pk
|
||||
i.log_action(action='pretix.event.quota.added', user=self.request.user, data=change_data)
|
||||
se.log_action('pretix.subevent.quota.added', user=self.request.user, data=change_data)
|
||||
|
||||
for f in self.cl_formset.forms:
|
||||
if self.cl_formset._should_delete_form(f):
|
||||
continue
|
||||
i = copy.copy(f.instance)
|
||||
i.subevent = se
|
||||
i.event = se.event
|
||||
i.save()
|
||||
i.limit_products.add(*f.cleaned_data.get('limit_products', []))
|
||||
change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
|
||||
change_data['id'] = i.pk
|
||||
i.log_action(action='pretix.event.checkinlist.added', user=self.request.user, data=change_data)
|
||||
|
||||
for f in self.itemvar_forms:
|
||||
i = copy.copy(f.instance)
|
||||
i.subevent = se
|
||||
i.save()
|
||||
|
||||
cnt += 1
|
||||
|
||||
messages.success(self.request, pgettext_lazy('subevent', '{} new dates have been created.').format(cnt))
|
||||
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)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
Reference in New Issue
Block a user