diff --git a/src/pretix/control/forms/filter.py b/src/pretix/control/forms/filter.py index fbca154f31..19db3f309d 100644 --- a/src/pretix/control/forms/filter.py +++ b/src/pretix/control/forms/filter.py @@ -1,4 +1,4 @@ -from datetime import datetime, time +from datetime import datetime, time, timedelta from decimal import Decimal from urllib.parse import urlencode @@ -766,10 +766,15 @@ class SubEventFilterForm(FilterForm): ), required=False ) - date = forms.DateField( - label=_('Date'), + date_from = forms.DateField( + label=_('Date from'), required=False, - widget=DatePickerWidget + widget=DatePickerWidget, + ) + date_until = forms.DateField( + label=_('Date until'), + required=False, + widget=DatePickerWidget, ) weekday = forms.ChoiceField( label=_('Weekday'), @@ -796,7 +801,8 @@ class SubEventFilterForm(FilterForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['date'].widget = DatePickerWidget() + self.fields['date_from'].widget = DatePickerWidget() + self.fields['date_until'].widget = DatePickerWidget() def filter_qs(self, qs): fdata = self.cleaned_data @@ -838,19 +844,21 @@ class SubEventFilterForm(FilterForm): Q(name__icontains=i18ncomp(query)) | Q(location__icontains=query) ) - if fdata.get('date'): - date_start = make_aware(datetime.combine( - fdata.get('date'), + if fdata.get('date_until'): + date_end = make_aware(datetime.combine( + fdata.get('date_until') + timedelta(days=1), time(hour=0, minute=0, second=0, microsecond=0) ), get_current_timezone()) - date_end = make_aware(datetime.combine( - fdata.get('date'), - time(hour=23, minute=59, second=59, microsecond=999999) - ), get_current_timezone()) qs = qs.filter( - Q(date_to__isnull=True, date_from__gte=date_start, date_from__lte=date_end) | - Q(date_to__isnull=False, date_from__lte=date_end, date_to__gte=date_start) + Q(date_to__isnull=True, date_from__lt=date_end) | + Q(date_to__isnull=False, date_to__lt=date_end) ) + if fdata.get('date_from'): + date_start = make_aware(datetime.combine( + fdata.get('date_from'), + time(hour=0, minute=0, second=0, microsecond=0) + ), get_current_timezone()) + qs = qs.filter(date_from__gte=date_start) if fdata.get('ordering'): qs = qs.order_by(self.get_order_by()) diff --git a/src/pretix/control/forms/renderers.py b/src/pretix/control/forms/renderers.py index 7b7edc2272..41a55bdbe4 100644 --- a/src/pretix/control/forms/renderers.py +++ b/src/pretix/control/forms/renderers.py @@ -1,4 +1,4 @@ -from bootstrap3.renderers import FieldRenderer +from bootstrap3.renderers import FieldRenderer, InlineFieldRenderer from bootstrap3.text import text_value from django.forms import CheckboxInput from django.forms.utils import flatatt @@ -58,3 +58,40 @@ class ControlFieldRenderer(FieldRenderer): optional=not required and not isinstance(self.widget, CheckboxInput) ) + html return html + + +class BulkEditMixin: + + def __init__(self, *args, **kwargs): + kwargs['layout'] = self.layout + super().__init__(*args, **kwargs) + + def wrap_field(self, html): + field_class = self.get_field_class() + name = '{}{}'.format(self.field.form.prefix, self.field.name) + checked = self.field.form.data and name in self.field.form.data.getlist('_bulk') + html = ( + '
' + '' + '
' + '{html}' + '
' + '
' + ).format( + klass=field_class or '', + name=name, + label=pgettext('form_bulk', 'change'), + checked='checked' if checked else '', + html=html + ) + return html + + +class BulkEditFieldRenderer(BulkEditMixin, FieldRenderer): + layout = 'horizontal' + + +class InlineBulkEditFieldRenderer(BulkEditMixin, InlineFieldRenderer): + layout = 'inline' diff --git a/src/pretix/control/forms/subevents.py b/src/pretix/control/forms/subevents.py index 1bbcb7ac44..d06439202b 100644 --- a/src/pretix/control/forms/subevents.py +++ b/src/pretix/control/forms/subevents.py @@ -1,8 +1,9 @@ -from datetime import timedelta +from datetime import datetime, timedelta from urllib.parse import urlencode from django import forms from django.forms import formset_factory +from django.forms.utils import ErrorDict from django.urls import reverse from django.utils.dates import MONTHS, WEEKDAYS from django.utils.functional import cached_property @@ -11,6 +12,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy from i18nfield.forms import I18nInlineFormSet from pretix.base.forms import I18nModelForm +from pretix.base.forms.widgets import DatePickerWidget, TimePickerWidget from pretix.base.models.event import SubEvent, SubEventMetaValue from pretix.base.models.items import SubEventItem from pretix.base.reldate import RelativeDateTimeField @@ -88,6 +90,142 @@ class SubEventBulkForm(SubEventForm): del self.fields['date_admission'] +class NullBooleanSelect(forms.NullBooleanSelect): + def __init__(self, attrs=None): + choices = ( + ('unknown', _('Keep the current values')), + ('true', _('Yes')), + ('false', _('No')), + ) + super(forms.NullBooleanSelect, self).__init__(attrs, choices) + + +class SubEventBulkEditForm(I18nModelForm): + def __init__(self, *args, **kwargs): + self.mixed_values = kwargs.pop('mixed_values') + self.queryset = kwargs.pop('queryset') + super().__init__(*args, **kwargs) + self.fields['location'].widget.attrs['rows'] = '3' + + for k in ('name', 'location', 'frontpage_text'): + # i18n fields + if k in self.mixed_values: + self.fields[k].widget.attrs['placeholder'] = '[{}]'.format(_('Selection contains various values')) + else: + self.fields[k].widget.attrs['placeholder'] = '' + self.fields[k].one_required = False + + for k in ('geo_lat', 'geo_lon'): + # scalar fields + if k in self.mixed_values: + self.fields[k].widget.attrs['placeholder'] = '[{}]'.format(_('Selection contains various values')) + else: + self.fields[k].widget.attrs['placeholder'] = '' + self.fields[k].widget.is_required = False + self.fields[k].required = False + + for k in ('date_from', 'date_to', 'date_admission', 'presale_start', 'presale_end'): + self.fields[k + '_day'] = forms.DateField( + label=self._meta.model._meta.get_field(k).verbose_name, + help_text=self._meta.model._meta.get_field(k).help_text, + widget=DatePickerWidget(), + required=False, + ) + self.fields[k + '_time'] = forms.TimeField( + label=self._meta.model._meta.get_field(k).verbose_name, + help_text=self._meta.model._meta.get_field(k).help_text, + widget=TimePickerWidget(), + required=False, + ) + + class Meta: + model = SubEvent + localized_fields = '__all__' + fields = [ + 'name', + 'location', + 'frontpage_text', + 'geo_lat', + 'geo_lon', + 'is_public', + 'active', + ] + field_classes = { + } + widgets = { + } + + def save(self, commit=True): + objs = list(self.queryset) + fields = set() + + check_map = { + 'geo_lat': '__geo', + 'geo_lon': '__geo', + } + for k in self.fields: + cb_val = self.prefix + check_map.get(k, k) + if cb_val not in self.data.getlist('_bulk'): + continue + + if k.endswith('_day'): + for obj in objs: + oldval = getattr(obj, k.replace('_day', '')) + cval = self.cleaned_data[k] + if cval is None: + newval = None + if not self._meta.model._meta.get_field(k.replace('_day', '')).null: + continue + elif oldval: + oldval = oldval.astimezone(self.event.timezone) + newval = oldval.replace( + year=cval.year, + month=cval.month, + day=cval.day, + ) + else: + # If there is no previous date/time set, we'll just set to midnight + # If the user also selected a time, this will be overridden anyways + newval = datetime( + year=cval.year, + month=cval.month, + day=cval.day, + tzinfo=self.event.timezone + ) + setattr(obj, k.replace('_day', ''), newval) + fields.add(k.replace('_day', '')) + elif k.endswith('_time'): + for obj in objs: + # If there is no previous date/time set and only a time is changed not the + # date, we instead use the date of the event + oldval = getattr(obj, k.replace('_time', '')) or obj.date_from + cval = self.cleaned_data[k] + if cval is None: + continue + oldval = oldval.astimezone(self.event.timezone) + newval = oldval.replace( + hour=cval.hour, + minute=cval.minute, + second=cval.second, + ) + setattr(obj, k.replace('_time', ''), newval) + fields.add(k.replace('_time', '')) + else: + fields.add(k) + for obj in objs: + setattr(obj, k, self.cleaned_data[k]) + + if fields: + SubEvent.objects.bulk_update(objs, fields, 200) + + def full_clean(self): + if len(self.data) == 0: + # form wasn't submitted + self._errors = ErrorDict() + return + super().full_clean() + + class SubEventItemOrVariationFormMixin: def __init__(self, *args, **kwargs): self.item = kwargs.pop('item') @@ -162,7 +300,7 @@ class SubEventMetaValueForm(forms.ModelForm): def __init__(self, *args, **kwargs): self.property = kwargs.pop('property') self.default = kwargs.pop('default', None) - self.disabled = kwargs.pop('disabled') + self.disabled = kwargs.pop('disabled', False) super().__init__(*args, **kwargs) if self.property.allowed_values: self.fields['value'] = forms.ChoiceField( diff --git a/src/pretix/control/templates/pretixcontrol/order/refund_choose.html b/src/pretix/control/templates/pretixcontrol/order/refund_choose.html index 8ae010e9e8..1d384b7408 100644 --- a/src/pretix/control/templates/pretixcontrol/order/refund_choose.html +++ b/src/pretix/control/templates/pretixcontrol/order/refund_choose.html @@ -15,7 +15,8 @@ {% endblocktrans %} -
+ {% csrf_token %}
{% trans "How should the refund be sent?" %} diff --git a/src/pretix/control/templates/pretixcontrol/subevents/bulk_edit.html b/src/pretix/control/templates/pretixcontrol/subevents/bulk_edit.html new file mode 100644 index 0000000000..292a4ba919 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/subevents/bulk_edit.html @@ -0,0 +1,351 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% load formset_tags %} +{% load captureas %} +{% load static %} +{% load eventsignal %} +{% block title %}{% trans "Change multiple dates" context "subevent" %}{% endblock %} +{% block content %} +

+ {% trans "Change multiple dates" context "subevent" %} + + {% blocktrans trimmed with number=subevents.count %} + {{ number }} selected + {% endblocktrans %} + +

+ + {% csrf_token %} + {% bootstrap_form_errors form %} + {% for f in itemvar_forms %} + {% bootstrap_form_errors f %} + {% endfor %} + +
+ {% trans "General information" %} + {% bootstrap_field form.name layout="bulkedit" %} + {% bootstrap_field form.active layout="bulkedit" %} +
+ {% bootstrap_field form.location layout="bulkedit" %} +
+ +
+
+ +
+
+
+ {% bootstrap_field form.geo_lat layout="inline" %} + {% if global_settings.opencagedata_apikey %} +

+ + {% trans "Geocoding data © OpenStreetMap" %} + +

+ {% endif %} +
+
+ {% bootstrap_field form.geo_lon layout="inline" %} +
+
+
+
+
+
+
+ {% bootstrap_field form.frontpage_text layout="bulkedit" %} + {% bootstrap_field form.is_public layout="bulkedit" %} + {% if meta_forms %} + + {% endif %} +
+
+ {% trans "Timeline" %} +
+ +
+ {% bootstrap_field form.date_from_day layout="bulkedit_inline" form_group_class="" %} +
+
+ {% bootstrap_field form.date_from_time layout="bulkedit_inline" form_group_class="" %} +
+
+
+ +
+ {% bootstrap_field form.date_to_day layout="bulkedit_inline" form_group_class="" %} +
+
+ {% bootstrap_field form.date_to_time layout="bulkedit_inline" form_group_class="" %} +
+
+
+ +
+ {% bootstrap_field form.date_admission_day layout="bulkedit_inline" form_group_class="" %} +
+
+ {% bootstrap_field form.date_admission_time layout="bulkedit_inline" form_group_class="" %} +
+
+
+ +
+ {% bootstrap_field form.presale_start_day layout="bulkedit_inline" form_group_class="" %} +
+
+ {% bootstrap_field form.presale_start_time layout="bulkedit_inline" form_group_class="" %} +
+
+
+ +
+ {% bootstrap_field form.presale_end_day layout="bulkedit_inline" form_group_class="" %} +
+
+ {% bootstrap_field form.presale_end_time layout="bulkedit_inline" form_group_class="" %} +
+
+
+
+ {% trans "Item prices" %} + {% for f in itemvar_forms %} +
+ +
+ {% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="bulkedit_inline" %} +
+
+ {% bootstrap_field f.disabled layout="bulkedit_inline" form_group_class="" %} +
+
+ {% endfor %} +
+
+ {% trans "Quotas" %} + {% if sampled_quotas|default_if_none:"NONE" == "NONE" %} +
+ {% blocktrans trimmed %} + You selected a set of dates that currently have different quota setups. You can therefore + not change their quotas in bulk. If you want, you can set up a new set of quotas to + replace the quota setup of all selected dates. + {% endblocktrans %} +
+ {% endif %} +
+ +
+
+ {{ formset.management_form }} + {% bootstrap_formset_errors formset %} +
+ {% for form in formset %} +
+
+ {{ form.id }} + {% bootstrap_field form.DELETE form_group_class="" layout="inline" %} +
+
+

+
+
+ {% bootstrap_field form.name layout='inline' form_group_class="" %} +
+
+ +
+
+

+
+
+ {% bootstrap_form_errors form %} + {% bootstrap_field form.size layout="control" %} + {% bootstrap_field form.itemvars layout="control" %} + {% bootstrap_field form.release_after_exit layout="control" %} +
+
+ {% endfor %} +
+ +

+ +

+
+
+
+
+

 

+
+ {% trans "Check-in lists" %} + {% if sampled_lists|default_if_none:"NONE" == "NONE" %} +
+ {% blocktrans trimmed %} + You selected a set of dates that currently have different check-in list setups. You can + therefore not change their check-in lists in bulk. + {% endblocktrans %} +
+ {% else %} +
+ +
+
+ {{ cl_formset.management_form }} + {% bootstrap_formset_errors cl_formset %} +
+ {% for form in cl_formset %} +
+
+ {{ form.id }} + {% bootstrap_field form.DELETE form_group_class="" layout="inline" %} +
+
+

+
+
+ {% bootstrap_field form.name layout='inline' form_group_class="" %} +
+
+ +
+
+

+
+
+ {% bootstrap_form_errors form %} + {% bootstrap_field form.include_pending layout="control" %} + {% bootstrap_field form.all_products layout="control" %} + {% bootstrap_field form.limit_products layout="control" %} + {% bootstrap_field form.allow_entry_after_exit layout="control" %} + {% if form.gates %} + {% bootstrap_field form.gates layout="control" %} + {% endif %} +
+
+ {% endfor %} +
+ +

+ +

+
+
+ {% endif %} +
+
+ +
+ +{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/subevents/index.html b/src/pretix/control/templates/pretixcontrol/subevents/index.html index 1a343bf2e4..bdec4ae962 100644 --- a/src/pretix/control/templates/pretixcontrol/subevents/index.html +++ b/src/pretix/control/templates/pretixcontrol/subevents/index.html @@ -22,14 +22,17 @@ {% else %}
-
+
{% bootstrap_field filter_form.query layout='inline' %}
-
+
{% bootstrap_field filter_form.status layout='inline' %}
- {% bootstrap_field filter_form.date layout='inline' %} + {% bootstrap_field filter_form.date_from layout='inline' %} +
+
+ {% bootstrap_field filter_form.date_until layout='inline' %}
{% bootstrap_field filter_form.weekday layout='inline' %} @@ -43,16 +46,21 @@
-

- - {% trans "Create a new date" context "subevent" %} - - {% trans "Create many new dates" context "subevent" %} -

+ {% if "can_change_event_settings" in request.eventpermset %} +

+ + {% trans "Create a new date" context "subevent" %} + + {% trans "Create many new dates" context "subevent" %} +

+ {% endif %}
{% csrf_token %} +
@@ -67,21 +75,33 @@ + {% if "can_change_event_settings" in request.eventpermset %} + + + + + {% endif %} {% for s in subevents %} @@ -150,6 +170,10 @@ + diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 2ebd95149f..54202fad5a 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -174,6 +174,7 @@ urlpatterns = [ url(r'^subevents/add$', subevents.SubEventCreate.as_view(), name='event.subevents.add'), url(r'^subevents/bulk_add$', subevents.SubEventBulkCreate.as_view(), name='event.subevents.bulk'), url(r'^subevents/bulk_action$', subevents.SubEventBulkAction.as_view(), name='event.subevents.bulkaction'), + url(r'^subevents/bulk_edit$', subevents.SubEventBulkEdit.as_view(), name='event.subevents.bulkedit'), url(r'^items/$', item.ItemList.as_view(), name='event.items'), url(r'^items/add$', item.ItemCreate.as_view(), name='event.items.add'), url(r'^items/(?P\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'), diff --git a/src/pretix/control/views/subevents.py b/src/pretix/control/views/subevents.py index 4d1fb40511..74137f3b3c 100644 --- a/src/pretix/control/views/subevents.py +++ b/src/pretix/control/views/subevents.py @@ -1,14 +1,17 @@ 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 F, IntegerField, OuterRef, Prefetch, Subquery, Sum -from django.db.models.functions import Coalesce +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, HttpResponseRedirect +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 @@ -16,7 +19,9 @@ 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, ListView, UpdateView +from django.views.generic import ( + CreateView, DeleteView, FormView, ListView, UpdateView, +) from pretix.base.models import CartPosition, LogEntry from pretix.base.models.checkin import CheckinList @@ -31,24 +36,27 @@ 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, SubEventBulkForm, - SubEventForm, SubEventItemForm, SubEventItemVariationForm, - SubEventMetaValueForm, TimeFormSet, + 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 SubEventList(EventPermissionRequiredMixin, PaginationMixin, ListView): - model = SubEvent - context_object_name = 'subevents' - template_name = 'pretixcontrol/subevents/index.html' - permission = 'can_change_settings' +class SubEventQueryMixin: - def get_queryset(self): + @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( @@ -56,18 +64,39 @@ class SubEventList(EventPermissionRequiredMixin, PaginationMixin, ListView): ).values( 's' ) - - qs = self.request.event.subevents.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') - ) + 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 @@ -95,10 +124,6 @@ class SubEventList(EventPermissionRequiredMixin, PaginationMixin, ListView): ) return ctx - @cached_property - def filter_form(self): - return SubEventFilterForm(data=self.request.GET) - class SubEventDelete(EventPermissionRequiredMixin, DeleteView): model = SubEvent @@ -535,19 +560,13 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi return formlist -class SubEventBulkAction(EventPermissionRequiredMixin, View): +class SubEventBulkAction(SubEventQueryMixin, EventPermissionRequiredMixin, View): permission = 'can_change_settings' - @cached_property - def objects(self): - return self.request.event.subevents.filter( - id__in=self.request.POST.getlist('subevent') - ) - @transaction.atomic def post(self, request, *args, **kwargs): if request.POST.get('action') == 'disable': - for obj in self.objects: + for obj in self.get_queryset(): obj.log_action( 'pretix.subevent.changed', user=self.request.user, data={ 'active': False @@ -557,7 +576,7 @@ class SubEventBulkAction(EventPermissionRequiredMixin, View): 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.objects: + for obj in self.get_queryset(): obj.log_action( 'pretix.subevent.changed', user=self.request.user, data={ 'active': True @@ -568,11 +587,11 @@ class SubEventBulkAction(EventPermissionRequiredMixin, View): 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.objects.filter(orderposition__isnull=True), - 'forbidden': self.objects.filter(orderposition__isnull=False), + '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.objects: + for obj in self.get_queryset(): if obj.allow_delete(): CartPosition.objects.filter(addon_to__subevent=obj).delete() obj.cartposition_set.all().delete() @@ -899,3 +918,537 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea 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 'itemvar' 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) diff --git a/src/pretix/settings.py b/src/pretix/settings.py index 2b86c8d0a9..f53d825026 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -720,6 +720,8 @@ BOOTSTRAP3 = { 'default': 'bootstrap3.renderers.FieldRenderer', 'inline': 'bootstrap3.renderers.InlineFieldRenderer', 'control': 'pretix.control.forms.renderers.ControlFieldRenderer', + 'bulkedit': 'pretix.control.forms.renderers.BulkEditFieldRenderer', + 'bulkedit_inline': 'pretix.control.forms.renderers.InlineBulkEditFieldRenderer', 'checkout': 'pretix.presale.forms.renderers.CheckoutFieldRenderer', }, } @@ -758,3 +760,6 @@ OAUTH2_PROVIDER = { COUNTRIES_OVERRIDE = { 'XK': _('Kosovo'), } + +DATA_UPLOAD_MAX_NUMBER_FIELDS = 25000 +DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10 MB diff --git a/src/pretix/static/pretixcontrol/js/ui/main.js b/src/pretix/static/pretixcontrol/js/ui/main.js index 092f3f83d8..40ea6acfd4 100644 --- a/src/pretix/static/pretixcontrol/js/ui/main.js +++ b/src/pretix/static/pretixcontrol/js/ui/main.js @@ -546,6 +546,25 @@ var form_handlers = function (el) { ); }); + el.find(".bulk-edit-field-group").each(function () { + var $checkbox = $(this).find("input[type=checkbox][name=_bulk]"); + var $content = $(this).find(".field-content"); + var $fields = $content.find("input, select, textarea, button"); + + var update = function () { + var isChecked = $checkbox.prop("checked"); + $content.toggleClass("enabled", isChecked); + $fields.attr("tabIndex", isChecked ? 0 : -1); + } + $content.on("focusin change click", function () { + if ($checkbox.prop("checked")) return; + $checkbox.prop("checked", true); + update(); + }); + $checkbox.on('change', update) + update(); + }); + el.find("input[name*=question], select[name*=question]").change(questions_toggle_dependent); questions_toggle_dependent(); }; @@ -563,7 +582,6 @@ $(function () { } ); $("[data-formset]").on("formAdded", "div", function (event) { - console.log("formAdded") form_handlers($(event.target)); }); $(document).on("click", ".variations .variations-select-all", function (e) { @@ -676,7 +694,7 @@ $(function () { var update = function () { var all_true = true; var all_false = true; - $toggle.closest("table").find("td:first-child input[type=checkbox]").each(function () { + $toggle.closest("table").find("tbody td:first-child input[type=checkbox]").each(function () { if ($(this).prop("checked")) { all_false = false; } else { @@ -690,11 +708,29 @@ $(function () { } else { $toggle.prop("checked", false).prop("indeterminate", true); } + var is_incomplete = $toggle.prop("indeterminate") || !$toggle.prop("checked") + $toggle.closest("table").find(".table-select-all").toggleClass( + "hidden", is_incomplete + ) + if (is_incomplete) { + $toggle.closest("table").find(".table-select-all input").prop( + "checked", false + ) + } }; $(this).closest("table").find("td:first-child input[type=checkbox]").change(update); $(this).change(function (ev) { - $(this).closest("table").find("td:first-child input[type=checkbox]").prop("checked", $(this).prop("checked")); + $(this).closest("table").find("tbody td:first-child input[type=checkbox]").prop("checked", $(this).prop("checked")); + var is_incomplete = $(this).prop("indeterminate") || !$(this).prop("checked") + $(this).closest("table").find(".table-select-all").toggleClass( + "hidden", is_incomplete + ) + if (is_incomplete) { + $(this).closest("table").find(".table-select-all input").prop( + "checked", false + ) + } }); }); diff --git a/src/pretix/static/pretixcontrol/scss/_forms.scss b/src/pretix/static/pretixcontrol/scss/_forms.scss index 18279f0463..d45a761182 100644 --- a/src/pretix/static/pretixcontrol/scss/_forms.scss +++ b/src/pretix/static/pretixcontrol/scss/_forms.scss @@ -613,3 +613,33 @@ table td > .checkbox input[type="checkbox"] { border-bottom: 1px solid $input-border; padding-bottom: 5px; } + +.bulk-edit-field-group { + .field-toggle { + font-weight: normal; + display: inline-block; + background: $gray-lighter; + padding: 2px 8px 4px; + border-top-left-radius: $border-radius-base; + border-top-right-radius: $border-radius-base; + margin-bottom: 0; + input { + position: relative; + top: 2px; + } + } + .field-content { + border: 2px solid $gray-lighter; + padding: 15px; + opacity: 0.5; + .datepickerfield::placeholder, .timepickerfield::placeholder { + opacity: 0; + } + &.enabled { + opacity: 1; + .datepickerfield::placeholder, .timepickerfield::placeholder { + opacity: 1; + } + } + } +} diff --git a/src/tests/base/__init__.py b/src/tests/base/__init__.py index 1fbe325330..abdd76fbaa 100644 --- a/src/tests/base/__init__.py +++ b/src/tests/base/__init__.py @@ -34,16 +34,32 @@ def extract_form_fields(soup): if field['type'] in ('checkbox', 'radio'): if field.has_attr('checked') and field.has_attr('name'): - data[field['name']] = field.get('value', 'on') + if field['name'] in data: + if not isinstance(data[field['name']], list): + data[field['name']] = [data[field['name']]] + data[field['name']].append(field.get('value', 'on')) + else: + data[field['name']] = field.get('value', 'on') continue elif field.has_attr('name'): # single element name/value fields - data[field['name']] = field.get('value', '') + value = field.get('value', '') + if field['name'] in data: + if not isinstance(data[field['name']], list): + data[field['name']] = [data[field['name']]] + data[field['name']].append(value) + else: + data[field['name']] = value continue # textareas for textarea in soup.findAll('textarea'): - data[textarea['name']] = textarea.text or '' + if textarea['name'] in data: + if not isinstance(data[textarea['name']], list): + data[textarea['name']] = [data[textarea['name']]] + data[textarea['name']].append(textarea.text or '') + else: + data[textarea['name']] = textarea.text or '' # select fields for select in soup.find_all('select'): @@ -66,6 +82,11 @@ def extract_form_fields(soup): else: value = [option['value'] for option in selected_options] - data[select['name']] = value + if select['name'] in data: + if not isinstance(data[select['name']], list): + data[select['name']] = [data[select['name']]] + data[select['name']].append(value) + else: + data[select['name']] = value return data diff --git a/src/tests/control/test_events.py b/src/tests/control/test_events.py index 69caa09e6a..25cbbd3e24 100644 --- a/src/tests/control/test_events.py +++ b/src/tests/control/test_events.py @@ -9,10 +9,7 @@ from i18nfield.strings import LazyI18nString from pytz import timezone from tests.base import SoupTest, extract_form_fields -from pretix.base.models import ( - Event, Order, OrderPosition, Organizer, SubEvent, Team, User, -) -from pretix.base.models.items import SubEventItem +from pretix.base.models import Event, Order, Organizer, Team, User from pretix.testutils.mock import mocker_context @@ -996,646 +993,6 @@ class EventsTest(SoupTest): assert doc.select(".has-error") -class SubEventsTest(SoupTest): - @scopes_disabled() - def setUp(self): - super().setUp() - self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy') - self.orga1 = Organizer.objects.create(name='CCC', slug='ccc') - self.event1 = Event.objects.create( - organizer=self.orga1, name='30C3', slug='30c3', - date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc), - plugins='pretix.plugins.banktransfer,tests.testdummy', - has_subevents=True - ) - - t = Team.objects.create(organizer=self.orga1, can_create_events=True, can_change_event_settings=True, - can_change_items=True) - t.members.add(self.user) - t.limit_events.add(self.event1) - self.ticket = self.event1.items.create(name='Early-bird ticket', - category=None, default_price=23, - admission=True) - - self.client.login(email='dummy@dummy.dummy', password='dummy') - - self.subevent1 = self.event1.subevents.create(name='SE1', date_from=now()) - self.subevent2 = self.event1.subevents.create(name='SE2', date_from=now()) - - def test_list(self): - doc = self.get_doc('/control/event/ccc/30c3/subevents/') - tabletext = doc.select("#page-wrapper .table")[0].text - self.assertIn("SE1", tabletext) - - def test_create(self): - doc = self.get_doc('/control/event/ccc/30c3/subevents/add') - assert doc.select("input[name=quotas-TOTAL_FORMS]") - doc = self.post_doc('/control/event/ccc/30c3/subevents/add', { - 'name_0': 'SE2', - 'active': 'on', - 'date_from_0': '2017-07-01', - 'date_from_1': '10:00:00', - 'date_to_0': '2017-07-01', - 'date_to_1': '12:00:00', - 'location_0': 'Hamburg', - 'presale_start_0': '2017-06-20', - 'presale_start_1': '10:00:00', - 'checkinlist_set-TOTAL_FORMS': '1', - 'checkinlist_set-INITIAL_FORMS': '0', - 'checkinlist_set-MIN_NUM_FORMS': '0', - 'checkinlist_set-MAX_NUM_FORMS': '1000', - 'checkinlist_set-0-name': 'Default', - 'checkinlist_set-0-all_products': 'on', - 'quotas-TOTAL_FORMS': '1', - 'quotas-INITIAL_FORMS': '0', - 'quotas-MIN_NUM_FORMS': '0', - 'quotas-MAX_NUM_FORMS': '1000', - 'quotas-0-name': 'Q1', - 'quotas-0-size': '50', - 'quotas-0-itemvars': str(self.ticket.pk), - 'item-%d-price' % self.ticket.pk: '12' - }) - assert doc.select(".alert-success") - with scopes_disabled(): - se = self.event1.subevents.first() - assert str(se.name) == "SE2" - assert se.active - assert se.date_from.isoformat() == "2017-07-01T10:00:00+00:00" - assert se.date_to.isoformat() == "2017-07-01T12:00:00+00:00" - assert str(se.location) == "Hamburg" - assert se.presale_start.isoformat() == "2017-06-20T10:00:00+00:00" - assert not se.presale_end - assert se.quotas.count() == 1 - q = se.quotas.last() - assert q.name == "Q1" - assert q.size == 50 - assert list(q.items.all()) == [self.ticket] - sei = SubEventItem.objects.get(subevent=se, item=self.ticket) - assert sei.price == 12 - assert se.checkinlist_set.count() == 1 - - def test_modify(self): - doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/' % self.subevent1.pk) - assert doc.select("input[name=quotas-TOTAL_FORMS]") - doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/' % self.subevent1.pk, { - 'name_0': 'SE2', - 'active': 'on', - 'date_from_0': '2017-07-01', - 'date_from_1': '10:00:00', - 'date_to_0': '2017-07-01', - 'date_to_1': '12:00:00', - 'location_0': 'Hamburg', - 'presale_start_0': '2017-06-20', - 'presale_start_1': '10:00:00', - 'quotas-TOTAL_FORMS': '1', - 'quotas-INITIAL_FORMS': '0', - 'quotas-MIN_NUM_FORMS': '0', - 'quotas-MAX_NUM_FORMS': '1000', - 'quotas-0-name': 'Q1', - 'quotas-0-size': '50', - 'quotas-0-itemvars': str(self.ticket.pk), - 'checkinlist_set-TOTAL_FORMS': '1', - 'checkinlist_set-INITIAL_FORMS': '0', - 'checkinlist_set-MIN_NUM_FORMS': '0', - 'checkinlist_set-MAX_NUM_FORMS': '1000', - 'checkinlist_set-0-name': 'Default', - 'checkinlist_set-0-all_products': 'on', - 'item-%d-price' % self.ticket.pk: '12' - }) - assert doc.select(".alert-success") - self.subevent1.refresh_from_db() - se = self.subevent1 - assert str(se.name) == "SE2" - assert se.active - assert se.date_from.isoformat() == "2017-07-01T10:00:00+00:00" - assert se.date_to.isoformat() == "2017-07-01T12:00:00+00:00" - assert str(se.location) == "Hamburg" - assert se.presale_start.isoformat() == "2017-06-20T10:00:00+00:00" - assert not se.presale_end - with scopes_disabled(): - assert se.quotas.count() == 1 - q = se.quotas.last() - assert q.name == "Q1" - assert q.size == 50 - assert list(q.items.all()) == [self.ticket] - sei = SubEventItem.objects.get(subevent=se, item=self.ticket) - assert sei.price == 12 - assert se.checkinlist_set.count() == 1 - - def test_delete(self): - doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk) - assert doc.select("button") - doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk, {}) - assert doc.select(".alert-success") - # deleting the second event - doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent2.pk, {}) - assert doc.select(".alert-success") - with scopes_disabled(): - assert not SubEvent.objects.filter(pk=self.subevent2.pk).exists() - assert not SubEvent.objects.filter(pk=self.subevent1.pk).exists() - - def test_delete_with_orders(self): - with scopes_disabled(): - o = Order.objects.create( - code='FOO', event=self.event1, email='dummy@dummy.test', - status=Order.STATUS_PENDING, - datetime=now(), expires=now() + datetime.timedelta(days=10), - total=14, locale='en' - ) - OrderPosition.objects.create( - order=o, - item=self.ticket, - subevent=self.subevent1, - price=Decimal("14"), - ) - doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk, follow=True) - assert doc.select(".alert-danger") - doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk, {}, follow=True) - assert doc.select(".alert-danger") - with scopes_disabled(): - assert self.event1.subevents.filter(pk=self.subevent1.pk).exists() - - def test_create_bulk(self): - with scopes_disabled(): - self.event1.subevents.all().delete() - self.event1.settings.timezone = 'Europe/Berlin' - - doc = self.get_doc('/control/event/ccc/30c3/subevents/bulk_add') - assert doc.select("input[name=rruleformset-TOTAL_FORMS]") - doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', { - 'rruleformset-TOTAL_FORMS': '1', - 'rruleformset-INITIAL_FORMS': '0', - 'rruleformset-MIN_NUM_FORMS': '0', - 'rruleformset-MAX_NUM_FORMS': '1000', - 'rruleformset-0-interval': '1', - 'rruleformset-0-freq': 'yearly', - 'rruleformset-0-dtstart': '2018-04-03', - 'rruleformset-0-yearly_same': 'on', - 'rruleformset-0-yearly_bysetpos': '1', - 'rruleformset-0-yearly_byweekday': 'MO', - 'rruleformset-0-yearly_bymonth': '1', - 'rruleformset-0-monthly_same': 'on', - 'rruleformset-0-monthly_bysetpos': '1', - 'rruleformset-0-monthly_byweekday': 'MO', - 'rruleformset-0-end': 'count', - 'rruleformset-0-count': '10', - 'rruleformset-0-until': '2019-04-03', - 'timeformset-TOTAL_FORMS': '1', - 'timeformset-INITIAL_FORMS': '0', - 'timeformset-MIN_NUM_FORMS': '1', - 'timeformset-MAX_NUM_FORMS': '1000', - 'timeformset-0-time_from': '13:29:31', - 'timeformset-0-time_to': '15:29:31', - 'name_0': 'Foo', - 'active': 'on', - 'location_0': 'Loc', - 'time_admission': '', - 'frontpage_text_0': '', - 'rel_presale_start_0': 'unset', - 'rel_presale_start_1': '', - 'rel_presale_start_2': '1', - 'rel_presale_start_3': 'date_from', - 'rel_presale_start_4': '', - 'rel_presale_end_1': '', - 'rel_presale_end_0': 'relative', - 'rel_presale_end_2': '1', - 'rel_presale_end_3': 'date_from', - 'rel_presale_end_4': '13:29:31', - 'quotas-TOTAL_FORMS': '1', - 'quotas-INITIAL_FORMS': '0', - 'quotas-MIN_NUM_FORMS': '0', - 'quotas-MAX_NUM_FORMS': '1000', - 'quotas-0-id': '', - 'quotas-0-name': 'Bar', - 'quotas-0-size': '12', - 'quotas-0-itemvars': str(self.ticket.pk), - 'item-%d-price' % self.ticket.pk: '16', - 'checkinlist_set-TOTAL_FORMS': '1', - 'checkinlist_set-INITIAL_FORMS': '0', - 'checkinlist_set-MIN_NUM_FORMS': '0', - 'checkinlist_set-MAX_NUM_FORMS': '1000', - 'checkinlist_set-0-id': '', - 'checkinlist_set-0-name': 'Foo', - 'checkinlist_set-0-limit_products': str(self.ticket.pk), - }) - assert doc.select(".alert-success") - with scopes_disabled(): - ses = list(self.event1.subevents.order_by('date_from')) - assert len(ses) == 10 - - assert str(ses[0].name) == "Foo" - assert ses[0].date_from.isoformat() == "2018-04-03T11:29:31+00:00" - assert ses[0].date_to.isoformat() == "2018-04-03T13:29:31+00:00" - assert not ses[0].presale_start - assert ses[0].presale_end.isoformat() == "2018-04-02T11:29:31+00:00" - with scopes_disabled(): - assert ses[0].quotas.count() == 1 - assert list(ses[0].quotas.first().items.all()) == [self.ticket] - assert SubEventItem.objects.get(subevent=ses[0], item=self.ticket).price == 16 - assert ses[0].checkinlist_set.count() == 1 - - assert str(ses[1].name) == "Foo" - assert ses[1].date_from.isoformat() == "2019-04-03T11:29:31+00:00" - assert ses[1].date_to.isoformat() == "2019-04-03T13:29:31+00:00" - assert not ses[1].presale_start - assert ses[1].presale_end.isoformat() == "2019-04-02T11:29:31+00:00" - with scopes_disabled(): - assert ses[1].quotas.count() == 1 - assert list(ses[1].quotas.first().items.all()) == [self.ticket] - assert SubEventItem.objects.get(subevent=ses[0], item=self.ticket).price == 16 - assert ses[1].checkinlist_set.count() == 1 - - assert ses[-1].date_from.isoformat() == "2027-04-03T11:29:31+00:00" - - def test_create_bulk_daily_interval(self): - with scopes_disabled(): - self.event1.subevents.all().delete() - self.event1.settings.timezone = 'Europe/Berlin' - - doc = self.get_doc('/control/event/ccc/30c3/subevents/bulk_add') - assert doc.select("input[name=rruleformset-TOTAL_FORMS]") - doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', { - 'rruleformset-TOTAL_FORMS': '1', - 'rruleformset-INITIAL_FORMS': '0', - 'rruleformset-MIN_NUM_FORMS': '0', - 'rruleformset-MAX_NUM_FORMS': '1000', - 'rruleformset-0-interval': '2', - 'rruleformset-0-freq': 'daily', - 'rruleformset-0-dtstart': '2018-04-03', - 'rruleformset-0-yearly_same': 'on', - 'rruleformset-0-yearly_bysetpos': '1', - 'rruleformset-0-yearly_byweekday': 'MO', - 'rruleformset-0-yearly_bymonth': '1', - 'rruleformset-0-monthly_same': 'on', - 'rruleformset-0-monthly_bysetpos': '1', - 'rruleformset-0-monthly_byweekday': 'MO', - 'rruleformset-0-end': 'until', - 'rruleformset-0-count': '10', - 'rruleformset-0-until': '2019-04-03', - 'timeformset-TOTAL_FORMS': '1', - 'timeformset-INITIAL_FORMS': '0', - 'timeformset-MIN_NUM_FORMS': '1', - 'timeformset-MAX_NUM_FORMS': '1000', - 'timeformset-0-time_from': '13:29:31', - 'timeformset-0-time_to': '15:29:31', - 'name_0': 'Foo', - 'active': 'on', - 'frontpage_text_0': '', - 'rel_presale_start_0': 'unset', - 'rel_presale_start_1': '', - 'rel_presale_start_2': '1', - 'rel_presale_start_3': 'date_from', - 'rel_presale_start_4': '', - 'rel_presale_end_1': '', - 'rel_presale_end_0': 'relative', - 'rel_presale_end_2': '1', - 'rel_presale_end_3': 'date_from', - 'rel_presale_end_4': '13:29:31', - 'quotas-TOTAL_FORMS': '1', - 'quotas-INITIAL_FORMS': '0', - 'quotas-MIN_NUM_FORMS': '1', - 'quotas-MAX_NUM_FORMS': '1000', - 'quotas-0-name': 'Q1', - 'quotas-0-size': '50', - 'quotas-0-itemvars': str(self.ticket.pk), - 'checkinlist_set-TOTAL_FORMS': '0', - 'checkinlist_set-INITIAL_FORMS': '0', - 'checkinlist_set-MIN_NUM_FORMS': '0', - 'checkinlist_set-MAX_NUM_FORMS': '1000', - }) - assert doc.select(".alert-success") - with scopes_disabled(): - ses = list(self.event1.subevents.order_by('date_from')) - assert len(ses) == 183 - - assert ses[0].date_from.isoformat() == "2018-04-03T11:29:31+00:00" - assert ses[110].date_from.isoformat() == "2018-11-09T12:29:31+00:00" # DST :) - assert ses[-1].date_from.isoformat() == "2019-04-02T11:29:31+00:00" - - def test_create_bulk_daily_interval_multiple_times(self): - with scopes_disabled(): - self.event1.subevents.all().delete() - self.event1.settings.timezone = 'Europe/Berlin' - - doc = self.get_doc('/control/event/ccc/30c3/subevents/bulk_add') - assert doc.select("input[name=rruleformset-TOTAL_FORMS]") - doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', { - 'rruleformset-TOTAL_FORMS': '1', - 'rruleformset-INITIAL_FORMS': '0', - 'rruleformset-MIN_NUM_FORMS': '0', - 'rruleformset-MAX_NUM_FORMS': '1000', - 'rruleformset-0-interval': '2', - 'rruleformset-0-freq': 'daily', - 'rruleformset-0-dtstart': '2018-04-03', - 'rruleformset-0-yearly_same': 'on', - 'rruleformset-0-yearly_bysetpos': '1', - 'rruleformset-0-yearly_byweekday': 'MO', - 'rruleformset-0-yearly_bymonth': '1', - 'rruleformset-0-monthly_same': 'on', - 'rruleformset-0-monthly_bysetpos': '1', - 'rruleformset-0-monthly_byweekday': 'MO', - 'rruleformset-0-end': 'until', - 'rruleformset-0-count': '10', - 'rruleformset-0-until': '2019-04-03', - 'timeformset-TOTAL_FORMS': '2', - 'timeformset-INITIAL_FORMS': '0', - 'timeformset-MIN_NUM_FORMS': '1', - 'timeformset-MAX_NUM_FORMS': '1000', - 'timeformset-0-time_from': '13:29:31', - 'timeformset-0-time_to': '15:29:31', - 'timeformset-1-time_from': '15:29:31', - 'timeformset-1-time_to': '17:29:31', - 'name_0': 'Foo', - 'active': 'on', - 'frontpage_text_0': '', - 'rel_presale_start_0': 'unset', - 'rel_presale_start_1': '', - 'rel_presale_start_2': '1', - 'rel_presale_start_3': 'date_from', - 'rel_presale_start_4': '', - 'rel_presale_end_1': '', - 'rel_presale_end_0': 'relative', - 'rel_presale_end_2': '1', - 'rel_presale_end_3': 'date_from', - 'rel_presale_end_4': '13:29:31', - 'quotas-TOTAL_FORMS': '1', - 'quotas-INITIAL_FORMS': '0', - 'quotas-MIN_NUM_FORMS': '0', - 'quotas-MAX_NUM_FORMS': '1000', - 'quotas-0-name': 'Q1', - 'quotas-0-size': '50', - 'quotas-0-itemvars': str(self.ticket.pk), - 'checkinlist_set-TOTAL_FORMS': '0', - 'checkinlist_set-INITIAL_FORMS': '0', - 'checkinlist_set-MIN_NUM_FORMS': '0', - 'checkinlist_set-MAX_NUM_FORMS': '1000', - }) - assert doc.select(".alert-success") - with scopes_disabled(): - ses = list(self.event1.subevents.order_by('date_from')) - assert len(ses) == 183 * 2 - - assert ses[0].date_from.isoformat() == "2018-04-03T11:29:31+00:00" - assert ses[1].date_from.isoformat() == "2018-04-03T13:29:31+00:00" - assert ses[220].date_from.isoformat() == "2018-11-09T12:29:31+00:00" # DST :) - assert ses[-1].date_from.isoformat() == "2019-04-02T13:29:31+00:00" - - def test_create_bulk_exclude(self): - with scopes_disabled(): - self.event1.subevents.all().delete() - self.event1.settings.timezone = 'Europe/Berlin' - - doc = self.get_doc('/control/event/ccc/30c3/subevents/bulk_add') - assert doc.select("input[name=rruleformset-TOTAL_FORMS]") - doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', { - 'rruleformset-TOTAL_FORMS': '2', - 'rruleformset-INITIAL_FORMS': '0', - 'rruleformset-MIN_NUM_FORMS': '0', - 'rruleformset-MAX_NUM_FORMS': '1000', - 'rruleformset-0-interval': '1', - 'rruleformset-0-freq': 'daily', - 'rruleformset-0-dtstart': '2018-04-03', - 'rruleformset-0-yearly_same': 'on', - 'rruleformset-0-yearly_bysetpos': '1', - 'rruleformset-0-yearly_byweekday': 'MO', - 'rruleformset-0-yearly_bymonth': '1', - 'rruleformset-0-monthly_same': 'on', - 'rruleformset-0-monthly_bysetpos': '1', - 'rruleformset-0-monthly_byweekday': 'MO', - 'rruleformset-0-end': 'until', - 'rruleformset-0-count': '10', - 'rruleformset-0-until': '2019-04-03', - 'rruleformset-1-interval': '1', - 'rruleformset-1-freq': 'weekly', - 'rruleformset-1-dtstart': '2018-04-03', - 'rruleformset-1-yearly_same': 'on', - 'rruleformset-1-yearly_bysetpos': '1', - 'rruleformset-1-yearly_byweekday': 'MO', - 'rruleformset-1-yearly_bymonth': '1', - 'rruleformset-1-monthly_same': 'on', - 'rruleformset-1-monthly_bysetpos': '1', - 'rruleformset-1-monthly_byweekday': 'MO', - 'rruleformset-1-weekly_byweekday': 'MO', - 'rruleformset-1-end': 'until', - 'rruleformset-1-count': '10', - 'rruleformset-1-until': '2019-04-03', - 'rruleformset-1-exclude': 'on', - 'timeformset-TOTAL_FORMS': '1', - 'timeformset-INITIAL_FORMS': '0', - 'timeformset-MIN_NUM_FORMS': '1', - 'timeformset-MAX_NUM_FORMS': '1000', - 'timeformset-0-time_from': '13:29:31', - 'timeformset-0-time_to': '15:29:31', - 'name_0': 'Foo', - 'active': 'on', - 'frontpage_text_0': '', - 'rel_presale_start_0': 'unset', - 'rel_presale_start_1': '', - 'rel_presale_start_2': '1', - 'rel_presale_start_3': 'date_from', - 'rel_presale_start_4': '', - 'rel_presale_end_1': '', - 'rel_presale_end_0': 'relative', - 'rel_presale_end_2': '1', - 'rel_presale_end_3': 'date_from', - 'rel_presale_end_4': '13:29:31', - 'quotas-TOTAL_FORMS': '1', - 'quotas-INITIAL_FORMS': '0', - 'quotas-MIN_NUM_FORMS': '0', - 'quotas-MAX_NUM_FORMS': '1000', - 'quotas-0-name': 'Q1', - 'quotas-0-size': '50', - 'quotas-0-itemvars': str(self.ticket.pk), - 'checkinlist_set-TOTAL_FORMS': '0', - 'checkinlist_set-INITIAL_FORMS': '0', - 'checkinlist_set-MIN_NUM_FORMS': '0', - 'checkinlist_set-MAX_NUM_FORMS': '1000', - }) - assert doc.select(".alert-success") - with scopes_disabled(): - ses = list(self.event1.subevents.order_by('date_from')) - assert len(ses) == 314 - - assert ses[0].date_from.isoformat() == "2018-04-03T11:29:31+00:00" - assert ses[5].date_from.isoformat() == "2018-04-08T11:29:31+00:00" - assert ses[6].date_from.isoformat() == "2018-04-10T11:29:31+00:00" - - def test_create_bulk_monthly_interval(self): - with scopes_disabled(): - self.event1.subevents.all().delete() - self.event1.settings.timezone = 'Europe/Berlin' - - doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', { - 'rruleformset-TOTAL_FORMS': '1', - 'rruleformset-INITIAL_FORMS': '0', - 'rruleformset-MIN_NUM_FORMS': '0', - 'rruleformset-MAX_NUM_FORMS': '1000', - 'rruleformset-0-interval': '1', - 'rruleformset-0-freq': 'monthly', - 'rruleformset-0-dtstart': '2018-04-03', - 'rruleformset-0-yearly_same': 'on', - 'rruleformset-0-yearly_bysetpos': '1', - 'rruleformset-0-yearly_byweekday': 'MO', - 'rruleformset-0-yearly_bymonth': '1', - 'rruleformset-0-monthly_same': 'off', - 'rruleformset-0-monthly_bysetpos': '-1', - 'rruleformset-0-monthly_byweekday': 'MO,TU,WE,TH,FR', - 'rruleformset-0-weekly_byweekday': 'TH', - 'rruleformset-0-end': 'until', - 'rruleformset-0-count': '10', - 'rruleformset-0-until': '2019-04-03', - 'timeformset-TOTAL_FORMS': '1', - 'timeformset-INITIAL_FORMS': '0', - 'timeformset-MIN_NUM_FORMS': '1', - 'timeformset-MAX_NUM_FORMS': '1000', - 'timeformset-0-time_from': '13:29:31', - 'timeformset-0-time_to': '15:29:31', - 'name_0': 'Foo', - 'active': 'on', - 'frontpage_text_0': '', - 'rel_presale_start_0': 'unset', - 'rel_presale_start_1': '', - 'rel_presale_start_2': '1', - 'rel_presale_start_3': 'date_from', - 'rel_presale_start_4': '', - 'rel_presale_end_0': 'unset', - 'rel_presale_end_1': '', - 'rel_presale_end_2': '1', - 'rel_presale_end_3': 'date_from', - 'rel_presale_end_4': '13:29:31', - 'quotas-TOTAL_FORMS': '1', - 'quotas-INITIAL_FORMS': '0', - 'quotas-MIN_NUM_FORMS': '0', - 'quotas-MAX_NUM_FORMS': '1000', - 'quotas-0-name': 'Q1', - 'quotas-0-size': '50', - 'quotas-0-itemvars': str(self.ticket.pk), - 'checkinlist_set-TOTAL_FORMS': '0', - 'checkinlist_set-INITIAL_FORMS': '0', - 'checkinlist_set-MIN_NUM_FORMS': '0', - 'checkinlist_set-MAX_NUM_FORMS': '1000', - }) - assert doc.select(".alert-success") - with scopes_disabled(): - ses = list(self.event1.subevents.order_by('date_from')) - assert len(ses) == 12 - - assert ses[0].date_from.isoformat() == "2018-04-30T11:29:31+00:00" - assert ses[1].date_from.isoformat() == "2018-05-31T11:29:31+00:00" - assert ses[-1].date_from.isoformat() == "2019-03-29T12:29:31+00:00" - - def test_create_bulk_weekly_interval(self): - with scopes_disabled(): - self.event1.subevents.all().delete() - self.event1.settings.timezone = 'Europe/Berlin' - - doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', { - 'rruleformset-TOTAL_FORMS': '1', - 'rruleformset-INITIAL_FORMS': '0', - 'rruleformset-MIN_NUM_FORMS': '0', - 'rruleformset-MAX_NUM_FORMS': '1000', - 'rruleformset-0-interval': '1', - 'rruleformset-0-freq': 'weekly', - 'rruleformset-0-dtstart': '2018-04-03', - 'rruleformset-0-yearly_same': 'on', - 'rruleformset-0-yearly_bysetpos': '1', - 'rruleformset-0-yearly_byweekday': 'MO', - 'rruleformset-0-yearly_bymonth': '1', - 'rruleformset-0-monthly_same': 'on', - 'rruleformset-0-monthly_bysetpos': '-1', - 'rruleformset-0-monthly_byweekday': 'MO,TU,WE,TH,FR', - 'rruleformset-0-weekly_byweekday': 'TH', - 'rruleformset-0-end': 'until', - 'rruleformset-0-count': '10', - 'rruleformset-0-until': '2019-04-03', - 'timeformset-TOTAL_FORMS': '1', - 'timeformset-INITIAL_FORMS': '0', - 'timeformset-MIN_NUM_FORMS': '1', - 'timeformset-MAX_NUM_FORMS': '1000', - 'timeformset-0-time_from': '13:29:31', - 'timeformset-0-time_to': '15:29:31', - 'name_0': 'Foo', - 'active': 'on', - 'frontpage_text_0': '', - 'rel_presale_start_0': 'unset', - 'rel_presale_start_1': '', - 'rel_presale_start_2': '1', - 'rel_presale_start_3': 'date_from', - 'rel_presale_start_4': '', - 'rel_presale_end_0': 'unset', - 'rel_presale_end_1': '', - 'rel_presale_end_2': '1', - 'rel_presale_end_3': 'date_from', - 'rel_presale_end_4': '13:29:31', - 'quotas-TOTAL_FORMS': '1', - 'quotas-INITIAL_FORMS': '0', - 'quotas-MIN_NUM_FORMS': '0', - 'quotas-MAX_NUM_FORMS': '1000', - 'quotas-0-name': 'Q1', - 'quotas-0-size': '50', - 'quotas-0-itemvars': str(self.ticket.pk), - 'checkinlist_set-TOTAL_FORMS': '0', - 'checkinlist_set-INITIAL_FORMS': '0', - 'checkinlist_set-MIN_NUM_FORMS': '0', - 'checkinlist_set-MAX_NUM_FORMS': '1000', - }) - assert doc.select(".alert-success") - with scopes_disabled(): - ses = list(self.event1.subevents.order_by('date_from')) - assert len(ses) == 52 - - assert ses[0].date_from.isoformat() == "2018-04-05T11:29:31+00:00" - assert ses[1].date_from.isoformat() == "2018-04-12T11:29:31+00:00" - assert ses[-1].date_from.isoformat() == "2019-03-28T12:29:31+00:00" - - def test_delete_bulk(self): - self.subevent2.active = True - self.subevent2.save() - with scopes_disabled(): - o = Order.objects.create( - code='FOO', event=self.event1, email='dummy@dummy.test', - status=Order.STATUS_PENDING, - datetime=now(), expires=now() + datetime.timedelta(days=10), - total=14, locale='en' - ) - OrderPosition.objects.create( - order=o, - item=self.ticket, - subevent=self.subevent1, - price=Decimal("14"), - ) - doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_action', { - 'subevent': [str(self.subevent1.pk), str(self.subevent2.pk)], - 'action': 'delete_confirm' - }, follow=True) - assert doc.select(".alert-success") - with scopes_disabled(): - assert not self.event1.subevents.filter(pk=self.subevent2.pk).exists() - assert self.event1.subevents.get(pk=self.subevent1.pk).active is False - - def test_disable_bulk(self): - self.subevent2.active = True - self.subevent2.save() - doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_action', { - 'subevent': str(self.subevent2.pk), - 'action': 'disable' - }, follow=True) - assert doc.select(".alert-success") - with scopes_disabled(): - assert self.event1.subevents.get(pk=self.subevent2.pk).active is False - - def test_enable_bulk(self): - self.subevent2.active = False - self.subevent2.save() - doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_action', { - 'subevent': str(self.subevent2.pk), - 'action': 'enable' - }, follow=True) - assert doc.select(".alert-success") - with scopes_disabled(): - assert self.event1.subevents.get(pk=self.subevent2.pk).active is True - - class EventDeletionTest(SoupTest): @scopes_disabled() def setUp(self): diff --git a/src/tests/control/test_subevents.py b/src/tests/control/test_subevents.py new file mode 100644 index 0000000000..be98d4a604 --- /dev/null +++ b/src/tests/control/test_subevents.py @@ -0,0 +1,1045 @@ +import datetime +from decimal import Decimal + +from django.utils.timezone import now +from django_scopes import scopes_disabled +from tests.base import SoupTest, extract_form_fields + +from pretix.base.models import ( + Event, Order, OrderPosition, Organizer, SubEvent, Team, User, +) +from pretix.base.models.items import SubEventItem + + +class SubEventsTest(SoupTest): + @scopes_disabled() + def setUp(self): + super().setUp() + self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy') + self.orga1 = Organizer.objects.create(name='CCC', slug='ccc') + self.event1 = Event.objects.create( + organizer=self.orga1, name='30C3', slug='30c3', + date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc), + plugins='pretix.plugins.banktransfer,tests.testdummy', + has_subevents=True + ) + + t = Team.objects.create(organizer=self.orga1, can_create_events=True, can_change_event_settings=True, + can_change_items=True) + t.members.add(self.user) + t.limit_events.add(self.event1) + self.ticket = self.event1.items.create(name='Early-bird ticket', + category=None, default_price=23, + admission=True) + + self.client.login(email='dummy@dummy.dummy', password='dummy') + + self.subevent1 = self.event1.subevents.create(name='SE1', date_from=now()) + self.subevent2 = self.event1.subevents.create(name='SE2', date_from=now()) + + def test_list(self): + doc = self.get_doc('/control/event/ccc/30c3/subevents/') + tabletext = doc.select("#page-wrapper .table")[0].text + self.assertIn("SE1", tabletext) + + def test_create(self): + doc = self.get_doc('/control/event/ccc/30c3/subevents/add') + assert doc.select("input[name=quotas-TOTAL_FORMS]") + doc = self.post_doc('/control/event/ccc/30c3/subevents/add', { + 'name_0': 'SE2', + 'active': 'on', + 'date_from_0': '2017-07-01', + 'date_from_1': '10:00:00', + 'date_to_0': '2017-07-01', + 'date_to_1': '12:00:00', + 'location_0': 'Hamburg', + 'presale_start_0': '2017-06-20', + 'presale_start_1': '10:00:00', + 'checkinlist_set-TOTAL_FORMS': '1', + 'checkinlist_set-INITIAL_FORMS': '0', + 'checkinlist_set-MIN_NUM_FORMS': '0', + 'checkinlist_set-MAX_NUM_FORMS': '1000', + 'checkinlist_set-0-name': 'Default', + 'checkinlist_set-0-all_products': 'on', + 'quotas-TOTAL_FORMS': '1', + 'quotas-INITIAL_FORMS': '0', + 'quotas-MIN_NUM_FORMS': '0', + 'quotas-MAX_NUM_FORMS': '1000', + 'quotas-0-name': 'Q1', + 'quotas-0-size': '50', + 'quotas-0-itemvars': str(self.ticket.pk), + 'item-%d-price' % self.ticket.pk: '12' + }) + assert doc.select(".alert-success") + with scopes_disabled(): + se = self.event1.subevents.first() + assert str(se.name) == "SE2" + assert se.active + assert se.date_from.isoformat() == "2017-07-01T10:00:00+00:00" + assert se.date_to.isoformat() == "2017-07-01T12:00:00+00:00" + assert str(se.location) == "Hamburg" + assert se.presale_start.isoformat() == "2017-06-20T10:00:00+00:00" + assert not se.presale_end + assert se.quotas.count() == 1 + q = se.quotas.last() + assert q.name == "Q1" + assert q.size == 50 + assert list(q.items.all()) == [self.ticket] + sei = SubEventItem.objects.get(subevent=se, item=self.ticket) + assert sei.price == 12 + assert se.checkinlist_set.count() == 1 + + def test_modify(self): + doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/' % self.subevent1.pk) + assert doc.select("input[name=quotas-TOTAL_FORMS]") + doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/' % self.subevent1.pk, { + 'name_0': 'SE2', + 'active': 'on', + 'date_from_0': '2017-07-01', + 'date_from_1': '10:00:00', + 'date_to_0': '2017-07-01', + 'date_to_1': '12:00:00', + 'location_0': 'Hamburg', + 'presale_start_0': '2017-06-20', + 'presale_start_1': '10:00:00', + 'quotas-TOTAL_FORMS': '1', + 'quotas-INITIAL_FORMS': '0', + 'quotas-MIN_NUM_FORMS': '0', + 'quotas-MAX_NUM_FORMS': '1000', + 'quotas-0-name': 'Q1', + 'quotas-0-size': '50', + 'quotas-0-itemvars': str(self.ticket.pk), + 'checkinlist_set-TOTAL_FORMS': '1', + 'checkinlist_set-INITIAL_FORMS': '0', + 'checkinlist_set-MIN_NUM_FORMS': '0', + 'checkinlist_set-MAX_NUM_FORMS': '1000', + 'checkinlist_set-0-name': 'Default', + 'checkinlist_set-0-all_products': 'on', + 'item-%d-price' % self.ticket.pk: '12' + }) + assert doc.select(".alert-success") + self.subevent1.refresh_from_db() + se = self.subevent1 + assert str(se.name) == "SE2" + assert se.active + assert se.date_from.isoformat() == "2017-07-01T10:00:00+00:00" + assert se.date_to.isoformat() == "2017-07-01T12:00:00+00:00" + assert str(se.location) == "Hamburg" + assert se.presale_start.isoformat() == "2017-06-20T10:00:00+00:00" + assert not se.presale_end + with scopes_disabled(): + assert se.quotas.count() == 1 + q = se.quotas.last() + assert q.name == "Q1" + assert q.size == 50 + assert list(q.items.all()) == [self.ticket] + sei = SubEventItem.objects.get(subevent=se, item=self.ticket) + assert sei.price == 12 + assert se.checkinlist_set.count() == 1 + + def test_delete(self): + doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk) + assert doc.select("button") + doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk, {}) + assert doc.select(".alert-success") + # deleting the second event + doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent2.pk, {}) + assert doc.select(".alert-success") + with scopes_disabled(): + assert not SubEvent.objects.filter(pk=self.subevent2.pk).exists() + assert not SubEvent.objects.filter(pk=self.subevent1.pk).exists() + + def test_delete_with_orders(self): + with scopes_disabled(): + o = Order.objects.create( + code='FOO', event=self.event1, email='dummy@dummy.test', + status=Order.STATUS_PENDING, + datetime=now(), expires=now() + datetime.timedelta(days=10), + total=14, locale='en' + ) + OrderPosition.objects.create( + order=o, + item=self.ticket, + subevent=self.subevent1, + price=Decimal("14"), + ) + doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk, follow=True) + assert doc.select(".alert-danger") + doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk, {}, follow=True) + assert doc.select(".alert-danger") + with scopes_disabled(): + assert self.event1.subevents.filter(pk=self.subevent1.pk).exists() + + def test_create_bulk(self): + with scopes_disabled(): + self.event1.subevents.all().delete() + self.event1.settings.timezone = 'Europe/Berlin' + + doc = self.get_doc('/control/event/ccc/30c3/subevents/bulk_add') + assert doc.select("input[name=rruleformset-TOTAL_FORMS]") + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', { + 'rruleformset-TOTAL_FORMS': '1', + 'rruleformset-INITIAL_FORMS': '0', + 'rruleformset-MIN_NUM_FORMS': '0', + 'rruleformset-MAX_NUM_FORMS': '1000', + 'rruleformset-0-interval': '1', + 'rruleformset-0-freq': 'yearly', + 'rruleformset-0-dtstart': '2018-04-03', + 'rruleformset-0-yearly_same': 'on', + 'rruleformset-0-yearly_bysetpos': '1', + 'rruleformset-0-yearly_byweekday': 'MO', + 'rruleformset-0-yearly_bymonth': '1', + 'rruleformset-0-monthly_same': 'on', + 'rruleformset-0-monthly_bysetpos': '1', + 'rruleformset-0-monthly_byweekday': 'MO', + 'rruleformset-0-end': 'count', + 'rruleformset-0-count': '10', + 'rruleformset-0-until': '2019-04-03', + 'timeformset-TOTAL_FORMS': '1', + 'timeformset-INITIAL_FORMS': '0', + 'timeformset-MIN_NUM_FORMS': '1', + 'timeformset-MAX_NUM_FORMS': '1000', + 'timeformset-0-time_from': '13:29:31', + 'timeformset-0-time_to': '15:29:31', + 'name_0': 'Foo', + 'active': 'on', + 'location_0': 'Loc', + 'time_admission': '', + 'frontpage_text_0': '', + 'rel_presale_start_0': 'unset', + 'rel_presale_start_1': '', + 'rel_presale_start_2': '1', + 'rel_presale_start_3': 'date_from', + 'rel_presale_start_4': '', + 'rel_presale_end_1': '', + 'rel_presale_end_0': 'relative', + 'rel_presale_end_2': '1', + 'rel_presale_end_3': 'date_from', + 'rel_presale_end_4': '13:29:31', + 'quotas-TOTAL_FORMS': '1', + 'quotas-INITIAL_FORMS': '0', + 'quotas-MIN_NUM_FORMS': '0', + 'quotas-MAX_NUM_FORMS': '1000', + 'quotas-0-id': '', + 'quotas-0-name': 'Bar', + 'quotas-0-size': '12', + 'quotas-0-itemvars': str(self.ticket.pk), + 'item-%d-price' % self.ticket.pk: '16', + 'checkinlist_set-TOTAL_FORMS': '1', + 'checkinlist_set-INITIAL_FORMS': '0', + 'checkinlist_set-MIN_NUM_FORMS': '0', + 'checkinlist_set-MAX_NUM_FORMS': '1000', + 'checkinlist_set-0-id': '', + 'checkinlist_set-0-name': 'Foo', + 'checkinlist_set-0-limit_products': str(self.ticket.pk), + }) + assert doc.select(".alert-success") + with scopes_disabled(): + ses = list(self.event1.subevents.order_by('date_from')) + assert len(ses) == 10 + + assert str(ses[0].name) == "Foo" + assert ses[0].date_from.isoformat() == "2018-04-03T11:29:31+00:00" + assert ses[0].date_to.isoformat() == "2018-04-03T13:29:31+00:00" + assert not ses[0].presale_start + assert ses[0].presale_end.isoformat() == "2018-04-02T11:29:31+00:00" + with scopes_disabled(): + assert ses[0].quotas.count() == 1 + assert list(ses[0].quotas.first().items.all()) == [self.ticket] + assert SubEventItem.objects.get(subevent=ses[0], item=self.ticket).price == 16 + assert ses[0].checkinlist_set.count() == 1 + + assert str(ses[1].name) == "Foo" + assert ses[1].date_from.isoformat() == "2019-04-03T11:29:31+00:00" + assert ses[1].date_to.isoformat() == "2019-04-03T13:29:31+00:00" + assert not ses[1].presale_start + assert ses[1].presale_end.isoformat() == "2019-04-02T11:29:31+00:00" + with scopes_disabled(): + assert ses[1].quotas.count() == 1 + assert list(ses[1].quotas.first().items.all()) == [self.ticket] + assert SubEventItem.objects.get(subevent=ses[0], item=self.ticket).price == 16 + assert ses[1].checkinlist_set.count() == 1 + + assert ses[-1].date_from.isoformat() == "2027-04-03T11:29:31+00:00" + + def test_create_bulk_daily_interval(self): + with scopes_disabled(): + self.event1.subevents.all().delete() + self.event1.settings.timezone = 'Europe/Berlin' + + doc = self.get_doc('/control/event/ccc/30c3/subevents/bulk_add') + assert doc.select("input[name=rruleformset-TOTAL_FORMS]") + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', { + 'rruleformset-TOTAL_FORMS': '1', + 'rruleformset-INITIAL_FORMS': '0', + 'rruleformset-MIN_NUM_FORMS': '0', + 'rruleformset-MAX_NUM_FORMS': '1000', + 'rruleformset-0-interval': '2', + 'rruleformset-0-freq': 'daily', + 'rruleformset-0-dtstart': '2018-04-03', + 'rruleformset-0-yearly_same': 'on', + 'rruleformset-0-yearly_bysetpos': '1', + 'rruleformset-0-yearly_byweekday': 'MO', + 'rruleformset-0-yearly_bymonth': '1', + 'rruleformset-0-monthly_same': 'on', + 'rruleformset-0-monthly_bysetpos': '1', + 'rruleformset-0-monthly_byweekday': 'MO', + 'rruleformset-0-end': 'until', + 'rruleformset-0-count': '10', + 'rruleformset-0-until': '2019-04-03', + 'timeformset-TOTAL_FORMS': '1', + 'timeformset-INITIAL_FORMS': '0', + 'timeformset-MIN_NUM_FORMS': '1', + 'timeformset-MAX_NUM_FORMS': '1000', + 'timeformset-0-time_from': '13:29:31', + 'timeformset-0-time_to': '15:29:31', + 'name_0': 'Foo', + 'active': 'on', + 'frontpage_text_0': '', + 'rel_presale_start_0': 'unset', + 'rel_presale_start_1': '', + 'rel_presale_start_2': '1', + 'rel_presale_start_3': 'date_from', + 'rel_presale_start_4': '', + 'rel_presale_end_1': '', + 'rel_presale_end_0': 'relative', + 'rel_presale_end_2': '1', + 'rel_presale_end_3': 'date_from', + 'rel_presale_end_4': '13:29:31', + 'quotas-TOTAL_FORMS': '1', + 'quotas-INITIAL_FORMS': '0', + 'quotas-MIN_NUM_FORMS': '1', + 'quotas-MAX_NUM_FORMS': '1000', + 'quotas-0-name': 'Q1', + 'quotas-0-size': '50', + 'quotas-0-itemvars': str(self.ticket.pk), + 'checkinlist_set-TOTAL_FORMS': '0', + 'checkinlist_set-INITIAL_FORMS': '0', + 'checkinlist_set-MIN_NUM_FORMS': '0', + 'checkinlist_set-MAX_NUM_FORMS': '1000', + }) + assert doc.select(".alert-success") + with scopes_disabled(): + ses = list(self.event1.subevents.order_by('date_from')) + assert len(ses) == 183 + + assert ses[0].date_from.isoformat() == "2018-04-03T11:29:31+00:00" + assert ses[110].date_from.isoformat() == "2018-11-09T12:29:31+00:00" # DST :) + assert ses[-1].date_from.isoformat() == "2019-04-02T11:29:31+00:00" + + def test_create_bulk_daily_interval_multiple_times(self): + with scopes_disabled(): + self.event1.subevents.all().delete() + self.event1.settings.timezone = 'Europe/Berlin' + + doc = self.get_doc('/control/event/ccc/30c3/subevents/bulk_add') + assert doc.select("input[name=rruleformset-TOTAL_FORMS]") + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', { + 'rruleformset-TOTAL_FORMS': '1', + 'rruleformset-INITIAL_FORMS': '0', + 'rruleformset-MIN_NUM_FORMS': '0', + 'rruleformset-MAX_NUM_FORMS': '1000', + 'rruleformset-0-interval': '2', + 'rruleformset-0-freq': 'daily', + 'rruleformset-0-dtstart': '2018-04-03', + 'rruleformset-0-yearly_same': 'on', + 'rruleformset-0-yearly_bysetpos': '1', + 'rruleformset-0-yearly_byweekday': 'MO', + 'rruleformset-0-yearly_bymonth': '1', + 'rruleformset-0-monthly_same': 'on', + 'rruleformset-0-monthly_bysetpos': '1', + 'rruleformset-0-monthly_byweekday': 'MO', + 'rruleformset-0-end': 'until', + 'rruleformset-0-count': '10', + 'rruleformset-0-until': '2019-04-03', + 'timeformset-TOTAL_FORMS': '2', + 'timeformset-INITIAL_FORMS': '0', + 'timeformset-MIN_NUM_FORMS': '1', + 'timeformset-MAX_NUM_FORMS': '1000', + 'timeformset-0-time_from': '13:29:31', + 'timeformset-0-time_to': '15:29:31', + 'timeformset-1-time_from': '15:29:31', + 'timeformset-1-time_to': '17:29:31', + 'name_0': 'Foo', + 'active': 'on', + 'frontpage_text_0': '', + 'rel_presale_start_0': 'unset', + 'rel_presale_start_1': '', + 'rel_presale_start_2': '1', + 'rel_presale_start_3': 'date_from', + 'rel_presale_start_4': '', + 'rel_presale_end_1': '', + 'rel_presale_end_0': 'relative', + 'rel_presale_end_2': '1', + 'rel_presale_end_3': 'date_from', + 'rel_presale_end_4': '13:29:31', + 'quotas-TOTAL_FORMS': '1', + 'quotas-INITIAL_FORMS': '0', + 'quotas-MIN_NUM_FORMS': '0', + 'quotas-MAX_NUM_FORMS': '1000', + 'quotas-0-name': 'Q1', + 'quotas-0-size': '50', + 'quotas-0-itemvars': str(self.ticket.pk), + 'checkinlist_set-TOTAL_FORMS': '0', + 'checkinlist_set-INITIAL_FORMS': '0', + 'checkinlist_set-MIN_NUM_FORMS': '0', + 'checkinlist_set-MAX_NUM_FORMS': '1000', + }) + assert doc.select(".alert-success") + with scopes_disabled(): + ses = list(self.event1.subevents.order_by('date_from')) + assert len(ses) == 183 * 2 + + assert ses[0].date_from.isoformat() == "2018-04-03T11:29:31+00:00" + assert ses[1].date_from.isoformat() == "2018-04-03T13:29:31+00:00" + assert ses[220].date_from.isoformat() == "2018-11-09T12:29:31+00:00" # DST :) + assert ses[-1].date_from.isoformat() == "2019-04-02T13:29:31+00:00" + + def test_create_bulk_exclude(self): + with scopes_disabled(): + self.event1.subevents.all().delete() + self.event1.settings.timezone = 'Europe/Berlin' + + doc = self.get_doc('/control/event/ccc/30c3/subevents/bulk_add') + assert doc.select("input[name=rruleformset-TOTAL_FORMS]") + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', { + 'rruleformset-TOTAL_FORMS': '2', + 'rruleformset-INITIAL_FORMS': '0', + 'rruleformset-MIN_NUM_FORMS': '0', + 'rruleformset-MAX_NUM_FORMS': '1000', + 'rruleformset-0-interval': '1', + 'rruleformset-0-freq': 'daily', + 'rruleformset-0-dtstart': '2018-04-03', + 'rruleformset-0-yearly_same': 'on', + 'rruleformset-0-yearly_bysetpos': '1', + 'rruleformset-0-yearly_byweekday': 'MO', + 'rruleformset-0-yearly_bymonth': '1', + 'rruleformset-0-monthly_same': 'on', + 'rruleformset-0-monthly_bysetpos': '1', + 'rruleformset-0-monthly_byweekday': 'MO', + 'rruleformset-0-end': 'until', + 'rruleformset-0-count': '10', + 'rruleformset-0-until': '2019-04-03', + 'rruleformset-1-interval': '1', + 'rruleformset-1-freq': 'weekly', + 'rruleformset-1-dtstart': '2018-04-03', + 'rruleformset-1-yearly_same': 'on', + 'rruleformset-1-yearly_bysetpos': '1', + 'rruleformset-1-yearly_byweekday': 'MO', + 'rruleformset-1-yearly_bymonth': '1', + 'rruleformset-1-monthly_same': 'on', + 'rruleformset-1-monthly_bysetpos': '1', + 'rruleformset-1-monthly_byweekday': 'MO', + 'rruleformset-1-weekly_byweekday': 'MO', + 'rruleformset-1-end': 'until', + 'rruleformset-1-count': '10', + 'rruleformset-1-until': '2019-04-03', + 'rruleformset-1-exclude': 'on', + 'timeformset-TOTAL_FORMS': '1', + 'timeformset-INITIAL_FORMS': '0', + 'timeformset-MIN_NUM_FORMS': '1', + 'timeformset-MAX_NUM_FORMS': '1000', + 'timeformset-0-time_from': '13:29:31', + 'timeformset-0-time_to': '15:29:31', + 'name_0': 'Foo', + 'active': 'on', + 'frontpage_text_0': '', + 'rel_presale_start_0': 'unset', + 'rel_presale_start_1': '', + 'rel_presale_start_2': '1', + 'rel_presale_start_3': 'date_from', + 'rel_presale_start_4': '', + 'rel_presale_end_1': '', + 'rel_presale_end_0': 'relative', + 'rel_presale_end_2': '1', + 'rel_presale_end_3': 'date_from', + 'rel_presale_end_4': '13:29:31', + 'quotas-TOTAL_FORMS': '1', + 'quotas-INITIAL_FORMS': '0', + 'quotas-MIN_NUM_FORMS': '0', + 'quotas-MAX_NUM_FORMS': '1000', + 'quotas-0-name': 'Q1', + 'quotas-0-size': '50', + 'quotas-0-itemvars': str(self.ticket.pk), + 'checkinlist_set-TOTAL_FORMS': '0', + 'checkinlist_set-INITIAL_FORMS': '0', + 'checkinlist_set-MIN_NUM_FORMS': '0', + 'checkinlist_set-MAX_NUM_FORMS': '1000', + }) + assert doc.select(".alert-success") + with scopes_disabled(): + ses = list(self.event1.subevents.order_by('date_from')) + assert len(ses) == 314 + + assert ses[0].date_from.isoformat() == "2018-04-03T11:29:31+00:00" + assert ses[5].date_from.isoformat() == "2018-04-08T11:29:31+00:00" + assert ses[6].date_from.isoformat() == "2018-04-10T11:29:31+00:00" + + def test_create_bulk_monthly_interval(self): + with scopes_disabled(): + self.event1.subevents.all().delete() + self.event1.settings.timezone = 'Europe/Berlin' + + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', { + 'rruleformset-TOTAL_FORMS': '1', + 'rruleformset-INITIAL_FORMS': '0', + 'rruleformset-MIN_NUM_FORMS': '0', + 'rruleformset-MAX_NUM_FORMS': '1000', + 'rruleformset-0-interval': '1', + 'rruleformset-0-freq': 'monthly', + 'rruleformset-0-dtstart': '2018-04-03', + 'rruleformset-0-yearly_same': 'on', + 'rruleformset-0-yearly_bysetpos': '1', + 'rruleformset-0-yearly_byweekday': 'MO', + 'rruleformset-0-yearly_bymonth': '1', + 'rruleformset-0-monthly_same': 'off', + 'rruleformset-0-monthly_bysetpos': '-1', + 'rruleformset-0-monthly_byweekday': 'MO,TU,WE,TH,FR', + 'rruleformset-0-weekly_byweekday': 'TH', + 'rruleformset-0-end': 'until', + 'rruleformset-0-count': '10', + 'rruleformset-0-until': '2019-04-03', + 'timeformset-TOTAL_FORMS': '1', + 'timeformset-INITIAL_FORMS': '0', + 'timeformset-MIN_NUM_FORMS': '1', + 'timeformset-MAX_NUM_FORMS': '1000', + 'timeformset-0-time_from': '13:29:31', + 'timeformset-0-time_to': '15:29:31', + 'name_0': 'Foo', + 'active': 'on', + 'frontpage_text_0': '', + 'rel_presale_start_0': 'unset', + 'rel_presale_start_1': '', + 'rel_presale_start_2': '1', + 'rel_presale_start_3': 'date_from', + 'rel_presale_start_4': '', + 'rel_presale_end_0': 'unset', + 'rel_presale_end_1': '', + 'rel_presale_end_2': '1', + 'rel_presale_end_3': 'date_from', + 'rel_presale_end_4': '13:29:31', + 'quotas-TOTAL_FORMS': '1', + 'quotas-INITIAL_FORMS': '0', + 'quotas-MIN_NUM_FORMS': '0', + 'quotas-MAX_NUM_FORMS': '1000', + 'quotas-0-name': 'Q1', + 'quotas-0-size': '50', + 'quotas-0-itemvars': str(self.ticket.pk), + 'checkinlist_set-TOTAL_FORMS': '0', + 'checkinlist_set-INITIAL_FORMS': '0', + 'checkinlist_set-MIN_NUM_FORMS': '0', + 'checkinlist_set-MAX_NUM_FORMS': '1000', + }) + assert doc.select(".alert-success") + with scopes_disabled(): + ses = list(self.event1.subevents.order_by('date_from')) + assert len(ses) == 12 + + assert ses[0].date_from.isoformat() == "2018-04-30T11:29:31+00:00" + assert ses[1].date_from.isoformat() == "2018-05-31T11:29:31+00:00" + assert ses[-1].date_from.isoformat() == "2019-03-29T12:29:31+00:00" + + def test_create_bulk_weekly_interval(self): + with scopes_disabled(): + self.event1.subevents.all().delete() + self.event1.settings.timezone = 'Europe/Berlin' + + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', { + 'rruleformset-TOTAL_FORMS': '1', + 'rruleformset-INITIAL_FORMS': '0', + 'rruleformset-MIN_NUM_FORMS': '0', + 'rruleformset-MAX_NUM_FORMS': '1000', + 'rruleformset-0-interval': '1', + 'rruleformset-0-freq': 'weekly', + 'rruleformset-0-dtstart': '2018-04-03', + 'rruleformset-0-yearly_same': 'on', + 'rruleformset-0-yearly_bysetpos': '1', + 'rruleformset-0-yearly_byweekday': 'MO', + 'rruleformset-0-yearly_bymonth': '1', + 'rruleformset-0-monthly_same': 'on', + 'rruleformset-0-monthly_bysetpos': '-1', + 'rruleformset-0-monthly_byweekday': 'MO,TU,WE,TH,FR', + 'rruleformset-0-weekly_byweekday': 'TH', + 'rruleformset-0-end': 'until', + 'rruleformset-0-count': '10', + 'rruleformset-0-until': '2019-04-03', + 'timeformset-TOTAL_FORMS': '1', + 'timeformset-INITIAL_FORMS': '0', + 'timeformset-MIN_NUM_FORMS': '1', + 'timeformset-MAX_NUM_FORMS': '1000', + 'timeformset-0-time_from': '13:29:31', + 'timeformset-0-time_to': '15:29:31', + 'name_0': 'Foo', + 'active': 'on', + 'frontpage_text_0': '', + 'rel_presale_start_0': 'unset', + 'rel_presale_start_1': '', + 'rel_presale_start_2': '1', + 'rel_presale_start_3': 'date_from', + 'rel_presale_start_4': '', + 'rel_presale_end_0': 'unset', + 'rel_presale_end_1': '', + 'rel_presale_end_2': '1', + 'rel_presale_end_3': 'date_from', + 'rel_presale_end_4': '13:29:31', + 'quotas-TOTAL_FORMS': '1', + 'quotas-INITIAL_FORMS': '0', + 'quotas-MIN_NUM_FORMS': '0', + 'quotas-MAX_NUM_FORMS': '1000', + 'quotas-0-name': 'Q1', + 'quotas-0-size': '50', + 'quotas-0-itemvars': str(self.ticket.pk), + 'checkinlist_set-TOTAL_FORMS': '0', + 'checkinlist_set-INITIAL_FORMS': '0', + 'checkinlist_set-MIN_NUM_FORMS': '0', + 'checkinlist_set-MAX_NUM_FORMS': '1000', + }) + assert doc.select(".alert-success") + with scopes_disabled(): + ses = list(self.event1.subevents.order_by('date_from')) + assert len(ses) == 52 + + assert ses[0].date_from.isoformat() == "2018-04-05T11:29:31+00:00" + assert ses[1].date_from.isoformat() == "2018-04-12T11:29:31+00:00" + assert ses[-1].date_from.isoformat() == "2019-03-28T12:29:31+00:00" + + def test_delete_bulk(self): + self.subevent2.active = True + self.subevent2.save() + with scopes_disabled(): + o = Order.objects.create( + code='FOO', event=self.event1, email='dummy@dummy.test', + status=Order.STATUS_PENDING, + datetime=now(), expires=now() + datetime.timedelta(days=10), + total=14, locale='en' + ) + OrderPosition.objects.create( + order=o, + item=self.ticket, + subevent=self.subevent1, + price=Decimal("14"), + ) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_action', { + 'subevent': [str(self.subevent1.pk), str(self.subevent2.pk)], + 'action': 'delete_confirm' + }, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + assert not self.event1.subevents.filter(pk=self.subevent2.pk).exists() + assert self.event1.subevents.get(pk=self.subevent1.pk).active is False + + def test_delete_bulk_by_query(self): + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_action', { + '__ALL': 'on', + 'filter-query': 'SE2', + 'action': 'delete_confirm' + }, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + assert not self.event1.subevents.filter(pk=self.subevent2.pk).exists() + assert self.event1.subevents.filter(pk=self.subevent1.pk).exists() + + def test_disable_bulk(self): + self.subevent2.active = True + self.subevent2.save() + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_action', { + 'subevent': str(self.subevent2.pk), + 'action': 'disable' + }, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + assert self.event1.subevents.get(pk=self.subevent2.pk).active is False + + def test_enable_bulk(self): + self.subevent2.active = False + self.subevent2.save() + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_action', { + 'subevent': str(self.subevent2.pk), + 'action': 'enable' + }, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + assert self.event1.subevents.get(pk=self.subevent2.pk).active is True + + def test_edit_bulk_scalar_change(self): + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + assert not fields.get('bulkedit-name_0') + fields.update({ + '_bulk': ['bulkeditname'], + 'bulkedit-name_0': 'SEFOO', + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + assert str(self.event1.subevents.get(pk=self.subevent1.pk).name) == 'SEFOO' + assert str(self.event1.subevents.get(pk=self.subevent2.pk).name) == 'SEFOO' + + def test_edit_bulk_scalar_keep_mixed(self): + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + assert not fields.get('bulkedit-name_0') + fields.update({ + '_bulk': ['bulkeditlocation'], + 'bulkedit-name_0': 'SEFOO', + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + assert str(self.event1.subevents.get(pk=self.subevent1.pk).name) == 'SE1' + assert str(self.event1.subevents.get(pk=self.subevent2.pk).name) == 'SE2' + + def test_edit_bulk_meta(self): + prop1 = self.orga1.meta_properties.create(name="Prop1") + prop2 = self.orga1.meta_properties.create(name="Prop2") + prop2.subevent_values.create(subevent=self.subevent1, value="Bla") + prop2.subevent_values.create(subevent=self.subevent2, value="Bla") + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + assert not fields.get('prop-{}-value'.format(prop1.pk)) + assert fields.get('prop-{}-value'.format(prop2.pk)) == 'Bla' + fields.update({ + '_bulk': ['prop-{}value'.format(prop1.pk), 'prop-{}value'.format(prop2.pk)], + 'prop-{}-value'.format(prop1.pk): 'Bla', + 'prop-{}-value'.format(prop2.pk): '', + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + assert self.event1.subevents.get(pk=self.subevent1.pk).meta_data == {'Prop1': 'Bla', 'Prop2': ''} + assert self.event1.subevents.get(pk=self.subevent2.pk).meta_data == {'Prop1': 'Bla', 'Prop2': ''} + + def test_edit_bulk_day_both_same_before(self): + with scopes_disabled(): + self.subevent1.date_from = datetime.datetime(2013, 12, 26, 9, 0, 0, tzinfo=datetime.timezone.utc) + self.subevent1.save() + self.subevent2.date_from = datetime.datetime(2013, 12, 26, 11, 0, 0, tzinfo=datetime.timezone.utc) + self.subevent2.save() + + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + assert fields.get('bulkedit-date_from_day') == '2013-12-26' + fields.update({ + '_bulk': ['bulkeditdate_from_day'], + 'bulkedit-date_from_day': '2013-12-27', + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + self.subevent1.refresh_from_db() + self.subevent2.refresh_from_db() + assert self.subevent1.date_from == datetime.datetime(2013, 12, 27, 9, 0, 0, tzinfo=datetime.timezone.utc) + assert self.subevent2.date_from == datetime.datetime(2013, 12, 27, 11, 0, 0, tzinfo=datetime.timezone.utc) + + def test_edit_bulk_day_both_different_before(self): + with scopes_disabled(): + self.subevent1.date_from = datetime.datetime(2013, 12, 26, 9, 0, 0, tzinfo=datetime.timezone.utc) + self.subevent1.save() + self.subevent2.date_from = datetime.datetime(2013, 12, 27, 11, 0, 0, tzinfo=datetime.timezone.utc) + self.subevent2.save() + + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + assert not fields.get('bulkedit-date_from_day') + fields.update({ + '_bulk': ['bulkeditdate_from_day'], + 'bulkedit-date_from_day': '2013-12-27', + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + self.subevent1.refresh_from_db() + self.subevent2.refresh_from_db() + assert self.subevent1.date_from == datetime.datetime(2013, 12, 27, 9, 0, 0, tzinfo=datetime.timezone.utc) + assert self.subevent2.date_from == datetime.datetime(2013, 12, 27, 11, 0, 0, tzinfo=datetime.timezone.utc) + + def test_edit_bulk_day_unset_before(self): + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + assert not fields.get('bulkedit-date_to_day') + fields.update({ + '_bulk': ['bulkeditdate_to_day'], + 'bulkedit-date_to_day': '2013-12-27', + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + self.subevent1.refresh_from_db() + self.subevent2.refresh_from_db() + assert self.subevent1.date_to == datetime.datetime(2013, 12, 27, 0, 0, 0, tzinfo=datetime.timezone.utc) + assert self.subevent2.date_to == datetime.datetime(2013, 12, 27, 0, 0, 0, tzinfo=datetime.timezone.utc) + + def test_edit_bulk_day_unset(self): + with scopes_disabled(): + self.subevent1.date_to = datetime.datetime(2013, 12, 26, 9, 0, 0, tzinfo=datetime.timezone.utc) + self.subevent1.save() + self.subevent2.date_to = datetime.datetime(2013, 12, 27, 11, 0, 0, tzinfo=datetime.timezone.utc) + self.subevent2.save() + + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + assert not fields.get('bulkedit-date_to_day') + fields.update({ + '_bulk': ['bulkeditdate_to_day'], + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + self.subevent1.refresh_from_db() + self.subevent2.refresh_from_db() + assert self.subevent1.date_to is None + assert self.subevent2.date_to is None + + def test_edit_bulk_time_both_same_before(self): + with scopes_disabled(): + self.subevent1.date_to = datetime.datetime(2013, 12, 26, 9, 0, 0, tzinfo=datetime.timezone.utc) + self.subevent1.save() + self.subevent2.date_to = datetime.datetime(2013, 12, 27, 9, 0, 0, tzinfo=datetime.timezone.utc) + self.subevent2.save() + + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + assert fields.get('bulkedit-date_to_time') == '09:00:00' + fields.update({ + '_bulk': ['bulkeditdate_to_time'], + 'bulkedit-date_to_time': '10:00:00', + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + self.subevent1.refresh_from_db() + self.subevent2.refresh_from_db() + assert self.subevent1.date_to == datetime.datetime(2013, 12, 26, 10, 0, 0, tzinfo=datetime.timezone.utc) + assert self.subevent2.date_to == datetime.datetime(2013, 12, 27, 10, 0, 0, tzinfo=datetime.timezone.utc) + + def test_edit_bulk_time_both_different_before(self): + with scopes_disabled(): + self.subevent1.date_to = datetime.datetime(2013, 12, 26, 9, 0, 0, tzinfo=datetime.timezone.utc) + self.subevent1.save() + self.subevent2.date_to = datetime.datetime(2013, 12, 27, 11, 0, 0, tzinfo=datetime.timezone.utc) + self.subevent2.save() + + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + assert not fields.get('bulkedit-date_to_time') + fields.update({ + '_bulk': ['bulkeditdate_to_time'], + 'bulkedit-date_to_time': '10:00:00', + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + self.subevent1.refresh_from_db() + self.subevent2.refresh_from_db() + assert self.subevent1.date_to == datetime.datetime(2013, 12, 26, 10, 0, 0, tzinfo=datetime.timezone.utc) + assert self.subevent2.date_to == datetime.datetime(2013, 12, 27, 10, 0, 0, tzinfo=datetime.timezone.utc) + + def test_edit_bulk_time_unset_before(self): + with scopes_disabled(): + self.subevent1.date_from = datetime.datetime(2013, 12, 26, 9, 0, 0, tzinfo=datetime.timezone.utc) + self.subevent1.save() + self.subevent2.date_from = datetime.datetime(2013, 12, 27, 11, 0, 0, tzinfo=datetime.timezone.utc) + self.subevent2.save() + + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + assert not fields.get('bulkedit-date_to_time') + fields.update({ + '_bulk': ['bulkeditdate_to_time'], + 'bulkedit-date_to_time': '17:00:00', + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + self.subevent1.refresh_from_db() + self.subevent2.refresh_from_db() + assert self.subevent1.date_to == datetime.datetime(2013, 12, 26, 17, 0, 0, tzinfo=datetime.timezone.utc) + assert self.subevent2.date_to == datetime.datetime(2013, 12, 27, 17, 0, 0, tzinfo=datetime.timezone.utc) + + def test_edit_bulk_price(self): + sei1 = SubEventItem.objects.create(subevent=self.subevent1, item=self.ticket, price=Decimal('4.00')) + sei2 = SubEventItem.objects.create(subevent=self.subevent2, item=self.ticket, price=Decimal('4.00')) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + assert fields.get('item-{}-price'.format(self.ticket.id)) == '4.00' + fields.update({ + '_bulk': ['item-{}price'.format(self.ticket.id)], + 'item-{}-price'.format(self.ticket.id): '5.00', + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + sei1.refresh_from_db() + sei2.refresh_from_db() + assert sei1.price == Decimal('5.00') + assert sei2.price == Decimal('5.00') + + def test_edit_bulk_quotas_add_and_edit(self): + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + fields.update({ + '_bulk': ['__quotas'], + 'quotas-TOTAL_FORMS': '2', + 'quotas-INITIAL_FORMS': '0', + 'quotas-MIN_NUM_FORMS': '0', + 'quotas-MAX_NUM_FORMS': '1000', + 'quotas-0-name': 'Q1', + 'quotas-0-size': '50', + 'quotas-0-itemvars': str(self.ticket.pk), + 'quotas-1-name': 'Q2', + 'quotas-1-size': '25', + 'quotas-1-itemvars': str(self.ticket.pk), + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + for se in [self.subevent1, self.subevent2]: + q = se.quotas.first() + assert q.name == 'Q1' + assert q.size == 50 + assert list(q.items.all()) == [self.ticket] + q = se.quotas.last() + assert q.name == 'Q2' + assert q.size == 25 + assert list(q.items.all()) == [self.ticket] + + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + fields.update({ + '_bulk': ['__quotas'], + 'quotas-0-size': '25', + 'quotas-1-size': '50', + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + for se in [self.subevent1, self.subevent2]: + q = se.quotas.get(name='Q1') + assert q.size == 25 + assert list(q.items.all()) == [self.ticket] + q = se.quotas.get(name='Q2') + assert q.size == 50 + assert list(q.items.all()) == [self.ticket] + + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + fields.update({ + '_bulk': ['__quotas'], + 'quotas-1-DELETE': 'on', + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + for se in [self.subevent1, self.subevent2]: + assert se.quotas.count() == 1 + + def test_edit_bulk_quotas_mixed_replace(self): + with scopes_disabled(): + self.subevent1.quotas.create(event=self.event1, name="Q1", size=20) + self.subevent2.quotas.create(event=self.event1, name="Q2", size=40) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + assert fields['quotas-TOTAL_FORMS'] == '0' + fields.update({ + '_bulk': ['_invalid_'], + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + assert self.subevent1.quotas.get().size == 20 + assert self.subevent2.quotas.get().size == 40 + + fields.update({ + '_bulk': ['__quotas'], + 'quotas-TOTAL_FORMS': '1', + 'quotas-INITIAL_FORMS': '0', + 'quotas-MIN_NUM_FORMS': '0', + 'quotas-MAX_NUM_FORMS': '1000', + 'quotas-0-name': 'Q1', + 'quotas-0-size': '100', + 'quotas-0-itemvars': str(self.ticket.pk), + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + assert self.subevent1.quotas.get().size == 100 + assert self.subevent2.quotas.get().size == 100 + + def test_edit_bulk_lists_add_and_edit(self): + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + fields.update({ + '_bulk': ['__checkinlists'], + 'checkinlist_set-TOTAL_FORMS': '2', + 'checkinlist_set-INITIAL_FORMS': '0', + 'checkinlist_set-MIN_NUM_FORMS': '0', + 'checkinlist_set-MAX_NUM_FORMS': '1000', + 'checkinlist_set-0-name': 'Q1', + 'checkinlist_set-0-limit_products': str(self.ticket.pk), + 'checkinlist_set-1-name': 'Q2', + 'checkinlist_set-1-limit_products': str(self.ticket.pk), + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + for se in [self.subevent1, self.subevent2]: + q = se.checkinlist_set.get(name='Q1') + assert list(q.limit_products.all()) == [self.ticket] + q = se.checkinlist_set.get(name='Q2') + assert list(q.limit_products.all()) == [self.ticket] + + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + fields.update({ + '_bulk': ['__checkinlists'], + 'checkinlist_set-1-DELETE': 'on', + }) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', fields, follow=True) + assert doc.select(".alert-success") + with scopes_disabled(): + for se in [self.subevent1, self.subevent2]: + assert se.checkinlist_set.count() == 1 + + def test_edit_bulk_lists_keep_mixed(self): + with scopes_disabled(): + self.subevent1.checkinlist_set.create(event=self.event1, name="C1") + self.subevent2.checkinlist_set.create(event=self.event1, name="C2") + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_edit', { + '__ALL': 'on', + }, follow=True) + fields = extract_form_fields(doc) + assert 'checkinlist_set-TOTAL_FORMS' not in fields
{% trans "Begin" %} - - + + {% trans "Paid tickets per quota" %} - - + + {% trans "Status" %} - - + +