mirror of
https://github.com/pretix/pretix.git
synced 2026-03-04 11:02:27 +00:00
Compare commits
11 Commits
datasync-l
...
answer-exp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e076f4bbd9 | ||
|
|
1a2ee155bd | ||
|
|
9743d7ae52 | ||
|
|
07f38819a6 | ||
|
|
b4dd2145ff | ||
|
|
1c26036976 | ||
|
|
a53fc2d256 | ||
|
|
8d24696ce3 | ||
|
|
a14c545883 | ||
|
|
10829aa2a5 | ||
|
|
f645b5a2d9 |
188
src/pretix/base/subevent.py
Normal file
188
src/pretix/base/subevent.py
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
from ast import literal_eval
|
||||||
|
from collections import namedtuple
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
|
|
||||||
|
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||||
|
from pretix.base.models import Event
|
||||||
|
from pretix.control.forms import SplitDateTimeField
|
||||||
|
from pretix.control.forms.widgets import Select2
|
||||||
|
|
||||||
|
SubEventSelection = namedtuple(
|
||||||
|
typename='SubEventSelection',
|
||||||
|
field_names=['selection', 'subevents', 'start', 'end', ],
|
||||||
|
defaults=['subevent', None, None, None],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
subeventselectionparts = namedtuple(
|
||||||
|
typename='subeventselectionparts',
|
||||||
|
field_names=['selection', 'subevents', 'start', 'end']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SubEventSelectionWrapper:
|
||||||
|
def __init__(self, data: Union[None, SubEventSelection]):
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
def get_queryset(self, event: Event):
|
||||||
|
if self.data.selection == 'subevent':
|
||||||
|
if self.data.subevents is None:
|
||||||
|
return event.subevents.all()
|
||||||
|
else:
|
||||||
|
return event.subevents.filter(pk=self.data.subevents)
|
||||||
|
elif self.data.selection == 'timerange':
|
||||||
|
if self.data.start and self.data.end:
|
||||||
|
return event.subevents.filter(date_from__lte=self.data.start,
|
||||||
|
date_from__gte=self.data.end)
|
||||||
|
elif self.data.start:
|
||||||
|
return event.subevents.filter(date_from__gte=self.data.start)
|
||||||
|
elif self.data.end:
|
||||||
|
return event.subevents.filter(date_from__lte=self.data.end)
|
||||||
|
return event.subevents.all()
|
||||||
|
|
||||||
|
def to_string(self) -> str:
|
||||||
|
if self.data:
|
||||||
|
if self.data.selection == 'subevent':
|
||||||
|
return 'SUBEVENT/pk/{}'.format(self.data.subevents.pk)
|
||||||
|
elif self.data.selection == 'timerange':
|
||||||
|
if self.data.start and self.data.end:
|
||||||
|
return 'SUBEVENT/range/{}/{}'.format(self.data.start.isoformat(), self.data.end.isoformat())
|
||||||
|
elif self.data.start:
|
||||||
|
return 'SUBEVENT/from/{}'.format(self.data.start)
|
||||||
|
elif self.data.end:
|
||||||
|
return 'SUBEVENT/to/{}'.format(self.data.end)
|
||||||
|
return 'SUBEVENT'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_string(cls, input: str):
|
||||||
|
data = SubEventSelection(selection='subevent')
|
||||||
|
|
||||||
|
if input.startswith('SUBEVENT'):
|
||||||
|
parts = input.split('/')
|
||||||
|
if len(parts) == 1:
|
||||||
|
data = SubEventSelection(selection='subevent')
|
||||||
|
elif parts[1] == 'pk':
|
||||||
|
data = SubEventSelection(
|
||||||
|
selection='subevent',
|
||||||
|
subevents=literal_eval(parts[2])
|
||||||
|
)
|
||||||
|
elif parts[1] == 'range':
|
||||||
|
data = SubEventSelection(
|
||||||
|
selection="timerange",
|
||||||
|
start=datetime.fromisoformat(parts[2]),
|
||||||
|
end=datetime.fromisoformat(parts[3]),
|
||||||
|
)
|
||||||
|
elif parts[1] == 'from':
|
||||||
|
data = SubEventSelection(
|
||||||
|
selection="timerange",
|
||||||
|
start=datetime.fromisoformat(parts[2]),
|
||||||
|
)
|
||||||
|
elif parts[1] == 'to':
|
||||||
|
data = SubEventSelection(
|
||||||
|
selection="timerange",
|
||||||
|
end=datetime.fromisoformat(parts[3]),
|
||||||
|
)
|
||||||
|
return SubEventSelectionWrapper(
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SubeventSelectionWidget(forms.MultiWidget):
|
||||||
|
template_name = 'pretixcontrol/forms/widgets/subeventselection.html'
|
||||||
|
parts = SubEventSelection
|
||||||
|
|
||||||
|
def __init__(self, event: Event, status_choices, subevent_choices, *args, **kwargs):
|
||||||
|
widgets = subeventselectionparts(
|
||||||
|
selection=forms.RadioSelect(
|
||||||
|
choices=status_choices,
|
||||||
|
|
||||||
|
),
|
||||||
|
subevents=Select2(
|
||||||
|
attrs={
|
||||||
|
'class': 'simple-subevent-choice',
|
||||||
|
'data-model-select2': 'event',
|
||||||
|
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
||||||
|
'event': event.slug,
|
||||||
|
'organizer': event.organizer.slug,
|
||||||
|
}),
|
||||||
|
'data-placeholder': pgettext_lazy('subevent', 'All dates')
|
||||||
|
},
|
||||||
|
),
|
||||||
|
start=SplitDateTimePickerWidget(),
|
||||||
|
end=SplitDateTimePickerWidget(),
|
||||||
|
|
||||||
|
)
|
||||||
|
widgets.subevents.choices = subevent_choices
|
||||||
|
super().__init__(widgets=widgets, *args, **kwargs)
|
||||||
|
|
||||||
|
def decompress(self, value):
|
||||||
|
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = SubEventSelectionWrapper.from_string(value)
|
||||||
|
if isinstance(value, subeventselectionparts):
|
||||||
|
return value
|
||||||
|
|
||||||
|
return subeventselectionparts(selection='subevent', start=None, end=None, subevents=None)
|
||||||
|
|
||||||
|
|
||||||
|
class SubeventSelectionField(forms.MultiValueField):
|
||||||
|
widget = SubeventSelectionWidget
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.event = kwargs.pop('event')
|
||||||
|
|
||||||
|
choices = [
|
||||||
|
("subevent", _("Subevent")),
|
||||||
|
("timerange", _("Timerange"))
|
||||||
|
]
|
||||||
|
|
||||||
|
fields = SubEventSelection(
|
||||||
|
selection=forms.ChoiceField(
|
||||||
|
choices=choices,
|
||||||
|
required=True,
|
||||||
|
),
|
||||||
|
subevents=forms.ModelChoiceField(
|
||||||
|
required=False,
|
||||||
|
queryset=self.event.subevents,
|
||||||
|
empty_label=pgettext_lazy('subevent', 'All dates')
|
||||||
|
),
|
||||||
|
start=SplitDateTimeField(
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
end=SplitDateTimeField(
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
kwargs['widget'] = SubeventSelectionWidget(
|
||||||
|
event=self.event,
|
||||||
|
status_choices=choices,
|
||||||
|
subevent_choices=fields.subevents.widget.choices,
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
fields=fields, require_all_fields=False, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def compress(self, data_list):
|
||||||
|
if not data_list:
|
||||||
|
return None
|
||||||
|
return SubEventSelectionWrapper(data=SubEventSelection(*data_list)).to_string()
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
data = subeventselectionparts(*value)
|
||||||
|
|
||||||
|
if data.selection == "timerange":
|
||||||
|
if (data.start != ["", ""] and data.end != ["", ""]) and data.end < data.start:
|
||||||
|
raise ValidationError(_("The end date must be after the start date."))
|
||||||
|
|
||||||
|
if (data.start == ["", ""]) and (data.end == ["", ""]):
|
||||||
|
raise ValidationError(_('At least one of start and end must be specified.'))
|
||||||
|
|
||||||
|
return super().clean(value)
|
||||||
@@ -47,6 +47,7 @@ from django.urls import reverse
|
|||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.html import escape, format_html
|
from django.utils.html import escape, format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext as __, gettext_lazy as _
|
from django.utils.translation import gettext as __, gettext_lazy as _
|
||||||
from django_scopes.forms import (
|
from django_scopes.forms import (
|
||||||
SafeModelChoiceField, SafeModelMultipleChoiceField,
|
SafeModelChoiceField, SafeModelMultipleChoiceField,
|
||||||
@@ -56,11 +57,14 @@ from i18nfield.forms import I18nFormField, I18nTextarea
|
|||||||
from pretix.base.forms import I18nFormSet, I18nMarkdownTextarea, I18nModelForm
|
from pretix.base.forms import I18nFormSet, I18nMarkdownTextarea, I18nModelForm
|
||||||
from pretix.base.forms.widgets import DatePickerWidget
|
from pretix.base.forms.widgets import DatePickerWidget
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Item, ItemCategory, ItemProgramTime, ItemVariation, Question,
|
Item, ItemCategory, ItemProgramTime, ItemVariation, Order, OrderPosition,
|
||||||
QuestionOption, Quota,
|
Question, QuestionOption, Quota,
|
||||||
)
|
)
|
||||||
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
|
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
|
||||||
from pretix.base.signals import item_copy_data
|
from pretix.base.signals import item_copy_data
|
||||||
|
from pretix.base.subevent import (
|
||||||
|
SubeventSelectionField, SubEventSelectionWrapper,
|
||||||
|
)
|
||||||
from pretix.control.forms import (
|
from pretix.control.forms import (
|
||||||
ButtonGroupRadioSelect, ExtFileField, ItemMultipleChoiceField,
|
ButtonGroupRadioSelect, ExtFileField, ItemMultipleChoiceField,
|
||||||
SalesChannelCheckboxSelectMultiple, SplitDateTimeField,
|
SalesChannelCheckboxSelectMultiple, SplitDateTimeField,
|
||||||
@@ -272,6 +276,87 @@ class QuestionOptionForm(I18nModelForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionFilterForm(forms.Form):
|
||||||
|
STATUS_VARIANTS = [
|
||||||
|
("", _("All orders")),
|
||||||
|
("p", _("Paid")),
|
||||||
|
("pv", _("Paid or confirmed")),
|
||||||
|
("n", _("Pending")),
|
||||||
|
("np", _("Pending or paid")),
|
||||||
|
("o", _("Pending (overdue)")),
|
||||||
|
("e", _("Expired")),
|
||||||
|
("ne", _("Pending or expired")),
|
||||||
|
("c", _("Canceled"))
|
||||||
|
]
|
||||||
|
|
||||||
|
status = forms.ChoiceField(
|
||||||
|
choices=STATUS_VARIANTS,
|
||||||
|
widget=forms.Select(
|
||||||
|
attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
required=False,
|
||||||
|
label=_("Status"),
|
||||||
|
initial="np",
|
||||||
|
)
|
||||||
|
item = forms.ChoiceField(
|
||||||
|
choices=[],
|
||||||
|
widget=forms.Select(
|
||||||
|
attrs={'class': 'form-control'}
|
||||||
|
),
|
||||||
|
required=False,
|
||||||
|
label=_("Items")
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.event = kwargs.pop('event')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if self.event.has_subevents:
|
||||||
|
self.fields['subevent_selection'] = SubeventSelectionField(
|
||||||
|
event=self.event,
|
||||||
|
label=_("Subevents"),
|
||||||
|
help_text=_("Select the subevents that should be included in the statistics either by subevent or by the timerange in which they occur."),
|
||||||
|
)
|
||||||
|
self.fields['item'].choices = [('', _('All products'))] + [(item.id, item.name) for item in
|
||||||
|
Item.objects.filter(event=self.event)]
|
||||||
|
|
||||||
|
def filter_qs(self):
|
||||||
|
fdata = self.cleaned_data
|
||||||
|
|
||||||
|
opqs = OrderPosition.objects.filter(
|
||||||
|
order__event=self.event,
|
||||||
|
)
|
||||||
|
sub_event_qs = SubEventSelectionWrapper.from_string(fdata['subevent_selection']).get_queryset(self.event)
|
||||||
|
opqs = opqs.filter(subevent__in=sub_event_qs)
|
||||||
|
|
||||||
|
s = fdata.get("status", "np")
|
||||||
|
if s != "":
|
||||||
|
if s == 'o':
|
||||||
|
opqs = opqs.filter(order__status=Order.STATUS_PENDING,
|
||||||
|
order__expires__lt=now().replace(hour=0, minute=0, second=0))
|
||||||
|
elif s == 'np':
|
||||||
|
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
|
||||||
|
elif s == 'pv':
|
||||||
|
opqs = opqs.filter(
|
||||||
|
Q(order__status=Order.STATUS_PAID) |
|
||||||
|
Q(order__status=Order.STATUS_PENDING, order__valid_if_pending=True)
|
||||||
|
)
|
||||||
|
elif s == 'ne':
|
||||||
|
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED])
|
||||||
|
else:
|
||||||
|
opqs = opqs.filter(order__status=s)
|
||||||
|
|
||||||
|
if s not in (Order.STATUS_CANCELED, ""):
|
||||||
|
opqs = opqs.filter(canceled=False)
|
||||||
|
if fdata.get("item", "") != "":
|
||||||
|
i = fdata.get("item", "")
|
||||||
|
opqs = opqs.filter(item_id__in=(i,))
|
||||||
|
|
||||||
|
return opqs
|
||||||
|
|
||||||
|
|
||||||
class QuotaForm(I18nModelForm):
|
class QuotaForm(I18nModelForm):
|
||||||
itemvars = forms.MultipleChoiceField(
|
itemvars = forms.MultipleChoiceField(
|
||||||
label=_("Products"),
|
label=_("Products"),
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
<div class="subevent-selection col-lg-12">
|
||||||
|
{% for group_name, group_choices, group-index in widget.subwidgets.0.optgroups %}
|
||||||
|
{% for selopt in group_choices %}
|
||||||
|
<div class="radio">
|
||||||
|
<label class="col-lg-2">
|
||||||
|
<input type="radio" name="{{ widget.subwidgets.0.name }}" value="{{ selopt.value }}"
|
||||||
|
{% include "django/forms/widgets/attrs.html" with widget=selopt %} />
|
||||||
|
{{ selopt.label }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{% if selopt.value == "subevent" %}
|
||||||
|
|
||||||
|
{% with widget.subwidgets.1 as widget %}
|
||||||
|
{% include widget.template_name %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% elif selopt.value == "timerange" %}
|
||||||
|
|
||||||
|
{% with widget.subwidgets.2 as widget %}
|
||||||
|
{% include widget.template_name %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<span class="spacer">{% trans "until" %}</span>
|
||||||
|
|
||||||
|
{% with widget.subwidgets.3 as widget %}
|
||||||
|
{% include widget.template_name %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
@@ -5,6 +5,11 @@
|
|||||||
{% load formset_tags %}
|
{% load formset_tags %}
|
||||||
{% block title %}{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}{% endblock %}
|
{% block title %}{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}{% endblock %}
|
||||||
{% block inside %}
|
{% block inside %}
|
||||||
|
{% for e in form.errors.values %}
|
||||||
|
<div class="alert alert-danger has-error">
|
||||||
|
{{ e }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
<h1>
|
<h1>
|
||||||
{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}
|
{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}
|
||||||
<a href="{% url "control:event.items.questions.edit" event=request.event.slug organizer=request.event.organizer.slug question=question.pk %}"
|
<a href="{% url "control:event.items.questions.edit" event=request.event.slug organizer=request.event.organizer.slug question=question.pk %}"
|
||||||
@@ -20,35 +25,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<form class="panel-body filter-form" action="" method="get">
|
<form class="panel-body filter-form" action="" method="get">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-2 col-sm-6 col-xs-6">
|
|
||||||
<select name="status" class="form-control">
|
<div class="col-lg-4 col-sm-6 col-xs-6">
|
||||||
<option value="" {% if request.GET.status == "" %}selected="selected"{% endif %}>{% trans "All orders" %}</option>
|
{% bootstrap_label form.status.label %}
|
||||||
<option value="p" {% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "Paid" %}</option>
|
{% bootstrap_field form.status layout="inline" %}
|
||||||
<option value="pv" {% if request.GET.status == "pv" %}selected="selected"{% endif %}>{% trans "Paid or confirmed" %}</option>
|
|
||||||
<option value="n" {% if request.GET.status == "n" %}selected="selected"{% endif %}>{% trans "Pending" %}</option>
|
</div>
|
||||||
<option value="np" {% if request.GET.status == "np" or "status" not in request.GET %}selected="selected"{% endif %}>{% trans "Pending or paid" %}</option>
|
<div class="col-lg-8 col-sm-6 col-xs-6">
|
||||||
<option value="o" {% if request.GET.status == "o" %}selected="selected"{% endif %}>{% trans "Pending (overdue)" %}</option>
|
{% bootstrap_label form.item.label %}
|
||||||
<option value="e" {% if request.GET.status == "e" %}selected="selected"{% endif %}>{% trans "Expired" %}</option>
|
{% bootstrap_field form.item layout="inline" %}
|
||||||
<option value="ne" {% if request.GET.status == "ne" %}selected="selected"{% endif %}>{% trans "Pending or expired" %}</option>
|
</div>
|
||||||
<option value="c" {% if request.GET.status == "c" %}selected="selected"{% endif %}>{% trans "Canceled" %}</option>
|
|
||||||
</select>
|
<div class="col-lg-12 col-sm-6 col-xs-6">
|
||||||
</div>
|
{% bootstrap_label form.subevent_selection.label %}
|
||||||
<div class="col-lg-5 col-sm-6 col-xs-6">
|
{{ form.subevent_selection }}
|
||||||
<select name="item" class="form-control">
|
<div class="help-block">
|
||||||
<option value="">{% trans "All products" %}</option>
|
{{ form.subevent_selection.help_text }}
|
||||||
{% for item in items %}
|
|
||||||
<option value="{{ item.id }}"
|
|
||||||
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
|
|
||||||
{{ item.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{% if request.event.has_subevents %}
|
|
||||||
<div class="col-lg-5 col-sm-6 col-xs-6">
|
|
||||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<button class="btn btn-primary btn-lg" type="submit">
|
<button class="btn btn-primary btn-lg" type="submit">
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from collections import OrderedDict, namedtuple
|
|||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
|
|
||||||
|
from django import forms
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
@@ -45,6 +46,7 @@ from django.db import transaction
|
|||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Count, Exists, F, OuterRef, Prefetch, ProtectedError, Q,
|
Count, Exists, F, OuterRef, Prefetch, ProtectedError, Q,
|
||||||
)
|
)
|
||||||
|
from django.dispatch import receiver
|
||||||
from django.forms.models import inlineformset_factory
|
from django.forms.models import inlineformset_factory
|
||||||
from django.http import (
|
from django.http import (
|
||||||
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
|
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
|
||||||
@@ -63,9 +65,10 @@ from pretix.api.serializers.item import (
|
|||||||
ItemAddOnSerializer, ItemBundleSerializer, ItemProgramTimeSerializer,
|
ItemAddOnSerializer, ItemBundleSerializer, ItemProgramTimeSerializer,
|
||||||
ItemVariationSerializer,
|
ItemVariationSerializer,
|
||||||
)
|
)
|
||||||
|
from pretix.base.exporter import ListExporter
|
||||||
from pretix.base.forms import I18nFormSet
|
from pretix.base.forms import I18nFormSet
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation, Order,
|
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation,
|
||||||
OrderPosition, Question, QuestionAnswer, QuestionOption, Quota,
|
OrderPosition, Question, QuestionAnswer, QuestionOption, Quota,
|
||||||
SeatCategoryMapping, Voucher,
|
SeatCategoryMapping, Voucher,
|
||||||
)
|
)
|
||||||
@@ -73,12 +76,13 @@ from pretix.base.models.event import SubEvent
|
|||||||
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
|
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.base.services.tickets import invalidate_cache
|
from pretix.base.services.tickets import invalidate_cache
|
||||||
from pretix.base.signals import quota_availability
|
from pretix.base.signals import quota_availability, register_data_exporters
|
||||||
from pretix.control.forms.item import (
|
from pretix.control.forms.item import (
|
||||||
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
|
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
|
||||||
ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemProgramTimeForm,
|
ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemProgramTimeForm,
|
||||||
ItemProgramTimeFormSet, ItemUpdateForm, ItemVariationForm,
|
ItemProgramTimeFormSet, ItemUpdateForm, ItemVariationForm,
|
||||||
ItemVariationsFormSet, QuestionForm, QuestionOptionForm, QuotaForm,
|
ItemVariationsFormSet, QuestionFilterForm, QuestionForm,
|
||||||
|
QuestionOptionForm, QuotaForm,
|
||||||
)
|
)
|
||||||
from pretix.control.permissions import (
|
from pretix.control.permissions import (
|
||||||
EventPermissionRequiredMixin, event_permission_required,
|
EventPermissionRequiredMixin, event_permission_required,
|
||||||
@@ -660,46 +664,73 @@ class QuestionMixin:
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingView, DetailView):
|
class QuestionAnswerExporter(ListExporter):
|
||||||
|
identifier = 'question_answer_exporter'
|
||||||
|
verbose_name = _('Question answers exporter')
|
||||||
|
description = _('Download a spreadsheet containing question answers')
|
||||||
|
category = _('Order data')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def additional_form_fields(self):
|
||||||
|
form = {
|
||||||
|
'question':
|
||||||
|
forms.ModelChoiceField(
|
||||||
|
label=_('Question'),
|
||||||
|
queryset=Question.objects.filter(event=self.event),
|
||||||
|
),
|
||||||
|
**QuestionFilterForm(event=self.event).fields
|
||||||
|
}
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
def iterate_list(self, form_data):
|
||||||
|
question = Question.objects.filter(event=self.event).get(pk=form_data['question'])
|
||||||
|
|
||||||
|
opqs = QuestionFilterForm(event=self.event, data=form_data).order_position_queryset()
|
||||||
|
|
||||||
|
qs = QuestionAnswer.objects.filter(
|
||||||
|
question=question, orderposition__isnull=False,
|
||||||
|
)
|
||||||
|
qs = qs.filter(orderposition__in=opqs)
|
||||||
|
|
||||||
|
headers = [
|
||||||
|
_("Subevent"),
|
||||||
|
_("Event start time"),
|
||||||
|
_("Order"),
|
||||||
|
_("Order position"),
|
||||||
|
question.question
|
||||||
|
]
|
||||||
|
|
||||||
|
yield headers
|
||||||
|
yield self.ProgressSetTotal(total=qs.count())
|
||||||
|
|
||||||
|
for questionAnswer in qs.iterator(chunk_size=1000):
|
||||||
|
row = [
|
||||||
|
questionAnswer.orderposition.subevent.name,
|
||||||
|
questionAnswer.orderposition.subevent.date_from.replace(tzinfo=None),
|
||||||
|
questionAnswer.orderposition.order.code,
|
||||||
|
questionAnswer.orderposition.positionid,
|
||||||
|
questionAnswer.answer
|
||||||
|
]
|
||||||
|
|
||||||
|
yield row
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_data_exporters, dispatch_uid="exporter_questions_exporter")
|
||||||
|
def register_data_exporter(sender, **kwargs):
|
||||||
|
return QuestionAnswerExporter
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionView(EventPermissionRequiredMixin, ChartContainingView, DetailView):
|
||||||
model = Question
|
model = Question
|
||||||
template_name = 'pretixcontrol/items/question.html'
|
template_name = 'pretixcontrol/items/question.html'
|
||||||
permission = 'can_change_items'
|
permission = 'can_change_items'
|
||||||
template_name_field = 'question'
|
template_name_field = 'question'
|
||||||
|
|
||||||
def get_answer_statistics(self):
|
def get_answer_statistics(self, opqs: OrderPosition):
|
||||||
opqs = OrderPosition.objects.filter(
|
|
||||||
order__event=self.request.event,
|
|
||||||
)
|
|
||||||
qs = QuestionAnswer.objects.filter(
|
qs = QuestionAnswer.objects.filter(
|
||||||
question=self.object, orderposition__isnull=False,
|
question=self.object, orderposition__isnull=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.request.GET.get("subevent", "") != "":
|
|
||||||
opqs = opqs.filter(subevent=self.request.GET["subevent"])
|
|
||||||
|
|
||||||
s = self.request.GET.get("status", "np")
|
|
||||||
if s != "":
|
|
||||||
if s == 'o':
|
|
||||||
opqs = opqs.filter(order__status=Order.STATUS_PENDING,
|
|
||||||
order__expires__lt=now().replace(hour=0, minute=0, second=0))
|
|
||||||
elif s == 'np':
|
|
||||||
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
|
|
||||||
elif s == 'pv':
|
|
||||||
opqs = opqs.filter(
|
|
||||||
Q(order__status=Order.STATUS_PAID) |
|
|
||||||
Q(order__status=Order.STATUS_PENDING, order__valid_if_pending=True)
|
|
||||||
)
|
|
||||||
elif s == 'ne':
|
|
||||||
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED])
|
|
||||||
else:
|
|
||||||
opqs = opqs.filter(order__status=s)
|
|
||||||
|
|
||||||
if s not in (Order.STATUS_CANCELED, ""):
|
|
||||||
opqs = opqs.filter(canceled=False)
|
|
||||||
if self.request.GET.get("item", "") != "":
|
|
||||||
i = self.request.GET.get("item", "")
|
|
||||||
opqs = opqs.filter(item_id__in=(i,))
|
|
||||||
|
|
||||||
qs = qs.filter(orderposition__in=opqs)
|
qs = qs.filter(orderposition__in=opqs)
|
||||||
op_cnt = opqs.filter(item__in=self.object.items.all()).count()
|
op_cnt = opqs.filter(item__in=self.object.items.all()).count()
|
||||||
|
|
||||||
@@ -747,8 +778,20 @@ class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingV
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data()
|
ctx = super().get_context_data()
|
||||||
ctx['items'] = self.object.items.all()
|
ctx['items'] = self.object.items.all()
|
||||||
stats = self.get_answer_statistics()
|
if self.request.GET:
|
||||||
ctx['stats'], ctx['total'] = stats
|
ctx['form'] = QuestionFilterForm(
|
||||||
|
data=self.request.GET,
|
||||||
|
event=self.request.event,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ctx['form'] = QuestionFilterForm(
|
||||||
|
event=self.request.event,
|
||||||
|
|
||||||
|
)
|
||||||
|
if ctx['form'].is_valid():
|
||||||
|
opqs = ctx['form'].filter_qs()
|
||||||
|
stats = self.get_answer_statistics(opqs)
|
||||||
|
ctx['stats'], ctx['total'] = stats
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def get_object(self, queryset=None) -> Question:
|
def get_object(self, queryset=None) -> Question:
|
||||||
|
|||||||
@@ -216,6 +216,20 @@ td > .form-group > .checkbox {
|
|||||||
.input-group-btn .btn {
|
.input-group-btn .btn {
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
.subevent-selection{
|
||||||
|
.splitdatetimerow{
|
||||||
|
max-width: 500px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.spacer{
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
.select2{
|
||||||
|
max-width:500px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
.reldatetime {
|
.reldatetime {
|
||||||
input[type=text], select {
|
input[type=text], select {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
Reference in New Issue
Block a user