Refactor: separate forms from view, improve code style

This commit is contained in:
Raphael Michel
2015-06-03 13:19:59 +02:00
parent 3114e2d959
commit 423dc9f0aa
21 changed files with 826 additions and 803 deletions

View File

@@ -1,134 +1,20 @@
from collections import OrderedDict
from django.conf import settings
from django.contrib import messages
from django.shortcuts import render, redirect
from django.utils.functional import cached_property
from django.views.generic import FormView
from django.views.generic.base import TemplateView
from django.views.generic.detail import SingleObjectMixin
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from pytz import common_timezones
from pretix.base.forms import VersionedModelForm, SettingsForm
from pretix.control.forms.event import ProviderForm, TicketSettingsForm, EventSettingsForm, EventUpdateForm
from pretix.base.models import Event
from pretix.base.signals import register_payment_providers, register_ticket_outputs
from pretix.control.permissions import EventPermissionRequiredMixin
from . import UpdateView
class EventUpdateForm(VersionedModelForm):
def clean_slug(self):
return self.instance.slug
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['slug'].widget.attrs['readonly'] = 'readonly'
class Meta:
model = Event
localized_fields = '__all__'
fields = [
'name',
'slug',
'currency',
'date_from',
'date_to',
'presale_start',
'presale_end',
]
class EventSettingsForm(SettingsForm):
show_date_to = forms.BooleanField(
label=_("Show event end date"),
help_text=_("If disabled, only event's start date will be displayed to the public."),
required=False
)
show_times = forms.BooleanField(
label=_("Show dates with time"),
help_text=_("If disabled, the event's start and end date will be displayed without the time of day."),
required=False
)
payment_term_days = forms.IntegerField(
label=_('Payment term in days'),
help_text=_("The number of days after placing an order the user has to pay to preserve his reservation."),
)
show_items_outside_presale_period = forms.BooleanField(
label=_("Show items outside presale period"),
help_text=_("Show item details before presale has started and after presale has ended"),
required=False
)
presale_start_show_date = forms.BooleanField(
label=_("Show start date"),
help_text=_("Show the presale start date before presale has started"),
required=False
)
payment_term_last = forms.DateTimeField(
label=_('Last date of payments'),
help_text=_("The last date any payments are accepted. This has precedence over the number of "
"days configured above."),
required=False
)
payment_term_accept_late = forms.BooleanField(
label=_('Accept late payments'),
help_text=_("Accept payments that come after the end of the order's payment term. "
"Payments will only be accepted if the regarding quotas have remaining "
"capacity. No payments will be accepted after the 'Last date of payments' "
"configured above."),
required=False
)
last_order_modification_date = forms.DateTimeField(
label=_('Last date of modifications'),
help_text=_("The last date users can modify details of their orders, such as attendee names or "
"answers to questions."),
required=False
)
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Default timezone"),
)
locales = forms.MultipleChoiceField(
choices=settings.LANGUAGES,
label=_("Available langauges"),
)
locale = forms.ChoiceField(
choices=settings.LANGUAGES,
label=_("Default language"),
)
user_mail_required = forms.BooleanField(
label=_("Require e-mail adresses"),
help_text=_("Require all customers to provide an e-mail address."),
required=False
)
attendee_names_asked = forms.BooleanField(
label=_("Ask for attendee names"),
help_text=_("Ask for a name for all tickets which include admission to the event."),
required=False
)
attendee_names_required = forms.BooleanField(
label=_("Require attendee names"),
help_text=_("Require customers to fill in the names of all attendees."),
required=False
)
max_items_per_order = forms.IntegerField(
min_value=1,
label=_("Maximum number of items per order")
)
reservation_time = forms.IntegerField(
min_value=0,
label=_("Reservation period"),
help_text=_("The number of minutes the items in a user's card are reserved for this user."),
)
mail_from = forms.EmailField(
label=_("Sender address"),
help_text=_("Sender address for outgoing e-mails")
)
class EventUpdate(EventPermissionRequiredMixin, UpdateView):
model = Event
form_class = EventUpdateForm
@@ -217,34 +103,6 @@ class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin
}) + '?success=true'
class ProviderForm(SettingsForm):
"""
This is a SettingsForm, but if fields are set to required=True, validation
errors are only raised if the payment method is enabled.
"""
def __init__(self, *args, **kwargs):
self.settingspref = kwargs.pop('settingspref')
super().__init__(*args, **kwargs)
def prepare_fields(self):
for k, v in self.fields.items():
v._required = v.required
v.required = False
v.widget.is_required = False
def clean(self):
cleaned_data = super().clean()
enabled = cleaned_data.get(self.settingspref + '_enabled') == 'True'
if not enabled:
return
for k, v in self.fields.items():
val = cleaned_data.get(k)
if v._required and (val is None or val == ""):
print(enabled, k, v)
self.add_error(k, _('This field is required.'))
class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
model = Event
context_object_name = 'event'
@@ -306,38 +164,6 @@ class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMi
}) + '?success=true'
class TicketSettingsForm(SettingsForm):
ticket_download = forms.BooleanField(
label=_("Use feature"),
help_text=_("Use pretix to generate tickets for the user to download and print out."),
required=False
)
ticket_download_date = forms.DateTimeField(
label=_("Download date"),
help_text=_("Ticket download will be offered after this date."),
required=True
)
def prepare_fields(self):
# See clean()
for k, v in self.fields.items():
v._required = v.required
v.required = False
v.widget.is_required = False
def clean(self):
# required=True files should only be required if the feature is enabled
cleaned_data = super().clean()
enabled = cleaned_data.get('ticket_download') == 'True'
if not enabled:
return
for k, v in self.fields.items():
val = cleaned_data.get(k)
if v._required and (val is None or val == ""):
print(enabled, k, v)
self.add_error(k, _('This field is required.'))
class TicketSettings(EventPermissionRequiredMixin, FormView):
model = Event
form_class = TicketSettingsForm

