Restructure our python module. A lot.

This commit is contained in:
Raphael Michel
2015-02-14 17:55:13 +01:00
parent cf18f3e200
commit 077413f41c
117 changed files with 193 additions and 163 deletions

View File

View File

@@ -0,0 +1,75 @@
from django.shortcuts import render, redirect
from django.contrib.auth.forms import AuthenticationForm as BaseAuthenticationForm
from django import forms
from django.utils.translation import ugettext as _
from django.contrib.auth import authenticate
from django.contrib.auth import login as auth_login
from django.contrib.auth import logout as auth_logout
class AuthenticationForm(BaseAuthenticationForm):
"""
The login form, providing an email and password field. The form already implements
validation for correct user data.
"""
email = forms.EmailField(label=_("E-mail address"), max_length=254)
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
username = None
error_messages = {
'invalid_login': _("Please enter a correct e-mail address and password."),
'inactive': _("This account is inactive."),
}
def __init__(self, request=None, *args, **kwargs):
self.request = request
self.user_cache = None
super(forms.Form, self).__init__(*args, **kwargs)
def clean(self):
email = self.cleaned_data.get('email')
password = self.cleaned_data.get('password')
if email and password:
self.user_cache = authenticate(identifier=email.lower(),
password=password)
if self.user_cache is None:
raise forms.ValidationError(
self.error_messages['invalid_login'],
code='invalid_login',
)
else:
self.confirm_login_allowed(self.user_cache)
return self.cleaned_data
def login(request):
"""
Render and process a most basic login form. Takes an URL as GET
parameter "next" for redirection after successful login
"""
ctx = {}
if request.user.is_authenticated():
if "next" in request.GET:
return redirect(request.GET.get("next", 'control:index'))
return redirect('control:index')
if request.method == 'POST':
form = AuthenticationForm(data=request.POST)
if form.is_valid() and form.user_cache:
auth_login(request, form.user_cache)
if "next" in request.GET:
return redirect(request.GET.get("next", 'control:index'))
return redirect('control:index')
else:
form = AuthenticationForm()
ctx['form'] = form
return render(request, 'pretixcontrol/auth/login.html', ctx)
def logout(request):
"""
Log the user out of the current session, then redirect to login page.
"""
auth_logout(request)
return redirect('control:auth.login')

View File

@@ -0,0 +1,111 @@
from django.shortcuts import render, redirect
from django.views.generic.edit import UpdateView
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
from pretix.base.models import Event
from pretix.control.permissions import EventPermissionRequiredMixin
class EventUpdateForm(VersionedModelForm):
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Default timezone"),
)
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',
'locale',
'timezone',
'currency',
'date_from',
'date_to',
'show_date_to',
'show_times',
'presale_start',
'presale_end',
'payment_term_days',
'payment_term_last',
'max_items_per_order'
]
class EventUpdate(EventPermissionRequiredMixin, UpdateView):
model = Event
form_class = EventUpdateForm
template_name = 'pretixcontrol/event/settings.html'
permission = 'can_change_settings'
def get_object(self, queryset=None) -> Event:
return self.request.event
def get_success_url(self) -> str:
return reverse('control:event.settings', kwargs={
'organizer': self.get_object().organizer.slug,
'event': self.get_object().slug,
}) + '?success=true'
class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
model = Event
context_object_name = 'event'
permission = 'can_change_settings'
template_name = 'pretixcontrol/event/plugins.html'
def get_object(self, queryset=None) -> Event:
return self.request.event
def get_context_data(self, *args, **kwargs) -> dict:
from pretix.base.plugins import get_all_plugins
context = super().get_context_data(*args, **kwargs)
context['plugins'] = [p for p in get_all_plugins() if not p.name.startswith('.')]
context['plugins_active'] = self.object.get_plugins()
return context
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
plugins_active = self.object.get_plugins()
for key, value in request.POST.items():
if key.startswith("plugin:"):
module = key.split(":")[1]
if value == "enable":
plugins_active.append(module)
else:
plugins_active.remove(module)
self.object.plugins = ",".join(plugins_active)
self.object.save()
return redirect(self.get_success_url())
def get_success_url(self) -> str:
return reverse('control:event.settings.plugins', kwargs={
'organizer': self.get_object().organizer.slug,
'event': self.get_object().slug,
}) + '?success=true'
def index(request, organizer, event):
return render(request, 'pretixcontrol/event/index.html', {})

