Merge branch 'master' into bulk-select-with-drag-over

This commit is contained in:
Richard Schreiber
2021-02-22 16:37:39 +01:00
22 changed files with 2548 additions and 811 deletions

View File

@@ -11,7 +11,7 @@ from django.db.models import QuerySet
from django.utils.formats import localize from django.utils.formats import localize
from django.utils.translation import gettext, gettext_lazy as _ from django.utils.translation import gettext, gettext_lazy as _
from openpyxl import Workbook from openpyxl import Workbook
from openpyxl.cell.cell import KNOWN_TYPES, ILLEGAL_CHARACTERS_RE from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE, KNOWN_TYPES
from pretix.base.models import Event from pretix.base.models import Event

View File

@@ -4,6 +4,7 @@ from datetime import date, datetime, time
from django.core.validators import MinLengthValidator, RegexValidator from django.core.validators import MinLengthValidator, RegexValidator
from django.db import models from django.db import models
from django.db.models import Exists, OuterRef, Q from django.db.models import Exists, OuterRef, Q
from django.urls import reverse
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import get_current_timezone, make_aware, now from django.utils.timezone import get_current_timezone, make_aware, now
@@ -88,6 +89,15 @@ class Organizer(LoggedModel):
return ObjectRelatedCache(self) return ObjectRelatedCache(self)
@cached_property
def all_logentries_link(self):
return reverse(
'control:organizer.log',
kwargs={
'organizer': self.slug,
}
)
@property @property
def has_gift_cards(self): def has_gift_cards(self):
return self.cache.get_or_set( return self.cache.get_or_set(

View File

@@ -1,4 +1,4 @@
from datetime import datetime, time from datetime import datetime, time, timedelta
from decimal import Decimal from decimal import Decimal
from urllib.parse import urlencode from urllib.parse import urlencode
@@ -766,10 +766,15 @@ class SubEventFilterForm(FilterForm):
), ),
required=False required=False
) )
date = forms.DateField( date_from = forms.DateField(
label=_('Date'), label=_('Date from'),
required=False, required=False,
widget=DatePickerWidget widget=DatePickerWidget,
)
date_until = forms.DateField(
label=_('Date until'),
required=False,
widget=DatePickerWidget,
) )
weekday = forms.ChoiceField( weekday = forms.ChoiceField(
label=_('Weekday'), label=_('Weekday'),
@@ -796,7 +801,8 @@ class SubEventFilterForm(FilterForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*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): def filter_qs(self, qs):
fdata = self.cleaned_data fdata = self.cleaned_data
@@ -838,19 +844,21 @@ class SubEventFilterForm(FilterForm):
Q(name__icontains=i18ncomp(query)) | Q(location__icontains=query) Q(name__icontains=i18ncomp(query)) | Q(location__icontains=query)
) )
if fdata.get('date'): if fdata.get('date_until'):
date_start = make_aware(datetime.combine( date_end = make_aware(datetime.combine(
fdata.get('date'), fdata.get('date_until') + timedelta(days=1),
time(hour=0, minute=0, second=0, microsecond=0) time(hour=0, minute=0, second=0, microsecond=0)
), get_current_timezone()) ), 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( qs = qs.filter(
Q(date_to__isnull=True, date_from__gte=date_start, date_from__lte=date_end) | Q(date_to__isnull=True, date_from__lt=date_end) |
Q(date_to__isnull=False, date_from__lte=date_end, date_to__gte=date_start) 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'): if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by()) qs = qs.order_by(self.get_order_by())

View File

@@ -1,4 +1,4 @@
from bootstrap3.renderers import FieldRenderer from bootstrap3.renderers import FieldRenderer, InlineFieldRenderer
from bootstrap3.text import text_value from bootstrap3.text import text_value
from django.forms import CheckboxInput from django.forms import CheckboxInput
from django.forms.utils import flatatt from django.forms.utils import flatatt
@@ -58,3 +58,40 @@ class ControlFieldRenderer(FieldRenderer):
optional=not required and not isinstance(self.widget, CheckboxInput) optional=not required and not isinstance(self.widget, CheckboxInput)
) + html ) + html
return 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 = (
'<div class="{klass} bulk-edit-field-group">'
'<label class="field-toggle">'
'<input type="checkbox" name="_bulk" value="{name}" {checked}> {label}'
'</label>'
'<div class="field-content">'
'{html}'
'</div>'
'</div>'
).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'

View File

@@ -1,8 +1,9 @@
from datetime import timedelta from datetime import datetime, timedelta
from urllib.parse import urlencode from urllib.parse import urlencode
from django import forms from django import forms
from django.forms import formset_factory from django.forms import formset_factory
from django.forms.utils import ErrorDict
from django.urls import reverse from django.urls import reverse
from django.utils.dates import MONTHS, WEEKDAYS from django.utils.dates import MONTHS, WEEKDAYS
from django.utils.functional import cached_property 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 i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm 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.event import SubEvent, SubEventMetaValue
from pretix.base.models.items import SubEventItem from pretix.base.models.items import SubEventItem
from pretix.base.reldate import RelativeDateTimeField from pretix.base.reldate import RelativeDateTimeField
@@ -88,6 +90,142 @@ class SubEventBulkForm(SubEventForm):
del self.fields['date_admission'] 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: class SubEventItemOrVariationFormMixin:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.item = kwargs.pop('item') self.item = kwargs.pop('item')
@@ -162,7 +300,7 @@ class SubEventMetaValueForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.property = kwargs.pop('property') self.property = kwargs.pop('property')
self.default = kwargs.pop('default', None) self.default = kwargs.pop('default', None)
self.disabled = kwargs.pop('disabled') self.disabled = kwargs.pop('disabled', False)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.property.allowed_values: if self.property.allowed_values:
self.fields['value'] = forms.ChoiceField( self.fields['value'] = forms.ChoiceField(

View File

@@ -273,8 +273,15 @@ def _display_checkin(event, logentry):
def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs): def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
plains = { plains = {
'pretix.object.cloned': _('This object has been created by cloning.'), 'pretix.object.cloned': _('This object has been created by cloning.'),
'pretix.organizer.changed': _('The organizer has been changed.'),
'pretix.organizer.settings': _('The organizer settings have been changed.'),
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
'pretix.webhook.created': _('The webhook has been created.'),
'pretix.webhook.changed': _('The webhook has been changed.'),
'pretix.event.comment': _('The event\'s internal comment has been updated.'), 'pretix.event.comment': _('The event\'s internal comment has been updated.'),
'pretix.event.canceled': _('The event has been canceled.'), 'pretix.event.canceled': _('The event has been canceled.'),
'pretix.event.deleted': _('An event has been deleted.'),
'pretix.event.order.modified': _('The order details have been changed.'), 'pretix.event.order.modified': _('The order details have been changed.'),
'pretix.event.order.unpaid': _('The order has been marked as unpaid.'), 'pretix.event.order.unpaid': _('The order has been marked as unpaid.'),
'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'), 'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'),

View File

@@ -15,7 +15,8 @@
{% endblocktrans %} {% endblocktrans %}
</a> </a>
</h1> </h1>
<form method="post" href=""> <form method="post" href=""
>
{% csrf_token %} {% csrf_token %}
<fieldset class="form-inline form-refund-choose"> <fieldset class="form-inline form-refund-choose">
<legend>{% trans "How should the refund be sent?" %}</legend> <legend>{% trans "How should the refund be sent?" %}</legend>

View File

@@ -22,54 +22,68 @@
{% csrf_token %} {% csrf_token %}
{% bootstrap_form_errors sform %} {% bootstrap_form_errors sform %}
{% bootstrap_form_errors form %} {% bootstrap_form_errors form %}
<div class="tabbed-form"> <div class="row">
<fieldset> <div class="col-xs-12 col-lg-10">
<legend>{% trans "General" %}</legend> <div class="tabbed-form">
{% bootstrap_field form.name layout="control" %} <fieldset>
{% bootstrap_field form.slug layout="control" %} <legend>{% trans "General" %}</legend>
{% if form.domain %} {% bootstrap_field form.name layout="control" %}
{% bootstrap_field form.domain layout="control" %} {% bootstrap_field form.slug layout="control" %}
{% endif %} {% if form.domain %}
{% bootstrap_field sform.imprint_url layout="control" %} {% bootstrap_field form.domain layout="control" %}
{% bootstrap_field sform.contact_mail layout="control" %} {% endif %}
{% bootstrap_field sform.organizer_info_text layout="control" %} {% bootstrap_field sform.imprint_url layout="control" %}
{% bootstrap_field sform.event_team_provisioning layout="control" %} {% bootstrap_field sform.contact_mail layout="control" %}
</fieldset> {% bootstrap_field sform.organizer_info_text layout="control" %}
<fieldset> {% bootstrap_field sform.event_team_provisioning layout="control" %}
<legend>{% trans "Organizer page" %}</legend> </fieldset>
{% bootstrap_field sform.organizer_logo_image layout="control" %} <fieldset>
{% bootstrap_field sform.organizer_logo_image_large layout="control" %} <legend>{% trans "Organizer page" %}</legend>
{% bootstrap_field sform.organizer_homepage_text layout="control" %} {% bootstrap_field sform.organizer_logo_image layout="control" %}
{% bootstrap_field sform.event_list_type layout="control" %} {% bootstrap_field sform.organizer_logo_image_large layout="control" %}
{% bootstrap_field sform.event_list_availability layout="control" %} {% bootstrap_field sform.organizer_homepage_text layout="control" %}
{% bootstrap_field sform.organizer_link_back layout="control" %} {% bootstrap_field sform.event_list_type layout="control" %}
</fieldset> {% bootstrap_field sform.event_list_availability layout="control" %}
<fieldset> {% bootstrap_field sform.organizer_link_back layout="control" %}
<legend>{% trans "Localization" %}</legend> </fieldset>
{% bootstrap_field sform.locales layout="control" %} <fieldset>
{% bootstrap_field sform.region layout="control" %} <legend>{% trans "Localization" %}</legend>
</fieldset> {% bootstrap_field sform.locales layout="control" %}
<fieldset> {% bootstrap_field sform.region layout="control" %}
<legend>{% trans "Shop design" %}</legend> </fieldset>
<p class="help-block"> <fieldset>
{% blocktrans trimmed %} <legend>{% trans "Shop design" %}</legend>
These settings will be used for the organizer page as well as for the default settings <p class="help-block">
for all events in this account that do not have their own design settings. {% blocktrans trimmed %}
{% endblocktrans %} These settings will be used for the organizer page as well as for the default settings
</p> for all events in this account that do not have their own design settings.
{% bootstrap_field sform.primary_color layout="control" %} {% endblocktrans %}
{% bootstrap_field sform.theme_color_success layout="control" %} </p>
{% bootstrap_field sform.theme_color_danger layout="control" %} {% bootstrap_field sform.primary_color layout="control" %}
{% bootstrap_field sform.theme_color_background layout="control" %} {% bootstrap_field sform.theme_color_success layout="control" %}
{% bootstrap_field sform.theme_round_borders layout="control" %} {% bootstrap_field sform.theme_color_danger layout="control" %}
{% bootstrap_field sform.primary_font layout="control" %} {% bootstrap_field sform.theme_color_background layout="control" %}
{% bootstrap_field sform.favicon layout="control" %} {% bootstrap_field sform.theme_round_borders layout="control" %}
</fieldset> {% bootstrap_field sform.primary_font layout="control" %}
<fieldset> {% bootstrap_field sform.favicon layout="control" %}
<legend>{% trans "Gift cards" %}</legend> </fieldset>
{% bootstrap_field sform.giftcard_expiry_years layout="control" %} <fieldset>
{% bootstrap_field sform.giftcard_length layout="control" %} <legend>{% trans "Gift cards" %}</legend>
</fieldset> {% bootstrap_field sform.giftcard_expiry_years layout="control" %}
{% bootstrap_field sform.giftcard_length layout="control" %}
</fieldset>
</div>
</div>
<div class="col-xs-12 col-lg-2">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Change history" %}
</h3>
</div>
{% include "pretixcontrol/includes/logs.html" with obj=organizer %}
</div>
</div>
</div> </div>
<div class="form-group submit-group"> <div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-primary btn-save">

View File

@@ -0,0 +1,85 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Organizer logs" %}{% endblock %}
{% block inside %}
<h1>{% trans "Organizer logs" %}</h1>
<form class="form-inline helper-display-inline" action="" method="get">
<input type="hidden" name="content_type" value="{{ request.GET.content_type }}">
<input type="hidden" name="object" value="{{ request.GET.object }}">
<p>
<select name="user" class="form-control">
<option value="">{% trans "All actions" %}</option>
{% for up in userlist %}
{% if up.user__id %}
<option value="{{ up.user__id }}"
{% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
{{ up.user__email }}
</option>
{% endif %}
{% endfor %}
</select>
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
</p>
</form>
<ul class="list-group">
{% for log in logs %}
<li class="list-group-item logentry">
<div class="row">
<div class="col-lg-2 col-sm-6 col-xs-12">
<span class="fa fa-clock-o"></span>
{{ log.datetime|date:"SHORT_DATETIME_FORMAT" }}
{% if log.shredded %}
<span class="fa fa-eraser fa-danger fa-fw"
data-toggle="tooltip"
title="{% trans "Personal data was cleared from this log entry." %}">
</span>
{% endif %}
</div>
<div class="col-lg-2 col-sm-6 col-xs-12">
{% if log.user %}
{% if log.user.is_staff %}
<span class="fa fa-id-card fa-danger fa-fw"
data-toggle="tooltip"
title="{% trans "This change was performed by a pretix administrator." %}">
</span>
{% else %}
<span class="fa fa-user fa-fw"></span>
{% endif %}
{{ log.user.get_full_name }}
{% if log.oauth_application %}
<br><span class="fa fa-plug fa-fw"></span>
{{ log.oauth_application.name }}
{% endif %}
{% elif log.device %}
<span class="fa fa-mobile fa-fw"></span>
{{ log.device.name }}
{% elif log.api_token %}
<span class="fa fa-key fa-fw"></span>
{{ log.api_token.name }}
{% endif %}
</div>
<div class="col-lg-2 col-sm-12 col-xs-12">
{% if log.display_object %}
<span class="fa fa-flag"></span> {{ log.display_object|safe }}
{% endif %}
</div>
<div class="col-lg-6 col-sm-12 col-xs-12">
{{ log.display }}
{% if staff_session %}
<a href="" class="btn btn-default btn-xs" data-expandlogs data-id="{{ log.pk }}">
<span class="fa-eye fa fa-fw"></span>
{% trans "Inspect" %}
</a>
{% endif %}
</div>
</div>
</li>
{% empty %}
<div class="list-group-item">
<em>{% trans "No results" %}</em>
</div>
{% endfor %}
</ul>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -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 %}
<h1>
{% trans "Change multiple dates" context "subevent" %}
<small>
{% blocktrans trimmed with number=subevents.count %}
{{ number }} selected
{% endblocktrans %}
</small>
</h1>
<form action="" method="post" class="form-horizontal" id="subevent-bulk-create-form">
{% csrf_token %}
{% bootstrap_form_errors form %}
{% for f in itemvar_forms %}
{% bootstrap_form_errors f %}
{% endfor %}
<div class="hidden">
{% for se in subevents %}
<input type="hidden" name="subevent" value="{{ se.pk }}">
{% endfor %}
</div>
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="bulkedit" %}
{% bootstrap_field form.active layout="bulkedit" %}
<div class="geodata-section">
{% bootstrap_field form.location layout="bulkedit" %}
<div class="form-group geodata-group"
data-tiles="{{ global_settings.leaflet_tiles|default_if_none:"" }}"
data-attrib="{{ global_settings.leaflet_tiles_attribution }}"
data-icon="{% static "leaflet/images/marker-icon.png" %}"
data-shadow="{% static "leaflet/images/marker-shadow.png" %}">
<label class="col-md-3 control-label">
{% trans "Geo coordinates" %}
</label>
<div class="col-md-9">
<div class="bulk-edit-field-group">
<label class="field-toggle">
<input type="checkbox" name="_bulk" value="{{ form.prefix }}__geo" {% if form.prefix|add:"__geo" in bulk_selected %}checked{% endif %}>
{% trans "change" context "form_bulk" %}
</label>
<div class="field-content">
<div class="row">
<div class="col-md-6">
{% bootstrap_field form.geo_lat layout="inline" %}
{% if global_settings.opencagedata_apikey %}
<p class="attrib">
<a href="https://openstreetmap.org/" target="_blank" tabindex="-1">
{% trans "Geocoding data © OpenStreetMap" %}
</a>
</p>
{% endif %}
</div>
<div class="col-md-6">
{% bootstrap_field form.geo_lon layout="inline" %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% bootstrap_field form.frontpage_text layout="bulkedit" %}
{% bootstrap_field form.is_public layout="bulkedit" %}
{% if meta_forms %}
<div class="form-group metadata-group">
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
<div class="col-md-9">
{% for form in meta_forms %}
<div class="row">
<div class="col-md-4">
<label for="{{ form.value.id_for_label }}">
{{ form.property.name }}
</label>
</div>
<div class="col-md-8">
{% bootstrap_form form layout="bulkedit_inline" %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</fieldset>
<fieldset>
<legend>{% trans "Timeline" %}</legend>
<div class="form-group">
<label class="col-md-3 control-label" for="{{ form.date_from_day.id_for_label }}">
{{ form.date_from_day.label }}
</label>
<div class="col-md-5">
{% bootstrap_field form.date_from_day layout="bulkedit_inline" form_group_class="" %}
</div>
<div class="col-md-4">
{% bootstrap_field form.date_from_time layout="bulkedit_inline" form_group_class="" %}
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label" for="{{ form.date_to_day.id_for_label }}">
{{ form.date_to_day.label }}
</label>
<div class="col-md-5">
{% bootstrap_field form.date_to_day layout="bulkedit_inline" form_group_class="" %}
</div>
<div class="col-md-4">
{% bootstrap_field form.date_to_time layout="bulkedit_inline" form_group_class="" %}
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label" for="{{ form.date_admission_day.id_for_label }}">
{{ form.date_admission_day.label }}
</label>
<div class="col-md-5">
{% bootstrap_field form.date_admission_day layout="bulkedit_inline" form_group_class="" %}
</div>
<div class="col-md-4">
{% bootstrap_field form.date_admission_time layout="bulkedit_inline" form_group_class="" %}
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label" for="{{ form.presale_start_day.id_for_label }}">
{{ form.presale_start_day.label }}
</label>
<div class="col-md-5">
{% bootstrap_field form.presale_start_day layout="bulkedit_inline" form_group_class="" %}
</div>
<div class="col-md-4">
{% bootstrap_field form.presale_start_time layout="bulkedit_inline" form_group_class="" %}
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label" for="{{ form.presale_end_day.id_for_label }}">
{{ form.presale_end_day.label }}
</label>
<div class="col-md-5">
{% bootstrap_field form.presale_end_day layout="bulkedit_inline" form_group_class="" %}
</div>
<div class="col-md-4">
{% bootstrap_field form.presale_end_time layout="bulkedit_inline" form_group_class="" %}
</div>
</div>
</fieldset>
<fieldset>
<legend>{% trans "Item prices" %}</legend>
{% for f in itemvar_forms %}
<div class="form-group">
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
{% if f.variation %}{{ f.item }} {{ f.variation }}{% else %}{{ f.item }}{% endif %}
</label>
<div class="col-md-6">
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="bulkedit_inline" %}
</div>
<div class="col-md-3">
{% bootstrap_field f.disabled layout="bulkedit_inline" form_group_class="" %}
</div>
</div>
{% endfor %}
</fieldset>
<fieldset>
<legend>{% trans "Quotas" %}</legend>
{% if sampled_quotas|default_if_none:"NONE" == "NONE" %}
<div class="alert alert-warning">
{% 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
<strong>replace</strong> the quota setup of all selected dates.
{% endblocktrans %}
</div>
{% endif %}
<div class="bulk-edit-field-group">
<label class="field-toggle">
<input type="checkbox" name="_bulk" value="__quotas" {% if "__quotas" in bulk_selected %}checked{% endif %}>
{% trans "change" context "form_bulk" %}
</label>
<div class="field-content">
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
{% for form in formset %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% bootstrap_field form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_form_errors form %}
{% bootstrap_field form.size layout="control" %}
{% bootstrap_field form.itemvars layout="control" %}
{% bootstrap_field form.release_after_exit layout="control" %}
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% bootstrap_field formset.empty_form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_field formset.empty_form.size layout="control" %}
{% bootstrap_field formset.empty_form.itemvars layout="control" %}
{% bootstrap_field formset.empty_form.release_after_exit layout="control" %}
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new quota" %}</button>
</p>
</div>
</div>
</div>
</fieldset>
<p>&nbsp;</p>
<fieldset>
<legend>{% trans "Check-in lists" %}</legend>
{% if sampled_lists|default_if_none:"NONE" == "NONE" %}
<div class="alert alert-warning">
{% 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 %}
</div>
{% else %}
<div class="bulk-edit-field-group">
<label class="field-toggle">
<input type="checkbox" name="_bulk" value="__checkinlists" {% if "__checkinlists" in bulk_selected %}checked{% endif %}>
{% trans "change" context "form_bulk" %}
</label>
<div class="field-content">
<div class="formset" data-formset data-formset-prefix="{{ cl_formset.prefix }}">
{{ cl_formset.management_form }}
{% bootstrap_formset_errors cl_formset %}
<div data-formset-body>
{% for form in cl_formset %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% bootstrap_field form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% 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 %}
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ cl_formset.empty_form.id }}
{% bootstrap_field cl_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% bootstrap_field cl_formset.empty_form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_field cl_formset.empty_form.include_pending layout="control" %}
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
{% bootstrap_field cl_formset.empty_form.allow_entry_after_exit layout="control" %}
{% if cl_formset.empty_form.gates %}
{% bootstrap_field cl_formset.empty_form.gates layout="control" %}
{% endif %}
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new check-in list" %}
</button>
</p>
</div>
</div>
{% endif %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -22,14 +22,17 @@
</div> </div>
{% else %} {% else %}
<form class="row filter-form" action="" method="get"> <form class="row filter-form" action="" method="get">
<div class="col-md-3 col-sm-6 col-xs-12"> <div class="col-md-2 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.query layout='inline' %} {% bootstrap_field filter_form.query layout='inline' %}
</div> </div>
<div class="col-md-3 col-sm-6 col-xs-12"> <div class="col-md-2 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.status layout='inline' %} {% bootstrap_field filter_form.status layout='inline' %}
</div> </div>
<div class="col-md-2 col-sm-6 col-xs-12"> <div class="col-md-2 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.date layout='inline' %} {% bootstrap_field filter_form.date_from layout='inline' %}
</div>
<div class="col-md-2 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.date_until layout='inline' %}
</div> </div>
<div class="col-md-2 col-sm-6 col-xs-12"> <div class="col-md-2 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.weekday layout='inline' %} {% bootstrap_field filter_form.weekday layout='inline' %}
@@ -43,16 +46,21 @@
</button> </button>
</div> </div>
</form> </form>
<p> {% if "can_change_event_settings" in request.eventpermset %}
<a href="{% url "control:event.subevents.add" organizer=request.event.organizer.slug event=request.event.slug %}" <p>
class="btn btn-default"><i class="fa fa-plus"></i> <a href="{% url "control:event.subevents.add" organizer=request.event.organizer.slug event=request.event.slug %}"
{% trans "Create a new date" context "subevent" %}</a> class="btn btn-default"><i class="fa fa-plus"></i>
<a href="{% url "control:event.subevents.bulk" organizer=request.event.organizer.slug event=request.event.slug %}" {% trans "Create a new date" context "subevent" %}</a>
class="btn btn-default"><i class="fa fa-plus"></i> <a href="{% url "control:event.subevents.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
{% trans "Create many new dates" context "subevent" %}</a> class="btn btn-default"><i class="fa fa-plus"></i>
</p> {% trans "Create many new dates" context "subevent" %}</a>
</p>
{% endif %}
<form action="{% url "control:event.subevents.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post"> <form action="{% url "control:event.subevents.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post">
{% csrf_token %} {% csrf_token %}
<div class="hidden">
{{ filter_form.as_p }}
</div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-quotas"> <table class="table table-hover table-quotas">
<thead> <thead>
@@ -67,21 +75,33 @@
</th> </th>
<th> <th>
{% trans "Begin" %} {% trans "Begin" %}
<a href="?{% url_replace request 'ordering' '-date_from' %}"><i class="fa fa-caret-down"></i></a> <a href="?{% url_replace request 'filter-ordering' '-date_from' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'date_from' %}"><i class="fa fa-caret-up"></i></a> <a href="?{% url_replace request 'filter-ordering' 'date_from' %}"><i class="fa fa-caret-up"></i></a>
</th> </th>
<th> <th>
{% trans "Paid tickets per quota" %} {% trans "Paid tickets per quota" %}
<a href="?{% url_replace request 'ordering' '-sum_tickets_paid' %}"><i class="fa fa-caret-down"></i></a> <a href="?{% url_replace request 'filter-ordering' '-sum_tickets_paid' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'sum_tickets_paid' %}"><i class="fa fa-caret-up"></i></a> <a href="?{% url_replace request 'filter-ordering' 'sum_tickets_paid' %}"><i class="fa fa-caret-up"></i></a>
</th> </th>
<th> <th>
{% trans "Status" %} {% trans "Status" %}
<a href="?{% url_replace request 'ordering' '-active' %}"><i class="fa fa-caret-down"></i></a> <a href="?{% url_replace request 'filter-ordering' '-active' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'active' %}"><i class="fa fa-caret-up"></i></a> <a href="?{% url_replace request 'filter-ordering' 'active' %}"><i class="fa fa-caret-up"></i></a>
</th> </th>
<th></th> <th></th>
</tr> </tr>
{% if "can_change_event_settings" in request.eventpermset %}
<tr class="table-select-all warning hidden">
<td>
<input type="checkbox" name="__ALL" id="__all">
</td>
<td colspan="5">
<label for="__all">
{% trans "Select all results on other pages as well" %}
</label>
</td>
</tr>
{% endif %}
</thead> </thead>
<tbody> <tbody>
{% for s in subevents %} {% for s in subevents %}
@@ -150,6 +170,10 @@
<button type="submit" class="btn btn-default btn-save" name="action" value="delete"> <button type="submit" class="btn btn-default btn-save" name="action" value="delete">
{% trans "Delete selected" %} {% trans "Delete selected" %}
</button> </button>
<button type="submit" class="btn btn-default btn-save" name="action" value="disable"
formaction="{% url "control:event.subevents.bulkedit" organizer=request.event.organizer.slug event=request.event.slug %}">
{% trans "Change selected" %}
</button>
<button type="submit" class="btn btn-default btn-save" name="action" value="enable"> <button type="submit" class="btn btn-default btn-save" name="action" value="enable">
{% trans "Enable selected" %} {% trans "Enable selected" %}
</button> </button>

View File

@@ -122,6 +122,7 @@ urlpatterns = [
url(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/delete$', organizer.TeamDeleteView.as_view(), url(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/delete$', organizer.TeamDeleteView.as_view(),
name='organizer.team.delete'), name='organizer.team.delete'),
url(r'^organizer/(?P<organizer>[^/]+)/slugrng', main.SlugRNG.as_view(), name='events.add.slugrng'), url(r'^organizer/(?P<organizer>[^/]+)/slugrng', main.SlugRNG.as_view(), name='events.add.slugrng'),
url(r'^organizer/(?P<organizer>[^/]+)/logs', organizer.LogView.as_view(), name='organizer.log'),
url(r'^organizer/(?P<organizer>[^/]+)/export/$', organizer.ExportView.as_view(), name='organizer.export'), url(r'^organizer/(?P<organizer>[^/]+)/export/$', organizer.ExportView.as_view(), name='organizer.export'),
url(r'^organizer/(?P<organizer>[^/]+)/export/do$', organizer.ExportDoView.as_view(), name='organizer.export.do'), url(r'^organizer/(?P<organizer>[^/]+)/export/do$', organizer.ExportDoView.as_view(), name='organizer.export.do'),
url(r'^nav/typeahead/$', typeahead.nav_context_list, name='nav.typeahead'), url(r'^nav/typeahead/$', typeahead.nav_context_list, name='nav.typeahead'),
@@ -173,6 +174,7 @@ urlpatterns = [
url(r'^subevents/add$', subevents.SubEventCreate.as_view(), name='event.subevents.add'), 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_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_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/$', item.ItemList.as_view(), name='event.items'),
url(r'^items/add$', item.ItemCreate.as_view(), name='event.items.add'), url(r'^items/add$', item.ItemCreate.as_view(), name='event.items.add'),
url(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'), url(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),

View File

@@ -51,6 +51,7 @@ from pretix.control.forms.organizer import (
GiftCardUpdateForm, OrganizerDeleteForm, OrganizerForm, GiftCardUpdateForm, OrganizerDeleteForm, OrganizerForm,
OrganizerSettingsForm, OrganizerUpdateForm, TeamForm, WebHookForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm, WebHookForm,
) )
from pretix.control.logdisplay import OVERVIEW_BANLIST
from pretix.control.permissions import ( from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, OrganizerPermissionRequiredMixin, AdministratorPermissionRequiredMixin, OrganizerPermissionRequiredMixin,
) )
@@ -1434,3 +1435,24 @@ class EventMetaPropertyDeleteView(OrganizerDetailViewMixin, OrganizerPermissionR
self.object.delete() self.object.delete()
messages.success(request, _('The selected property has been deleted.')) messages.success(request, _('The selected property has been deleted.'))
return redirect(success_url) return redirect(success_url)
class LogView(OrganizerPermissionRequiredMixin, ListView):
template_name = 'pretixcontrol/organizers/logs.html'
permission = 'can_change_organizer_settings'
model = LogEntry
context_object_name = 'logs'
paginate_by = 20
def get_queryset(self):
qs = self.request.organizer.all_logentries().select_related(
'user', 'content_type', 'api_token', 'oauth_application', 'device'
).order_by('-datetime')
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
if self.request.GET.get('user'):
qs = qs.filter(user_id=self.request.GET.get('user'))
return qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
return ctx

View File

@@ -1,14 +1,17 @@
import copy import copy
from collections import defaultdict
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rruleset from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rruleset
from django.contrib import messages from django.contrib import messages
from django.core.files import File from django.core.files import File
from django.db import connections, transaction from django.db import connections, transaction
from django.db.models import F, IntegerField, OuterRef, Prefetch, Subquery, Sum from django.db.models import (
from django.db.models.functions import Coalesce Count, F, IntegerField, OuterRef, Prefetch, Subquery, Sum,
)
from django.db.models.functions import Coalesce, TruncDate, TruncTime
from django.forms import inlineformset_factory 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.shortcuts import redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.formats import get_format 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.timezone import make_aware
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.views import View 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 import CartPosition, LogEntry
from pretix.base.models.checkin import CheckinList 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.filter import SubEventFilterForm
from pretix.control.forms.item import QuotaForm from pretix.control.forms.item import QuotaForm
from pretix.control.forms.subevents import ( from pretix.control.forms.subevents import (
CheckinListFormSet, QuotaFormSet, RRuleFormSet, SubEventBulkForm, CheckinListFormSet, QuotaFormSet, RRuleFormSet, SubEventBulkEditForm,
SubEventForm, SubEventItemForm, SubEventItemVariationForm, SubEventBulkForm, SubEventForm, SubEventItemForm,
SubEventMetaValueForm, TimeFormSet, SubEventItemVariationForm, SubEventMetaValueForm, TimeFormSet,
) )
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import subevent_forms from pretix.control.signals import subevent_forms
from pretix.control.views import PaginationMixin from pretix.control.views import PaginationMixin
from pretix.control.views.event import MetaDataEditorMixin from pretix.control.views.event import MetaDataEditorMixin
from pretix.helpers import GroupConcat
from pretix.helpers.models import modelcopy from pretix.helpers.models import modelcopy
class SubEventList(EventPermissionRequiredMixin, PaginationMixin, ListView): class SubEventQueryMixin:
model = SubEvent
context_object_name = 'subevents'
template_name = 'pretixcontrol/subevents/index.html'
permission = 'can_change_settings'
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( sum_tickets_paid = Quota.objects.filter(
subevent=OuterRef('pk') subevent=OuterRef('pk')
).order_by().values('subevent').annotate( ).order_by().values('subevent').annotate(
@@ -56,18 +64,39 @@ class SubEventList(EventPermissionRequiredMixin, PaginationMixin, ListView):
).values( ).values(
's' 's'
) )
qs = self.request.event.subevents
qs = self.request.event.subevents.annotate( if list:
sum_tickets_paid=Subquery(sum_tickets_paid, output_field=IntegerField()) qs = qs.annotate(
).prefetch_related( sum_tickets_paid=Subquery(sum_tickets_paid, output_field=IntegerField())
Prefetch('quotas', ).prefetch_related(
queryset=Quota.objects.annotate(s=Coalesce(F('size'), 0)).order_by('-s'), Prefetch('quotas',
to_attr='first_quotas') queryset=Quota.objects.annotate(s=Coalesce(F('size'), 0)).order_by('-s'),
) to_attr='first_quotas')
)
if self.filter_form.is_valid(): if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs) 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 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): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['filter_form'] = self.filter_form ctx['filter_form'] = self.filter_form
@@ -95,10 +124,6 @@ class SubEventList(EventPermissionRequiredMixin, PaginationMixin, ListView):
) )
return ctx return ctx
@cached_property
def filter_form(self):
return SubEventFilterForm(data=self.request.GET)
class SubEventDelete(EventPermissionRequiredMixin, DeleteView): class SubEventDelete(EventPermissionRequiredMixin, DeleteView):
model = SubEvent model = SubEvent
@@ -535,19 +560,13 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi
return formlist return formlist
class SubEventBulkAction(EventPermissionRequiredMixin, View): class SubEventBulkAction(SubEventQueryMixin, EventPermissionRequiredMixin, View):
permission = 'can_change_settings' permission = 'can_change_settings'
@cached_property
def objects(self):
return self.request.event.subevents.filter(
id__in=self.request.POST.getlist('subevent')
)
@transaction.atomic @transaction.atomic
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if request.POST.get('action') == 'disable': if request.POST.get('action') == 'disable':
for obj in self.objects: for obj in self.get_queryset():
obj.log_action( obj.log_action(
'pretix.subevent.changed', user=self.request.user, data={ 'pretix.subevent.changed', user=self.request.user, data={
'active': False 'active': False
@@ -557,7 +576,7 @@ class SubEventBulkAction(EventPermissionRequiredMixin, View):
obj.save(update_fields=['active']) obj.save(update_fields=['active'])
messages.success(request, pgettext_lazy('subevent', 'The selected dates have been disabled.')) messages.success(request, pgettext_lazy('subevent', 'The selected dates have been disabled.'))
elif request.POST.get('action') == 'enable': elif request.POST.get('action') == 'enable':
for obj in self.objects: for obj in self.get_queryset():
obj.log_action( obj.log_action(
'pretix.subevent.changed', user=self.request.user, data={ 'pretix.subevent.changed', user=self.request.user, data={
'active': True 'active': True
@@ -568,11 +587,11 @@ class SubEventBulkAction(EventPermissionRequiredMixin, View):
messages.success(request, pgettext_lazy('subevent', 'The selected dates have been enabled.')) messages.success(request, pgettext_lazy('subevent', 'The selected dates have been enabled.'))
elif request.POST.get('action') == 'delete': elif request.POST.get('action') == 'delete':
return render(request, 'pretixcontrol/subevents/delete_bulk.html', { return render(request, 'pretixcontrol/subevents/delete_bulk.html', {
'allowed': self.objects.filter(orderposition__isnull=True), 'allowed': self.get_queryset().filter(orderposition__isnull=True),
'forbidden': self.objects.filter(orderposition__isnull=False), 'forbidden': self.get_queryset().filter(orderposition__isnull=False).distinct(),
}) })
elif request.POST.get('action') == 'delete_confirm': elif request.POST.get('action') == 'delete_confirm':
for obj in self.objects: for obj in self.get_queryset():
if obj.allow_delete(): if obj.allow_delete():
CartPosition.objects.filter(addon_to__subevent=obj).delete() CartPosition.objects.filter(addon_to__subevent=obj).delete()
obj.cartposition_set.all().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.')) messages.error(self.request, _('We could not save your changes. See below for details.'))
return self.form_invalid(form) 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)

View File

@@ -8,16 +8,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-27 17:45+0000\n" "POT-Creation-Date: 2021-01-27 17:45+0000\n"
"PO-Revision-Date: 2020-12-14 10:00+0000\n" "PO-Revision-Date: 2021-02-16 06:00+0000\n"
"Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n" "Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix/cs/" "Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix/cs/>"
">\n" "\n"
"Language: cs\n" "Language: cs\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
"X-Generator: Weblate 3.10.3\n" "X-Generator: Weblate 4.4.2\n"
#: htmlcov/pretix_control_views_dashboards_py.html:898 #: htmlcov/pretix_control_views_dashboards_py.html:898
#: pretix/control/templates/pretixcontrol/events/index.html:144 #: pretix/control/templates/pretixcontrol/events/index.html:144
@@ -218,17 +218,17 @@ msgstr ""
#: pretix/api/serializers/organizer.py:142 #: pretix/api/serializers/organizer.py:142
#: pretix/control/views/organizer.py:539 #: pretix/control/views/organizer.py:539
msgid "pretix account invitation" msgid "pretix account invitation"
msgstr "" msgstr "pozvánka k pretix účtu"
#: pretix/api/serializers/organizer.py:164 #: pretix/api/serializers/organizer.py:164
#: pretix/control/views/organizer.py:638 #: pretix/control/views/organizer.py:638
msgid "This user already has been invited for this team." msgid "This user already has been invited for this team."
msgstr "Tento uživatel byl již pozván do této skupiny." msgstr "Tento uživatel byl již pozván do tohoto týmu."
#: pretix/api/serializers/organizer.py:180 #: pretix/api/serializers/organizer.py:180
#: pretix/control/views/organizer.py:655 #: pretix/control/views/organizer.py:655
msgid "This user already has permissions for this team." msgid "This user already has permissions for this team."
msgstr "" msgstr "Tento uživatel již má nastavena práva pro tento tým."
#: pretix/api/views/oauth.py:85 pretix/control/logdisplay.py:356 #: pretix/api/views/oauth.py:85 pretix/control/logdisplay.py:356
#, python-brace-format #, python-brace-format
@@ -240,7 +240,7 @@ msgstr ""
#: pretix/api/views/order.py:460 pretix/control/views/orders.py:1186 #: pretix/api/views/order.py:460 pretix/control/views/orders.py:1186
#: pretix/presale/views/order.py:663 pretix/presale/views/order.py:728 #: pretix/presale/views/order.py:663 pretix/presale/views/order.py:728
msgid "You cannot generate an invoice for this order." msgid "You cannot generate an invoice for this order."
msgstr "" msgstr "Nemůžete vygenerovat fakturu pro tuto objednávku."
#: pretix/api/views/order.py:465 pretix/control/views/orders.py:1188 #: pretix/api/views/order.py:465 pretix/control/views/orders.py:1188
#: pretix/presale/views/order.py:665 pretix/presale/views/order.py:730 #: pretix/presale/views/order.py:665 pretix/presale/views/order.py:730
@@ -1868,7 +1868,7 @@ msgstr "Časové pásmo"
#: pretix/base/models/auth.py:108 #: pretix/base/models/auth.py:108
msgid "Two-factor authentication is required to log in" msgid "Two-factor authentication is required to log in"
msgstr "" msgstr "Pro přihlášení je vyžadována dvoufaktorová autentizace"
#: pretix/base/models/auth.py:112 #: pretix/base/models/auth.py:112
msgid "Receive notifications according to my settings below" msgid "Receive notifications according to my settings below"
@@ -1991,7 +1991,7 @@ msgstr ""
#: pretix/base/models/devices.py:91 #: pretix/base/models/devices.py:91
#: pretix/control/templates/pretixcontrol/organizers/gates.html:16 #: pretix/control/templates/pretixcontrol/organizers/gates.html:16
msgid "Gate" msgid "Gate"
msgstr "" msgstr "Brána"
#: pretix/base/models/devices.py:109 #: pretix/base/models/devices.py:109
#: pretix/control/templates/pretixcontrol/organizers/devices.html:38 #: pretix/control/templates/pretixcontrol/organizers/devices.html:38
@@ -2008,7 +2008,7 @@ msgstr ""
#: pretix/base/models/event.py:46 #: pretix/base/models/event.py:46
msgid "The end of the event has to be later than its start." msgid "The end of the event has to be later than its start."
msgstr "" msgstr "Konec události musí být pozdější než začátek."
#: pretix/base/models/event.py:353 #: pretix/base/models/event.py:353
msgid "" msgid ""
@@ -2030,7 +2030,7 @@ msgstr ""
#: pretix/base/models/event.py:369 #: pretix/base/models/event.py:369
msgid "Shop is live" msgid "Shop is live"
msgstr "" msgstr "Obchod je spuštěný"
#: pretix/base/models/event.py:371 #: pretix/base/models/event.py:371
msgid "Event currency" msgid "Event currency"
@@ -2057,7 +2057,7 @@ msgstr ""
#: pretix/base/models/event.py:380 pretix/base/models/event.py:1106 #: pretix/base/models/event.py:380 pretix/base/models/event.py:1106
msgid "Show in lists" msgid "Show in lists"
msgstr "" msgstr "Zobrazit v seznamu"
#: pretix/base/models/event.py:381 #: pretix/base/models/event.py:381
msgid "" msgid ""
@@ -2068,7 +2068,7 @@ msgstr ""
#: pretix/base/models/event.py:384 pretix/base/models/event.py:1120 #: pretix/base/models/event.py:384 pretix/base/models/event.py:1120
#: pretix/control/forms/subevents.py:75 #: pretix/control/forms/subevents.py:75
msgid "End of presale" msgid "End of presale"
msgstr "" msgstr "Konec předprodeje"
#: pretix/base/models/event.py:385 pretix/base/models/event.py:1121 #: pretix/base/models/event.py:385 pretix/base/models/event.py:1121
#: pretix/control/forms/subevents.py:76 #: pretix/control/forms/subevents.py:76
@@ -2080,7 +2080,7 @@ msgstr ""
#: pretix/base/models/event.py:390 pretix/base/models/event.py:1126 #: pretix/base/models/event.py:390 pretix/base/models/event.py:1126
#: pretix/control/forms/subevents.py:69 #: pretix/control/forms/subevents.py:69
msgid "Start of presale" msgid "Start of presale"
msgstr "" msgstr "Začátek předprodeje"
#: pretix/base/models/event.py:391 pretix/base/models/event.py:1127 #: pretix/base/models/event.py:391 pretix/base/models/event.py:1127
#: pretix/control/forms/subevents.py:70 #: pretix/control/forms/subevents.py:70
@@ -2101,13 +2101,13 @@ msgstr ""
#: pretix/base/models/event.py:416 pretix/control/navigation.py:44 #: pretix/base/models/event.py:416 pretix/control/navigation.py:44
msgid "Plugins" msgid "Plugins"
msgstr "" msgstr "Zásuvné moduly"
#: pretix/base/models/event.py:419 #: pretix/base/models/event.py:419
#: pretix/control/templates/pretixcontrol/event/index.html:143 #: pretix/control/templates/pretixcontrol/event/index.html:143
#: pretix/control/templates/pretixcontrol/order/index.html:865 #: pretix/control/templates/pretixcontrol/order/index.html:865
msgid "Internal comment" msgid "Internal comment"
msgstr "" msgstr "Interní poznámka"
#: pretix/base/models/event.py:423 pretix/control/forms/event.py:211 #: pretix/base/models/event.py:423 pretix/control/forms/event.py:211
#: pretix/control/forms/filter.py:988 #: pretix/control/forms/filter.py:988
@@ -2133,7 +2133,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/search/orders.html:44 #: pretix/control/templates/pretixcontrol/search/orders.html:44
#: pretix/presale/templates/pretixpresale/event/waitinglist.html:18 #: pretix/presale/templates/pretixpresale/event/waitinglist.html:18
msgid "Event" msgid "Event"
msgstr "" msgstr "Událost"
#: pretix/base/models/event.py:437 pretix/control/navigation.py:305 #: pretix/base/models/event.py:437 pretix/control/navigation.py:305
#: pretix/control/navigation.py:407 #: pretix/control/navigation.py:407
@@ -2144,7 +2144,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/webhooks.html:37 #: pretix/control/templates/pretixcontrol/organizers/webhooks.html:37
#: pretix/control/views/organizer.py:1205 #: pretix/control/views/organizer.py:1205
msgid "Events" msgid "Events"
msgstr "" msgstr "Události"
#: pretix/base/models/event.py:951 #: pretix/base/models/event.py:951
msgid "" msgid ""
@@ -2182,7 +2182,7 @@ msgstr ""
#: pretix/control/forms/filter.py:1250 #: pretix/control/forms/filter.py:1250
#: pretix/control/templates/pretixcontrol/users/index.html:46 #: pretix/control/templates/pretixcontrol/users/index.html:46
msgid "Active" msgid "Active"
msgstr "" msgstr "Aktivní"
#: pretix/base/models/event.py:1103 #: pretix/base/models/event.py:1103
msgid "" msgid ""
@@ -6399,7 +6399,7 @@ msgstr ""
#: pretix/base/settings.py:1694 #: pretix/base/settings.py:1694
msgid "Primary color" msgid "Primary color"
msgstr "" msgstr "Hlavní barva"
#: pretix/base/settings.py:1714 #: pretix/base/settings.py:1714
msgid "Accent color for success" msgid "Accent color for success"
@@ -9737,7 +9737,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/dashboard.html:3 #: pretix/control/templates/pretixcontrol/dashboard.html:3
#: pretix/control/templates/pretixcontrol/dashboard.html:5 #: pretix/control/templates/pretixcontrol/dashboard.html:5
msgid "Dashboard" msgid "Dashboard"
msgstr "" msgstr "Nástěnka"
#: pretix/control/navigation.py:28 pretix/control/navigation.py:329 #: pretix/control/navigation.py:28 pretix/control/navigation.py:329
#: pretix/control/navigation.py:424 #: pretix/control/navigation.py:424
@@ -10328,7 +10328,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:123 #: pretix/control/templates/pretixcontrol/waitinglist/index.html:123
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html:80 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html:80
msgid "Filter" msgid "Filter"
msgstr "" msgstr "Filtrovat"
#: pretix/control/templates/pretixcontrol/checkin/index.html:52 #: pretix/control/templates/pretixcontrol/checkin/index.html:52
msgid "No attendee record was found." msgid "No attendee record was found."
@@ -10558,11 +10558,11 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/dashboard.html:9 #: pretix/control/templates/pretixcontrol/dashboard.html:9
msgid "Go to event" msgid "Go to event"
msgstr "" msgstr "Jít na událost"
#: pretix/control/templates/pretixcontrol/dashboard.html:15 #: pretix/control/templates/pretixcontrol/dashboard.html:15
msgid "Your upcoming events" msgid "Your upcoming events"
msgstr "" msgstr "Vaše nadcházející události"
#: pretix/control/templates/pretixcontrol/dashboard.html:20 #: pretix/control/templates/pretixcontrol/dashboard.html:20
#: pretix/control/templates/pretixcontrol/events/create_base.html:4 #: pretix/control/templates/pretixcontrol/events/create_base.html:4
@@ -10571,11 +10571,11 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/events/index.html:57 #: pretix/control/templates/pretixcontrol/events/index.html:57
#: pretix/control/templates/pretixcontrol/organizers/detail.html:12 #: pretix/control/templates/pretixcontrol/organizers/detail.html:12
msgid "Create a new event" msgid "Create a new event"
msgstr "" msgstr "Vytvořit novou událost"
#: pretix/control/templates/pretixcontrol/dashboard.html:39 #: pretix/control/templates/pretixcontrol/dashboard.html:39
msgid "View all upcoming events" msgid "View all upcoming events"
msgstr "" msgstr "Zobrazit všechny nadcházející události"
#: pretix/control/templates/pretixcontrol/dashboard.html:44 #: pretix/control/templates/pretixcontrol/dashboard.html:44
msgid "Your most recent events" msgid "Your most recent events"
@@ -13546,7 +13546,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/orders/index.html:98 #: pretix/control/templates/pretixcontrol/orders/index.html:98
msgid "Remove filter" msgid "Remove filter"
msgstr "" msgstr "Odstranit filtr"
#: pretix/control/templates/pretixcontrol/orders/index.html:116 #: pretix/control/templates/pretixcontrol/orders/index.html:116
msgid "Order paid / total" msgid "Order paid / total"
@@ -13618,7 +13618,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/orders/overview.html:69 #: pretix/control/templates/pretixcontrol/orders/overview.html:69
#: pretix/plugins/reports/exporters.py:259 #: pretix/plugins/reports/exporters.py:259
msgid "Purchased" msgid "Purchased"
msgstr "" msgstr "Zakoupeno"
#: pretix/control/templates/pretixcontrol/orders/overview.html:178 #: pretix/control/templates/pretixcontrol/orders/overview.html:178
msgid "" msgid ""
@@ -13679,7 +13679,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/device_connect.html:14 #: pretix/control/templates/pretixcontrol/organizers/device_connect.html:14
msgid "Download pretixSCAN" msgid "Download pretixSCAN"
msgstr "" msgstr "Stáhnout pretixSCAN"
#: pretix/control/templates/pretixcontrol/organizers/device_connect.html:18 #: pretix/control/templates/pretixcontrol/organizers/device_connect.html:18
msgid "" msgid ""
@@ -17735,12 +17735,12 @@ msgstr ""
#: pretix/plugins/reports/exporters.py:120 #: pretix/plugins/reports/exporters.py:120
#, python-format #, python-format
msgid "Page %d" msgid "Page %d"
msgstr "" msgstr "Strana %d"
#: pretix/plugins/reports/exporters.py:122 #: pretix/plugins/reports/exporters.py:122
#, python-format #, python-format
msgid "Created: %s" msgid "Created: %s"
msgstr "" msgstr "Vytvořeno: %s"
#: pretix/plugins/reports/exporters.py:162 #: pretix/plugins/reports/exporters.py:162
msgid "Order overview (PDF)" msgid "Order overview (PDF)"
@@ -17810,10 +17810,8 @@ msgstr ""
#: pretix/plugins/reports/exporters.py:668 #: pretix/plugins/reports/exporters.py:668
#: pretix/plugins/reports/exporters.py:713 #: pretix/plugins/reports/exporters.py:713
#, fuzzy
#| msgid "Country"
msgid "Country code" msgid "Country code"
msgstr "Stát" msgstr "Kód země"
#: pretix/plugins/returnurl/__init__.py:9 #: pretix/plugins/returnurl/__init__.py:9
#: pretix/plugins/returnurl/__init__.py:12 #: pretix/plugins/returnurl/__init__.py:12

View File

@@ -720,6 +720,8 @@ BOOTSTRAP3 = {
'default': 'bootstrap3.renderers.FieldRenderer', 'default': 'bootstrap3.renderers.FieldRenderer',
'inline': 'bootstrap3.renderers.InlineFieldRenderer', 'inline': 'bootstrap3.renderers.InlineFieldRenderer',
'control': 'pretix.control.forms.renderers.ControlFieldRenderer', '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', 'checkout': 'pretix.presale.forms.renderers.CheckoutFieldRenderer',
}, },
} }
@@ -758,3 +760,6 @@ OAUTH2_PROVIDER = {
COUNTRIES_OVERRIDE = { COUNTRIES_OVERRIDE = {
'XK': _('Kosovo'), 'XK': _('Kosovo'),
} }
DATA_UPLOAD_MAX_NUMBER_FIELDS = 25000
DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10 MB

View File

@@ -362,6 +362,9 @@ $(document).ready(function () {
} else { } else {
this.$set(this.rule[this.operator], 1, time); this.$set(this.rule[this.operator], 1, time);
} }
if (event.target.value === "custom") {
this.$set(this.rule[this.operator], 2, 0);
}
}, },
setTimeValue: function (val) { setTimeValue: function (val) {
console.log(val); console.log(val);

View File

@@ -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); el.find("input[name*=question], select[name*=question]").change(questions_toggle_dependent);
questions_toggle_dependent(); questions_toggle_dependent();
}; };
@@ -563,7 +582,6 @@ $(function () {
} }
); );
$("[data-formset]").on("formAdded", "div", function (event) { $("[data-formset]").on("formAdded", "div", function (event) {
console.log("formAdded")
form_handlers($(event.target)); form_handlers($(event.target));
}); });
$(document).on("click", ".variations .variations-select-all", function (e) { $(document).on("click", ".variations .variations-select-all", function (e) {
@@ -672,8 +690,8 @@ $(function () {
// Tables with bulk selection, e.g. subevent list // Tables with bulk selection, e.g. subevent list
$("input[data-toggle-table]").each(function (ev) { $("input[data-toggle-table]").each(function (ev) {
var $toggle = $(this); var $toggle = $(this);
var $table = $toggle.closest("table"); var $table = $toggle.closest("table");
var $selectAll = $table.find(".table-select-all");
var $rows = $table.find("tbody tr"); var $rows = $table.find("tbody tr");
var $checkboxes = $rows.find("td:first-child input[type=checkbox]"); var $checkboxes = $rows.find("td:first-child input[type=checkbox]");
var firstIndex, lastIndex, selectionChecked, onChangeSelectionHappened = false; var firstIndex, lastIndex, selectionChecked, onChangeSelectionHappened = false;
@@ -739,6 +757,7 @@ $(function () {
}); });
var update = function () { var update = function () {
console.log("update!");
var all_same; var all_same;
var checkboxes = $checkboxes.toArray(); var checkboxes = $checkboxes.toArray();
var i = checkboxes.length; var i = checkboxes.length;
@@ -748,19 +767,25 @@ $(function () {
continue; continue;
} }
if (all_same != checkboxes[i].checked) { if (all_same != checkboxes[i].checked) {
$toggle.prop("checked", false).prop("indeterminate", true); $toggle.prop("checked", false).prop("indeterminate", true).trigger("change");
return; return;
} }
} }
$toggle.prop("checked", all_same).prop("indeterminate", false); $toggle.prop("checked", all_same).prop("indeterminate", false).trigger("change");
}; };
var debounceUpdate;
$checkboxes.change(function() { $checkboxes.change(function() {
$(this).closest("tr").toggleClass("warning", this.checked); $(this).closest("tr").toggleClass("warning", this.checked);
update(); // when changing the $toggles checked-property, lots of change events
// get triggered => debounce
if (debounceUpdate) window.clearTimeout(debounceUpdate);
debounceUpdate = window.setTimeout(update, 10);
}); });
$(this).change(function (ev) { $toggle.change(function (ev) {
$checkboxes.prop("checked", this.checked).trigger("change"); if (!$toggle.prop("indeterminate")) $checkboxes.prop("checked", this.checked).trigger("change");
$selectAll.toggleClass("hidden", !this.checked).prop("hidden", !this.checked);
if (!this.checked) $selectAll.find("input").prop("checked", false);
}); });
}); });

View File

@@ -614,9 +614,40 @@ table td > .checkbox input[type="checkbox"] {
padding-bottom: 5px; padding-bottom: 5px;
} }
.batch-select-label { .batch-select-label {
display: block; display: block;
width: 100%; width: 100%;
height: 1.5em; height: 1.5em;
cursor: pointer; cursor: pointer;
} }
.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;
}
}
}
}