View File

@@ -1,400 +0,0 @@
from functools import partial
from itertools import product
from django import forms
from django.core.exceptions import ValidationError
from django.db import transaction
from django.forms import BaseInlineFormSet
from django.forms.widgets import flatatt
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from pretix.base.forms import VersionedModelForm
from pretix.base.models import ItemVariation, Item
class I18nInlineFormSet(BaseInlineFormSet):
"""
This is equivalent to a normal BaseInlineFormset, but cares for the special needs
of I18nForms (see there for more information).
"""
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event', None)
super().__init__(*args, **kwargs)
def _construct_form(self, i, **kwargs):
kwargs['event'] = self.event
return super()._construct_form(i, **kwargs)
class TolerantFormsetModelForm(VersionedModelForm):
"""
This is equivalent to a normal VersionedModelForm, but works around a problem that
arises when the form is used inside a FormSet with can_order=True and django-formset-js
enabled. In this configuration, even empty "extra" forms might have an ORDER value
sent and Django marks the form as empty and raises validation errors because the other
fields have not been filled.
"""
def has_changed(self) -> bool:
"""
Returns True if data differs from initial. Contrary to the default
implementation, the ORDER field is being ignored.
"""
for name, field in self.fields.items():
if name == 'ORDER' or name == 'id':
continue
prefixed_name = self.add_prefix(name)
data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
if not field.show_hidden_initial:
initial_value = self.initial.get(name, field.initial)
if callable(initial_value):
initial_value = initial_value()
else:
initial_prefixed_name = self.add_initial_prefix(name)
hidden_widget = field.hidden_widget()
try:
initial_value = field.to_python(hidden_widget.value_from_datadict(
self.data, self.files, initial_prefixed_name))
except forms.ValidationError:
# Always assume data has changed if validation fails.
self._changed_data.append(name)
continue
# We're using a private API of Django here. This is not nice, but no problem as it seems
# like this will become a public API in future Django.
if field._has_changed(initial_value, data_value):
return True
return False
class RestrictionForm(TolerantFormsetModelForm):
"""
The restriction form provides useful functionality for all forms
representing a restriction instance. To be concret, this form does
the necessary magic to make the 'variations' field work correctly
and look beautiful.
"""
def __init__(self, *args, **kwargs):
if 'item' in kwargs:
self.item = kwargs['item']
del kwargs['item']
super().__init__(*args, **kwargs)
if 'variations' in self.fields and isinstance(self.fields['variations'], VariationsField):
self.fields['variations'].set_item(self.item)
class RestrictionInlineFormset(forms.BaseInlineFormSet):
"""
This is the base class you should use for any formset you return
from a ``restriction_formset`` signal receiver that contains
RestrictionForm objects as its forms, as it correcly handles the
necessary item parameter for the RestrictionForm. While this could
be achieved with a regular formset, this also adds a
``initialized_empty_form`` method which is the only way to correctly
render a working empty form for a JavaScript-enabled restriction formset.
"""
def __init__(self, data=None, files=None, instance=None,
save_as_new=False, prefix=None, queryset=None, **kwargs):
super().__init__(
data, files, instance, save_as_new, prefix, queryset, **kwargs
)
if isinstance(self.instance, Item):
self.queryset = self.queryset.as_of().prefetch_related("variations")
def initialized_empty_form(self):
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
item=self.instance
)
self.add_fields(form, None)
return form
def _construct_form(self, i, **kwargs):
kwargs['item'] = self.instance
return super()._construct_form(i, **kwargs)
class Meta:
exclude = ['item']
def selector(values, prop):
# Given an iterable of PropertyValue objects, this will return a
# list of their primary keys, ordered by the primary keys of the
# properties they belong to EXCEPT the value for the property prop2.
# We'll see later why we need this.
return [
v.identity for v in sorted(values, key=lambda v: v.prop.identity)
if v.prop.identity != prop.identity
]
def sort(v, prop):
# Given a list of variations, this will sort them by their position
# on the x-axis
return v[prop.identity].sortkey
class VariationsFieldRenderer(forms.widgets.CheckboxFieldRenderer):
"""
This is the default renderer for a VariationsField. Based on the choice input class
this renders a list or a matrix of checkboxes/radio buttons/...
"""
def __init__(self, name, value, attrs, choices):
self.name = name
self.value = value
self.attrs = attrs
self.choices = choices
def render(self):
"""
Outputs a grid for this set of choice fields.
"""
if len(self.choices) == 0:
raise ValueError("Can't handle empty lists")
variations = []
for key, value in self.choices:
value['key'] = key
variations.append(value)
properties = [v.prop for v in variations[0].relevant_values()]
dimension = len(properties)
id_ = self.attrs.get('id', None)
start_tag = format_html('<div class="variations" id="{0}">', id_) if id_ else '<div class="variations">'
output = [start_tag]
# TODO: This is very duplicate to pretixcontrol.views.item.ItemVariations.get_forms()
# Find a common abstraction to avoid the repetition.
if dimension == 0:
output.append(format_html('<em>{0}</em>', _("not applicable")))
elif dimension == 1:
output = self.render_1d(output, variations, properties)
else:
output = self.render_nd(output, variations, properties)
output.append(
('<div class="help-block"><a href="#" class="variations-select-all">{0}</a> · '
'<a href="#" class="variations-select-none">{1}</a></div></div>').format(
_("Select all"),
_("Deselect all")
)
)
return mark_safe('\n'.join(output))
def render_1d(self, output, variations, properties):
output.append('<ul>')
for i, variation in enumerate(variations):
final_attrs = dict(
self.attrs.copy(), type=self.choice_input_class.input_type,
name=self.name, value=variation['key']
)
if variation['key'] in self.value:
final_attrs['checked'] = 'checked'
w = self.choice_input_class(
self.name, self.value, self.attrs.copy(),
(variation['key'], variation[properties[0].identity].value),
i
)
output.append(format_html('<li>{0}</li>', force_text(w)))
output.append('</ul>')
return output
def render_nd(self, output, variations, properties):
# prop1 is the property on all the grid's y-axes
prop1 = properties[0]
prop1v = list(prop1.values.current.all())
# prop2 is the property on all the grid's x-axes
prop2 = properties[1]
prop2v = list(prop2.values.current.all())
# We now iterate over the cartesian product of all the other
# properties which are NOT on the axes of the grid because we
# create one grid for any combination of them.
for gridrow in product(*[prop.values.current.all() for prop in properties[2:]]):
if len(gridrow) > 0:
output.append('<strong>')
output.append(", ".join([value.value for value in gridrow]))
output.append('</strong>')
output.append('<table class="table"><thead><tr><th></th>')
for val2 in prop2v:
output.append(format_html('<th>{0}</th>', val2.value))
output.append('</thead><tbody>')
for val1 in prop1v:
output.append(format_html('<tr><th>{0}</th>', val1.value))
# We are now inside one of the rows of the grid and have to
# select the variations to display in this row. In order to
# achieve this, we use the 'selector' lambda defined above.
# It gives us a normalized, comparable version of a set of
# PropertyValue objects. In this case, we compute the
# selector of our row as the selector of the sum of the
# values defining our grind and the value defining our row.
selection = selector(gridrow + (val1,), prop2)
# We now iterate over all variations who generate the same
# selector as 'selection'.
filtered = [v for v in variations if selector(v.relevant_values(), prop2) == selection]
for variation in sorted(filtered, key=partial(sort, prop=prop2)):
final_attrs = dict(
self.attrs.copy(), type=self.choice_input_class.input_type,
name=self.name, value=variation['key']
)
if variation['key'] in self.value:
final_attrs['checked'] = 'checked'
output.append(format_html('<td><label><input{0} /></label></td>', flatatt(final_attrs)))
output.append('</td>')
output.append('</tbody></table>')
return output
class VariationsCheckboxRenderer(VariationsFieldRenderer):
"""
This is the same as VariationsFieldRenderer but with the choice input class
forced to checkboxes
"""
choice_input_class = forms.widgets.CheckboxChoiceInput
class VariationsSelectMultiple(forms.CheckboxSelectMultiple):
"""
This is the default widget for a VariationsField
"""
renderer = VariationsCheckboxRenderer
_empty_value = []
class VariationsField(forms.ModelMultipleChoiceField):
"""
This form field is intended to be used to let the user select a
variation of a certain item, for example in a restriction plugin.
As this field expects the non-standard keyword parameter ``item``
at initialization time, this is field is normally named ``variations``
and lives inside a ``pretixcontrol.views.forms.RestrictionForm``, which
does some magic to provide this parameter.
"""
def __init__(self, *args, item=None, **kwargs):
self.item = item
if 'widget' not in args or kwargs['widget'] is None:
kwargs['widget'] = VariationsSelectMultiple
super().__init__(*args, **kwargs)
def set_item(self, item: Item):
assert isinstance(item, Item)
self.item = item
self._set_choices(self._get_choices())
def _get_choices(self) -> "list[(str, VariationDict)]":
"""
We can't use a normal QuerySet as there theoretically might be
two types of variations: Some who already have a ItemVariation
object associated with them and some who don't. We therefore use
the item's ``get_all_variations`` method. In the first case, we
use the ItemVariation objects primary key as our choice, key,
in the latter case we use a string constructed from the values
(see VariationDict.key() for implementation details).
"""
if self.item is None:
return ()
variations = self.item.get_all_variations(use_cache=True)
return (
(
v['variation'].identity if 'variation' in v else v.key(),
v
) for v in variations
)
def clean(self, value: "list[int]"):
"""
At cleaning time, we have to clean up the mess we produced with our
_get_choices implementation. In the case of ItemVariation object ids
we don't to anything to them, but if one of the selected items is a
list of PropertyValue objects (see _get_choices), we need to create
a new ItemVariation object for this combination and then add this to
our list of selected items.
"""
if self.item is None:
raise ValueError(
"VariationsField object was not properly initialized. Please"
"use a pretixcontrol.views.forms.RestrictionForm form instead of"
"a plain Django ModelForm"
)
# Standard validation foo
if self.required and not value:
raise ValidationError(self.error_messages['required'], code='required')
elif not self.required and not value:
return []
if not isinstance(value, (list, tuple)):
raise ValidationError(self.error_messages['list'], code='list')
# Build up a cache of variations having an ItemVariation object
# For implementation details, see ItemVariation.get_all_variations()
# which uses a very similar method
all_variations = self.item.variations.all().prefetch_related("values")
variations_cache = {
var.to_variation_dict().identify(): var.identity for var in all_variations
}
cleaned_value = []
# Wrap this in a transaction to prevent strange database state if we
# get a ValidationError half-way through
with transaction.atomic():
for pk in value:
if ":" in pk:
# A combination of PropertyValues was given
# Hash the combination in the same way as in our cache above
key = ",".join([pair.split(":")[1] for pair in sorted(pk.split(","))])
if key in variations_cache:
# An ItemVariation object already exists for this variation,
# so use this. (This might occur if the variation object was
# created _after_ the user loaded the form but _before_ he
# submitted it.)
cleaned_value.append(str(variations_cache[key]))
continue
# No ItemVariation present, create one!
var = ItemVariation()
var.item_id = self.item.identity
var.save()
# Add the values to the ItemVariation object
try:
var.add_values_from_string(pk)
except:
raise ValidationError(
self.error_messages['invalid_pk_value'],
code='invalid_pk_value',
params={'pk': value},
)
variations_cache[key] = var.identity
cleaned_value.append(str(var.identity))
else:
# An ItemVariation id was given
cleaned_value.append(pk)
qs = self.item.variations.current.filter(identity__in=cleaned_value)
# Re-check for consistency
pks = set(force_text(getattr(o, "identity")) for o in qs)
for val in cleaned_value:
if force_text(val) not in pks:
raise ValidationError(
self.error_messages['invalid_choice'],
code='invalid_choice',
params={'value': val},
)
# Since this overrides the inherited ModelChoiceField.clean
# we run custom validators here
self.run_validators(cleaned_value)
return qs
choices = property(_get_choices, forms.ChoiceField._set_choices)