View File

@@ -0,0 +1,389 @@
from itertools import product
from django import forms
from django.core.exceptions import ValidationError
from django.db import transaction
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, PropertyValue, Item
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']
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.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>')
elif dimension >= 2:
# 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())
def selector(values):
# 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 != prop2.identity
]
def sort(v):
# Given a list of variations, this will sort them by their position
# on the x-axis
return v[prop2.identity].identity
# 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,))
# We now iterate over all variations who generate the same
# selector as 'selection'.
filtered = [v for v in variations if selector(v.relevant_values()) == selection]
for variation in sorted(filtered, key=sort):
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>')
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))
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 = {}
for var in all_variations:
key = []
for v in var.values.all():
key.append((v.prop_id, v.identity))
key = tuple(sorted(key))
variations_cache[key] = var.identity
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 = []
for pair in pk.split(","):
key.append(tuple([i for i in pair.split(":")]))
key = tuple(sorted(key))
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
for pair in pk.split(","):
prop, value = pair.split(":")
try:
var.values.add(
PropertyValue.objects.current.get(
identity=value,
prop_id=prop
)
)
except PropertyValue.DoesNotExist:
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

@@ -0,0 +1,899 @@
from itertools import product
from django.db import transaction
from django.forms import BooleanField, ModelForm
from django.utils.functional import cached_property
from django.views.generic import ListView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.views.generic.base import TemplateView
from django.views.generic.detail import SingleObjectMixin
from django.core.urlresolvers import resolve, reverse
from django.http import HttpResponseRedirect, HttpResponseForbidden
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
from pretix.base.models import (
Item, ItemCategory, Property, ItemVariation, PropertyValue, Question, Quota,
Versionable)
from pretix.control.permissions import EventPermissionRequiredMixin, event_permission_required
from pretix.control.views.forms import TolerantFormsetModelForm, VariationsField
from pretix.control.signals import restriction_formset
class ItemList(ListView):
model = Item
context_object_name = 'items'
template_name = 'pretixcontrol/items/index.html'
def get_queryset(self):
return Item.objects.current.filter(
event=self.request.event
).prefetch_related("category")
class CategoryForm(VersionedModelForm):
class Meta:
model = ItemCategory
localized_fields = '__all__'
fields = [
'name'
]
class CategoryDelete(EventPermissionRequiredMixin, DeleteView):
model = ItemCategory
form_class = CategoryForm
template_name = 'pretixcontrol/items/category_delete.html'
permission = 'can_change_items'
context_object_name = 'category'
def get_object(self, queryset=None) -> ItemCategory:
url = resolve(self.request.path_info)
return self.request.event.categories.current.get(
identity=url.kwargs['category']
)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
for item in self.object.items.current.all():
# TODO: Clone!?
item.category = None
item.save()
success_url = self.get_success_url()
self.object.delete()
return HttpResponseRedirect(success_url)
def get_success_url(self) -> str:
return reverse('control:event.items.categories', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?deleted=true'
class CategoryUpdate(EventPermissionRequiredMixin, UpdateView):
model = ItemCategory
form_class = CategoryForm
template_name = 'pretixcontrol/items/category.html'
permission = 'can_change_items'
context_object_name = 'category'
def get_object(self, queryset=None) -> ItemCategory:
url = resolve(self.request.path_info)
return self.request.event.categories.current.get(
identity=url.kwargs['category']
)
def get_success_url(self) -> str:
return reverse('control:event.items.categories', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?updated=true'
class CategoryCreate(EventPermissionRequiredMixin, CreateView):
model = ItemCategory
form_class = CategoryForm
template_name = 'pretixcontrol/items/category.html'
permission = 'can_change_items'
context_object_name = 'category'
def get_success_url(self) -> str:
return reverse('control:event.items.categories', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?created=true'
def form_valid(self, form):
form.instance.event = self.request.event
return super().form_valid(form)
class CategoryList(ListView):
model = ItemCategory
context_object_name = 'categories'
template_name = 'pretixcontrol/items/categories.html'
def get_queryset(self):
return self.request.event.categories.current.all()
def category_move(request, category, up=True):
"""
This is a helper function to avoid duplicating code in category_move_up and
category_move_down. It takes a category and a direction and then tries to bring
all categories for this event in a new order.
"""
category = request.event.categories.current.get(
identity=category
)
categories = list(request.event.categories.current.order_by("position"))
index = categories.index(category)
if index != 0 and up:
categories[index - 1], categories[index] = categories[index], categories[index - 1]
elif index != len(categories) - 1 and not up:
categories[index + 1], categories[index] = categories[index], categories[index + 1]
for i, cat in enumerate(categories):
if cat.position != i:
cat.position = i
cat.save() # TODO: Clone or document sloppiness?
@event_permission_required("can_change_items")
def category_move_up(request, organizer, event, category):
category_move(request, category, up=True)
return redirect(reverse('control:event.items.categories', kwargs={
'organizer': request.event.organizer.slug,
'event': request.event.slug,
}) + '?ordered=true')
@event_permission_required("can_change_items")
def category_move_down(request, organizer, event, category):
category_move(request, category, up=False)
return redirect(reverse('control:event.items.categories', kwargs={
'organizer': request.event.organizer.slug,
'event': request.event.slug,
}) + '?ordered=true')
class PropertyList(ListView):
model = Property
context_object_name = 'properties'
template_name = 'pretixcontrol/items/properties.html'
def get_queryset(self):
return Property.objects.current.filter(
event=self.request.event
)
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
template_name = 'pretixcontrol/items/property.html'
permission = 'can_change_items'
context_object_name = 'property'
def get_object(self, queryset=None) -> Property:
url = resolve(self.request.path_info)
return self.request.event.properties.current.get(
identity=url.kwargs['property']
)
def get_success_url(self) -> str:
url = resolve(self.request.path_info)
return reverse('control:event.items.properties.edit', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
'property': url.kwargs['property']
}) + '?success=true'
def get_formset(self):
formsetclass = inlineformset_factory(
Property, PropertyValue,
form=PropertyValueForm,
can_order=True,
extra=0,
)
kwargs = self.get_form_kwargs()
kwargs['queryset'] = self.object.values.current.all()
formset = formsetclass(**kwargs)
return formset
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['formset'] = self.get_formset()
return context
@transaction.atomic()
def form_valid(self, form, formset):
for f in formset.deleted_forms:
f.instance.delete()
f.instance.pk = None
for i, f in enumerate(formset.ordered_forms):
if f.instance.pk is not None:
f.instance = f.instance.clone()
f.instance.position = i
f.instance.save()
return super().form_valid(form)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = self.get_formset()
if form.is_valid() and formset.is_valid():
return self.form_valid(form, formset)
else:
return self.form_invalid(form)
class PropertyCreate(EventPermissionRequiredMixin, CreateView):
model = Property
form_class = PropertyForm
template_name = 'pretixcontrol/items/property.html'
permission = 'can_change_items'
context_object_name = 'property'
def get_success_url(self) -> str:
return reverse('control:event.items.properties', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?created=true'
def get_formset(self):
formsetclass = inlineformset_factory(
Property, PropertyValue,
form=PropertyValueForm,
can_order=True,
extra=3,
)
formset = formsetclass(**self.get_form_kwargs())
return formset
def get_context_data(self, *args, **kwargs) -> dict:
self.object = None
context = super().get_context_data(*args, **kwargs)
context['formset'] = self.get_formset()
return context
@transaction.atomic()
def form_valid(self, form, formset):
form.instance.event = self.request.event
resp = super().form_valid(form)
for i, f in enumerate(formset.ordered_forms):
f.instance.position = i
f.instance.prop = form.instance
f.instance.save()
return resp
def post(self, request, *args, **kwargs):
form_class = self.get_form_class()
form = self.get_form(form_class)
formset = self.get_formset()
if form.is_valid() and formset.is_valid():
return self.form_valid(form, formset)
else:
return self.form_invalid(form)
class PropertyDelete(EventPermissionRequiredMixin, DeleteView):
model = Property
form_class = PropertyForm
template_name = 'pretixcontrol/items/property_delete.html'
permission = 'can_change_items'
context_object_name = 'property'
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['dependent'] = self.get_object().items.current.all()
context['possible'] = self.is_allowed()
return context
def is_allowed(self) -> bool:
return self.get_object().items.current.count() == 0
def get_object(self, queryset=None) -> Property:
if not hasattr(self, 'object') or not self.object:
url = resolve(self.request.path_info)
self.object = self.request.event.properties.current.get(
identity=url.kwargs['property']
)
return self.object
def delete(self, request, *args, **kwargs):
if self.is_allowed():
success_url = self.get_success_url()
self.get_object().delete()
return HttpResponseRedirect(success_url)
else:
return HttpResponseForbidden()
def get_success_url(self) -> str:
return reverse('control:event.items.properties', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?deleted=true'
class QuestionList(ListView):
model = Question
context_object_name = 'questions'
template_name = 'pretixcontrol/items/questions.html'
def get_queryset(self):
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'
permission = 'can_change_items'
context_object_name = 'question'
def get_object(self, queryset=None) -> Question:
url = resolve(self.request.path_info)
return self.request.event.questions.current.get(
identity=url.kwargs['question']
)
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['dependent'] = list(self.get_object().items.current.all())
return context
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
success_url = self.get_success_url()
self.object.delete()
return HttpResponseRedirect(success_url)
def get_success_url(self) -> str:
return reverse('control:event.items.questions', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?deleted=true'
class QuestionUpdate(EventPermissionRequiredMixin, UpdateView):
model = Question
form_class = QuestionForm
template_name = 'pretixcontrol/items/question.html'
permission = 'can_change_items'
context_object_name = 'question'
def get_object(self, queryset=None) -> Question:
url = resolve(self.request.path_info)
return self.request.event.questions.current.get(
identity=url.kwargs['question']
)
def get_success_url(self) -> str:
return reverse('control:event.items.questions', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?updated=true'
class QuestionCreate(EventPermissionRequiredMixin, CreateView):
model = Question
form_class = QuestionForm
template_name = 'pretixcontrol/items/question.html'
permission = 'can_change_items'
context_object_name = 'question'
def get_success_url(self) -> str:
return reverse('control:event.items.questions', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?created=true'
def form_valid(self, form):
form.instance.event = self.request.event
return super().form_valid(form)
class QuotaList(ListView):
model = Quota
context_object_name = 'quotas'
template_name = 'pretixcontrol/items/quotas.html'
def get_queryset(self):
return Quota.objects.current.filter(
event=self.request.event
).prefetch_related("items")
class QuotaForm(ModelForm):
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()
# TODO: order_cache, lock_cache are emptied by that but you'll have
# to rebuild them anyway
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"))
def get_form(self, form_class):
if not hasattr(self, '_form'):
kwargs = self.get_form_kwargs()
kwargs['items'] = self.items
self._form = form_class(**kwargs)
return self._form
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['items'] = self.items
for item in context['items']:
item.field = self.get_form(QuotaForm)['item_%s' % item.identity]
return context
@transaction.atomic()
def form_valid(self, form):
res = super().form_valid(form)
# The following commented-out checks are not necessary as both self.object.items
# and self.object.variations can be expected empty due to the performance
# optimization of pretixbase.models.Versionable.clone_shallow()
# items = self.object.items.all()
# variations = self.object.variations.all()
self.object = form.instance
for item in self.items:
field = form.fields['item_%s' % item.identity]
data = form.cleaned_data['item_%s' % item.identity]
if isinstance(field, VariationsField):
self.object.variations.add(*data)
# for v in data:
# if v not in variations:
# self.object.variations.add(v)
# for v in variations:
# if v not in data:
# self.object.variations.remove(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)
return res
class QuotaCreate(EventPermissionRequiredMixin, QuotaEditorMixin, CreateView):
model = Quota
form_class = QuotaForm
template_name = 'pretixcontrol/items/quota.html'
permission = 'can_change_items'
context_object_name = 'quota'
def get_success_url(self) -> str:
return reverse('control:event.items.quotas', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?created=true'
def form_valid(self, form):
form.instance.event = self.request.event
return super().form_valid(form)
class QuotaUpdate(EventPermissionRequiredMixin, QuotaEditorMixin, UpdateView):
model = Quota
form_class = QuotaForm
template_name = 'pretixcontrol/items/quota.html'
permission = 'can_change_items'
context_object_name = 'quota'
def get_object(self, queryset=None) -> Quota:
url = resolve(self.request.path_info)
return self.request.event.quotas.current.get(
identity=url.kwargs['quota']
)
def get_success_url(self) -> str:
return reverse('control:event.items.quotas', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?updated=true'
class QuotaDelete(EventPermissionRequiredMixin, DeleteView):
model = Quota
template_name = 'pretixcontrol/items/quota_delete.html'
permission = 'can_change_items'
context_object_name = 'quota'
def get_object(self, queryset=None) -> Quota:
url = resolve(self.request.path_info)
return self.request.event.quotas.current.get(
identity=url.kwargs['quota']
)
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['dependent'] = list(self.get_object().items.current.all())
return context
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
success_url = self.get_success_url()
self.object.delete()
return HttpResponseRedirect(success_url)
def get_success_url(self) -> str:
return reverse('control:event.items.quotas', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}) + '?deleted=true'
class ItemDetailMixin(SingleObjectMixin):
model = Item
context_object_name = 'item'
def get_object(self, queryset=None) -> Item:
if not hasattr(self, 'object') or not self.object:
url = resolve(self.request.path_info)
self.item = self.request.event.items.current.get(
identity=url.kwargs['item']
)
self.object = self.item
return self.object
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',
'short_description',
'long_description',
'default_price',
'tax_rate',
'properties',
'questions',
]
class ItemCreate(EventPermissionRequiredMixin, CreateView):
form_class = ItemFormGeneral
template_name = 'pretixcontrol/item/index.html'
permission = 'can_change_items'
def get_success_url(self) -> str:
return reverse('control:event.item', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
'item': self.object.identity,
}) + '?success=true'
def get_form_kwargs(self):
"""
Returns the keyword arguments for instantiating the form.
"""
newinst = Item(event=self.request.event)
kwargs = super().get_form_kwargs()
kwargs.update({'instance': newinst})
return kwargs
class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateView):
form_class = ItemFormGeneral
template_name = 'pretixcontrol/item/index.html'
permission = 'can_change_items'
def get_success_url(self) -> str:
return reverse('control:event.item', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
'item': self.get_object().identity,
}) + '?success=true'
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):
super().__init__(*args, **kwargs)
self.item = None
def get_form(self, variation, data=None) -> ItemVariationForm:
"""
Return the dict for one given variation. Variations are expected to be
dictionaries in the format of Item.get_all_variations()
"""
# Values are all dictionary ite
values = [i[1] for i in sorted([it for it in variation.items() if it[0] != 'variation'])]
if 'variation' in variation:
form = ItemVariationForm(
data,
instance=variation['variation'],
prefix=",".join([str(i.identity) for i in values]),
)
else:
inst = ItemVariation(item=self.object)
inst.item_id = self.object.identity
inst.creation = True
form = ItemVariationForm(
data,
instance=inst,
prefix=",".join([str(i.identity) for i in values]),
)
form.values = values
return form
def get_forms(self) -> tuple:
"""
Returns one form per possible item variation. The forms are returned
twice: The first entry in the returned tuple contains a 1-, 2- or
3-dimensional list, depending on the number of properties associated
with this item (this is being used for form display), the second
contains all forms in one single list (this is used for processing).
The first, hierarchical list, is a list of dicts on all levels but the
last one, where the dict contains the two entries 'row' containing a
string describing this layer and 'forms' which contains the forms or
the next list of dicts.
"""
forms = []
forms_flat = []
variations = self.object.get_all_variations()
data = self.request.POST if self.request.method == 'POST' else None
if self.dimension == 1:
# For one-dimensional structures we just have a list of forms
for variation in variations:
form = self.get_form(variation, data)
forms.append(form)
forms_flat = forms
elif self.dimension >= 2:
# For 2 or more dimensional structures we display a list of grids
# of forms
# prop1 is the property on all the grid's y-axes
prop1 = self.properties[0]
# prop2 is the property on all the grid's x-axes
prop2 = self.properties[1]
def selector(values):
# 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 != prop2.identity
]
def sort(v):
# Given a list of variations, this will sort them by their position
# on the x-axis
return v[prop2.identity].identity
# 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 self.properties[2:]]):
grids = []
for val1 in prop1.values.current.all():
formrow = []
# 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,))
# We now iterate over all variations who generate the same
# selector as 'selection'.
filtered = [v for v in variations if selector(v.relevant_values()) == selection]
for variation in sorted(filtered, key=sort):
form = self.get_form(variation, data)
formrow.append(form)
forms_flat.append(form)
grids.append({'row': val1, 'forms': formrow})
forms.append({'row': ", ".join([value.value for value in gridrow]), 'forms': grids})
return forms, forms_flat
def main(self, request, *args, **kwargs):
self.object = self.get_object()
self.properties = list(self.object.properties.current.all().prefetch_related("values"))
self.dimension = len(self.properties)
self.forms, self.forms_flat = self.get_forms()
def get(self, request, *args, **kwargs):
self.main(request, *args, **kwargs)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.main(request, *args, **kwargs)
context = self.get_context_data(object=self.object)
with transaction.atomic():
for form in self.forms_flat:
if form.is_valid() and form.has_changed():
form.save()
if hasattr(form.instance, 'creation') and form.instance.creation:
# We need this special 'creation' field set to true in get_form
# for newly created items as cleanerversion does already set the
# primary key in its post_init hook
form.instance.values.add(*form.values)
# TODO: Redirect to success message
return self.render_to_response(context)
def get_template_names(self) -> "List[str]":
if self.dimension == 0:
return ['pretixcontrol/item/variations_0d.html']
elif self.dimension == 1:
return ['pretixcontrol/item/variations_1d.html']
elif self.dimension >= 2:
return ['pretixcontrol/item/variations_nd.html']
def get_context_data(self, **kwargs) -> dict:
context = super().get_context_data(**kwargs)
context['forms'] = self.forms
context['properties'] = self.properties
return context
class ItemRestrictions(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_items'
template_name = 'pretixcontrol/item/restrictions.html'
def get_formsets(self):
responses = restriction_formset.send(self.object.event, item=self.object)
formsets = []
for receiver, response in responses:
response['formset'] = response['formsetclass'](
self.request.POST if self.request.method == 'POST' else None,
instance=self.object,
prefix=response['prefix'],
)
formsets.append(response)
return formsets
def main(self, request, *args, **kwargs):
self.object = self.get_object()
self.request = request
self.formsets = self.get_formsets()
def get(self, request, *args, **kwargs):
self.main(request, *args, **kwargs)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
@transaction.atomic()
def post(self, request, *args, **kwargs):
self.main(request, *args, **kwargs)
valid = True
for f in self.formsets:
valid &= f['formset'].is_valid()
if valid:
for f in self.formsets:
for form in f['formset']:
if 'DELETE' in form.cleaned_data and form.cleaned_data['DELETE'] is True:
if form.instance.pk is None:
continue
form.instance.delete()
else:
form.instance.event = request.event
form.instance.item = self.object
form.save()
return redirect(self.get_success_url())
else:
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['formsets'] = self.formsets
return context
def get_success_url(self) -> str:
return reverse('control:event.item.restrictions', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
'item': self.object.identity
}) + '?success=true'

View File

@@ -0,0 +1,21 @@
from django.shortcuts import render
from django.views.generic import ListView
from pretix.base.models import Event
class EventList(ListView):
model = Event
context_object_name = 'events'
template_name = 'pretixcontrol/events/index.html'
def get_queryset(self):
return Event.objects.current.filter(
permitted__id__exact=self.request.user.pk
).prefetch_related(
"organizer",
)
def index(request):
return render(request, 'pretixcontrol/base.html', {})