View File

@@ -34,16 +34,32 @@ def extract_form_fields(soup):
if field['type'] in ('checkbox', 'radio'): if field['type'] in ('checkbox', 'radio'):
if field.has_attr('checked') and field.has_attr('name'): 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 continue
elif field.has_attr('name'): elif field.has_attr('name'):
# single element name/value fields # 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 continue
# textareas # textareas
for textarea in soup.findAll('textarea'): 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 # select fields
for select in soup.find_all('select'): for select in soup.find_all('select'):
@@ -66,6 +82,11 @@ def extract_form_fields(soup):
else: else:
value = [option['value'] for option in selected_options] 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 return data

View File

@@ -9,10 +9,7 @@ from i18nfield.strings import LazyI18nString
from pytz import timezone from pytz import timezone
from tests.base import SoupTest, extract_form_fields from tests.base import SoupTest, extract_form_fields
from pretix.base.models import ( from pretix.base.models import Event, Order, Organizer, Team, User
Event, Order, OrderPosition, Organizer, SubEvent, Team, User,
)
from pretix.base.models.items import SubEventItem
from pretix.testutils.mock import mocker_context from pretix.testutils.mock import mocker_context
@@ -996,646 +993,6 @@ class EventsTest(SoupTest):
assert doc.select(".has-error") 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): class EventDeletionTest(SoupTest):
@scopes_disabled() @scopes_disabled()
def setUp(self): def setUp(self):

File diff suppressed because it is too large Load Diff