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:
Raphael Michel
2018-04-03 18:21:27 +02:00
committed by GitHub
parent 8564f93706
commit 7939503a11
15 changed files with 3820 additions and 21 deletions

View File

@@ -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)