View File

@@ -1,9 +1,8 @@
from itertools import product
from django.contrib import messages
from django.db import transaction
from django.forms import BooleanField
from django.utils.functional import cached_property
from django.views.generic import ListView
from django.views.generic.edit import DeleteView
from django.views.generic.base import TemplateView
@@ -13,13 +12,13 @@ from django.http import HttpResponseRedirect, HttpResponseForbidden, Http404
from django.shortcuts import redirect
from django.forms.models import inlineformset_factory
from django.utils.translation import ugettext_lazy as _
from pretix.base.forms import VersionedModelForm, I18nModelForm
from pretix.base.models import (
Item, ItemCategory, Property, ItemVariation, PropertyValue, Question, Quota,
Versionable)
Item, ItemCategory, Property, ItemVariation, PropertyValue, Question, Quota)
from pretix.control.forms.item import ItemVariationForm, QuotaForm, QuestionForm, PropertyForm, PropertyValueForm, \
CategoryForm
from pretix.control.forms.item import ItemFormGeneral
from pretix.control.permissions import EventPermissionRequiredMixin, event_permission_required
from pretix.control.views.forms import TolerantFormsetModelForm, VariationsField, I18nInlineFormSet
from pretix.control.forms import VariationsField, I18nInlineFormSet
from pretix.control.signals import restriction_formset
from . import UpdateView, CreateView
@@ -81,16 +80,6 @@ def item_move_down(request, organizer, event, item):
event=request.event.slug)
class CategoryForm(VersionedModelForm):
class Meta:
model = ItemCategory
localized_fields = '__all__'
fields = [
'name'
]
class CategoryDelete(EventPermissionRequiredMixin, DeleteView):
model = ItemCategory
form_class = CategoryForm
@@ -235,24 +224,6 @@ class PropertyList(ListView):
)
class PropertyForm(VersionedModelForm):
class Meta:
model = Property
localized_fields = '__all__'
fields = [
'name',
]
class PropertyValueForm(TolerantFormsetModelForm):
class Meta:
model = PropertyValue
localized_fields = '__all__'
fields = [
'value',
]
class PropertyUpdate(EventPermissionRequiredMixin, UpdateView):
model = Property
form_class = PropertyForm
@@ -422,18 +393,6 @@ class QuestionList(ListView):
return self.request.event.questions.current.all()
class QuestionForm(VersionedModelForm):
class Meta:
model = Question
localized_fields = '__all__'
fields = [
'question',
'type',
'required',
]
class QuestionDelete(EventPermissionRequiredMixin, DeleteView):
model = Question
template_name = 'pretixcontrol/items/question_delete.html'
@@ -524,52 +483,7 @@ class QuotaList(ListView):
).prefetch_related("items")
class QuotaForm(I18nModelForm):
def __init__(self, **kwargs):
items = kwargs['items']
del kwargs['items']
super().__init__(**kwargs)
if hasattr(self, 'instance'):
active_items = set(self.instance.items.all())
active_variations = set(self.instance.variations.all())
else:
active_items = set()
active_variations = set()
for item in items:
if len(item.properties.all()) > 0:
self.fields['item_%s' % item.identity] = VariationsField(
item, label=_("Activate for"),
required=False,
initial=active_variations
)
self.fields['item_%s' % item.identity].set_item(item)
else:
self.fields['item_%s' % item.identity] = BooleanField(
label=_("Activate"),
required=False,
initial=(item in active_items)
)
def save(self, commit=True):
if self.instance.pk is not None and isinstance(self.instance, Versionable):
if self.has_changed():
self.instance = self.instance.clone_shallow()
return super().save(commit)
class Meta:
model = Quota
localized_fields = '__all__'
fields = [
'name',
'size',
]
class QuotaEditorMixin:
@cached_property
def items(self) -> "List[Item]":
return list(self.request.event.items.all().prefetch_related("properties", "variations"))
@@ -606,8 +520,8 @@ class QuotaEditorMixin:
selected_variations.append(v)
if data: # and item not in items:
self.object.items.add(item)
# elif not data and item in items:
# self.object.items.remove(item)
# elif not data and item in items:
# self.object.items.remove(item)
self.object.variations.add(*[v for v in selected_variations]) # if v not in variations])
# self.object.variations.remove(*[v for v in variations if v not in selected_variations])
@@ -708,31 +622,6 @@ class ItemDetailMixin(SingleObjectMixin):
raise Http404(_("The requested item does not exist."))
class ItemFormGeneral(VersionedModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['category'].queryset = self.instance.event.categories.current.all()
self.fields['properties'].queryset = self.instance.event.properties.current.all()
self.fields['questions'].queryset = self.instance.event.questions.current.all()
class Meta:
model = Item
localized_fields = '__all__'
fields = [
'category',
'name',
'active',
'admission',
'short_description',
'long_description',
'default_price',
'tax_rate',
'properties',
'questions',
]
class ItemCreate(EventPermissionRequiredMixin, CreateView):
form_class = ItemFormGeneral
template_name = 'pretixcontrol/item/index.html'
@@ -776,19 +665,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
return super().form_valid(form)
class ItemVariationForm(VersionedModelForm):
class Meta:
model = ItemVariation
localized_fields = '__all__'
fields = [
'active',
'default_price',
]
class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_items'
def __init__(self, *args, **kwargs):
@@ -952,7 +829,6 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
class ItemRestrictions(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_items'
template_name = 'pretixcontrol/item/restrictions.html'

View File

@@ -1,12 +1,11 @@
from django import forms
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.shortcuts import render
from django.views.generic import ListView, CreateView, TemplateView
from django.utils.translation import ugettext_lazy as _
from pretix.base.forms import VersionedModelForm
from pretix.base.models import Event, EventPermission, Organizer, OrganizerPermission
from pretix.base.models import Event, EventPermission, OrganizerPermission
from pretix.control.forms.event import EventCreateForm
from pretix.control.permissions import OrganizerPermissionRequiredMixin
@@ -28,37 +27,6 @@ def index(request):
return render(request, 'pretixcontrol/base.html', {})
class EventForm(VersionedModelForm):
error_messages = {
'duplicate_slug': _("You already used this slug for a different event. Please choose a new one."),
}
class Meta:
model = Event
fields = [
'name',
'slug',
'currency',
'date_from',
'date_to',
'presale_start',
'presale_end'
]
def __init__(self, *args, **kwargs):
self.organizer = kwargs.pop('organizer')
super().__init__(*args, **kwargs)
def clean_slug(self):
slug = self.cleaned_data['slug']
if Event.objects.filter(slug=slug, organizer=self.organizer).exists():
raise forms.ValidationError(
self.error_messages['duplicate_slug'],
code='duplicate_slug',
)
return slug
class EventCreateStart(TemplateView):
template_name = 'pretixcontrol/events/start.html'
@@ -74,7 +42,7 @@ class EventCreateStart(TemplateView):
class EventCreate(OrganizerPermissionRequiredMixin, CreateView):
model = Event
form_class = EventForm
form_class = EventCreateForm
template_name = 'pretixcontrol/events/create.html'
context_object_name = 'event'
permission = 'can_create_events'

View File

@@ -1,6 +1,6 @@
from itertools import groupby
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.db.models import Count
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
@@ -8,10 +8,9 @@ from django.http import HttpResponse
from django.shortcuts import redirect, render
from django.utils.functional import cached_property
from django.views.generic import ListView, DetailView, TemplateView
from pretix.base.forms import VersionedModelForm
from pretix.base.models import Order, Quota, OrderPosition
from pretix.base.signals import register_payment_providers
from pretix.control.forms.orders import ExtendForm
from pretix.control.permissions import EventPermissionRequiredMixin
@@ -51,12 +50,6 @@ class OrderView(EventPermissionRequiredMixin, DetailView):
return provider
class ExtendForm(VersionedModelForm):
class Meta:
model = Order
fields = ['expires']
class OrderDetail(OrderView):
template_name = 'pretixcontrol/order/index.html'
permission = 'can_view_orders'
@@ -84,8 +77,8 @@ class OrderDetail(OrderView):
# We do this by list manipulations instead of a GROUP BY query, as
# Django is unable to join related models in a .values() query
def keyfunc(pos):
if ((pos.item.admission and self.request.event.settings.attendee_names_asked)
or pos.item.questions.all()):
if (pos.item.admission and self.request.event.settings.attendee_names_asked) \
or pos.item.questions.all():
return pos.id, "", "", ""
return "", pos.item_id, pos.variation_id, pos.price
@@ -212,33 +205,38 @@ class OverView(EventPermissionRequiredMixin, TemplateView):
num_total = {
(p['item'], p['variation']): p['cnt']
for p in OrderPosition.objects.current.filter(
order__event=self.request.event
).values('item', 'variation').annotate(cnt=Count('id'))
for p in
OrderPosition.objects.current.filter(order__event=self.request.event).values('item', 'variation').annotate(
cnt=Count('id'))
}
num_cancelled = {
(p['item'], p['variation']): p['cnt']
for p in OrderPosition.objects.current.filter(
order__event=self.request.event, order__status=Order.STATUS_CANCELLED
).values('item', 'variation').annotate(cnt=Count('id'))
for p in (OrderPosition.objects.current
.filter(order__event=self.request.event, order__status=Order.STATUS_CANCELLED)
.values('item', 'variation')
.annotate(cnt=Count('id')))
}
num_refunded = {
(p['item'], p['variation']): p['cnt']
for p in OrderPosition.objects.current.filter(
order__event=self.request.event, order__status=Order.STATUS_REFUNDED
).values('item', 'variation').annotate(cnt=Count('id'))
for p in (OrderPosition.objects.current
.filter(order__event=self.request.event, order__status=Order.STATUS_REFUNDED)
.values('item', 'variation')
.annotate(cnt=Count('id')))
}
num_pending = {
(p['item'], p['variation']): p['cnt']
for p in OrderPosition.objects.current.filter(
order__event=self.request.event, order__status__in=(Order.STATUS_PENDING, Order.STATUS_EXPIRED)
).values('item', 'variation').annotate(cnt=Count('id'))
for p in (OrderPosition.objects.current
.filter(order__event=self.request.event,
order__status__in=(Order.STATUS_PENDING, Order.STATUS_EXPIRED))
.values('item', 'variation')
.annotate(cnt=Count('id')))
}
num_paid = {
(p['item'], p['variation']): p['cnt']
for p in OrderPosition.objects.current.filter(
order__event=self.request.event, order__status=Order.STATUS_PAID
).values('item', 'variation').annotate(cnt=Count('id'))
for p in (OrderPosition.objects.current
.filter(order__event=self.request.event, order__status=Order.STATUS_PAID)
.values('item', 'variation')
.annotate(cnt=Count('id')))
}
for item in items:
@@ -260,12 +258,16 @@ class OverView(EventPermissionRequiredMixin, TemplateView):
item.num_paid = sum(var.num_paid for var in item.all_variations)
# Regroup those by category
ctx['items_by_category'] = sorted([
# a group is a tuple of a category and a list of items
(cat, [i for i in items if i.category == cat])
for cat in set([i.category for i in items]) # insert categories into a set for uniqueness
# a set is unsorted, so sort again by category
], key=lambda group: (group[0].position, group[0].identity) if group[0] is not None else (0, ""))
ctx['items_by_category'] = sorted(
[
# a group is a tuple of a category and a list of items
(cat, [i for i in items if i.category == cat])
for cat in set([i.category for i in items])
# insert categories into a set for uniqueness
# a set is unsorted, so sort again by category
],
key=lambda group: (group[0].position, group[0].identity) if group[0] is not None else (0, "")
)
for c in ctx['items_by_category']:
c[0].num_total = sum(item.num_total for item in c[1])
c[0].num_pending = sum(item.num_pending for item in c[1])

View File

@@ -1,12 +1,11 @@
from django import forms
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.http import HttpResponseForbidden
from django.utils.translation import ugettext_lazy as _
from django.views.generic import ListView, UpdateView, CreateView
from pretix.base.forms import VersionedModelForm
from pretix.base.models import Organizer, OrganizerPermission
from pretix.control.forms.organizer import OrganizerUpdateForm, OrganizerForm
from pretix.control.permissions import OrganizerPermissionRequiredMixin
@@ -25,35 +24,6 @@ class OrganizerList(ListView):
)
class OrganizerForm(VersionedModelForm):
error_messages = {
'duplicate_slug': _("This slug is already in use. Please choose a different one."),
}
class Meta:
model = Organizer
fields = ['name', 'slug']
def clean_slug(self):
slug = self.cleaned_data['slug']
if Organizer.objects.filter(slug=slug).exists():
raise forms.ValidationError(
self.error_messages['duplicate_slug'],
code='duplicate_slug',
)
return slug
class OrganizerUpdateForm(OrganizerForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['slug'].widget.attrs['disabled'] = 'disabled'
def clean_slug(self):
return self.instance.slug
class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
model = Organizer
form_class = OrganizerUpdateForm

View File

@@ -1,100 +1,13 @@
from django import forms
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.hashers import check_password
from django.core.urlresolvers import reverse
from django.db.models import Q
from django.views.generic import UpdateView
from django.utils.translation import ugettext_lazy as _
from pytz import common_timezones
from pretix.control.forms.user import UserSettingsForm
from pretix.base.models import User
class UserSettingsForm(forms.ModelForm):
error_messages = {
'duplicate_identifier': _("There already is an account associated with this e-mail address. "
"Please choose a different one."),
'pw_current': _("Please enter your current password if you want to change your e-mail "
"address or password."),
'pw_current_wrong': _("The current password you entered was not correct."),
'pw_mismatch': _("Please enter the same password twice"),
}
old_pw = forms.CharField(max_length=255,
required=False,
label=_("Your current password"),
widget=forms.PasswordInput())
new_pw = forms.CharField(max_length=255,
required=False,
label=_("New password"),
widget=forms.PasswordInput())
new_pw_repeat = forms.CharField(max_length=255,
required=False,
label=_("Repeat new password"),
widget=forms.PasswordInput())
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Default timezone"),
)
class Meta:
model = User
fields = [
'givenname', 'familyname', 'locale', 'timezone', 'email'
]
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
def clean_old_pw(self):
old_pw = self.cleaned_data.get('old_pw')
if old_pw and not check_password(old_pw, self.user.password):
raise forms.ValidationError(
self.error_messages['pw_current_wrong'],
code='pw_current_wrong',
)
return old_pw
def clean_email(self):
email = self.cleaned_data['email']
if User.objects.filter(
Q(identifier=email)
& ~Q(pk=self.instance.pk)
).exists():
raise forms.ValidationError(
self.error_messages['duplicate_identifier'],
code='duplicate_identifier',
)
return email
def clean(self):
password1 = self.cleaned_data.get('new_pw')
password2 = self.cleaned_data.get('new_pw_repeat')
old_pw = self.cleaned_data.get('old_pw')
email = self.cleaned_data.get('email')
if (password1 or email != self.user.email) and not old_pw:
raise forms.ValidationError(
self.error_messages['pw_current'],
code='pw_current',
)
if password1 and password1 != password2:
raise forms.ValidationError(
self.error_messages['pw_mismatch'],
code='pw_mismatch',
)
if password1:
self.instance.set_password(password1)
self.instance.identifier = email
return self.cleaned_data
class UserSettings(UpdateView):
model = User
form_class = UserSettingsForm