Removed multi-dimensional item variations [backwards-incompatible]

This commit is contained in:
Raphael Michel
2015-12-13 15:03:56 +01:00
parent bc13ba9517
commit f748752391
36 changed files with 616 additions and 2019 deletions

View File

@@ -110,277 +110,6 @@ def selector(values, prop):
]
def sort(v, prop):
# Given a list of variations, this will sort them by their position
# on the x-axis
return v[prop.id].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].id].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.all())
# prop2 is the property on all the grid's x-axes
prop2 = properties[1]
prop2v = list(prop2.values.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.all() for prop in properties[2:]]):
if len(gridrow) > 0:
output.append('<strong>')
output.append(", ".join([str(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'].id 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')
cleaned_value = self._clean_value(value)
qs = self.item.variations.filter(id__in=cleaned_value)
# Re-check for consistency
pks = set(force_text(getattr(o, "id")) 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
def _clean_value(self, value):
# 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.id 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.id
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.id
cleaned_value.append(str(var.id))
else:
# An ItemVariation id was given
cleaned_value.append(pk)
return cleaned_value
choices = property(_get_choices, forms.ChoiceField._set_choices)
class ExtFileField(forms.FileField):
def __init__(self, *args, **kwargs):
ext_whitelist = kwargs.pop("ext_whitelist")
@@ -396,108 +125,3 @@ class ExtFileField(forms.FileField):
if ext not in self.ext_whitelist:
raise forms.ValidationError(_("Filetype not allowed!"))
return data
class BaseNestedFormset(I18nFormSet):
def add_fields(self, form, index):
# allow the super class to create the fields as usual
super().add_fields(form, index)
form.nested = []
for f in self.nested_formset_class:
inner_formset = f(
instance=form.instance,
data=form.data if form.is_bound else None,
prefix='%s-%s' % (form.prefix, f.get_default_prefix()),
queryset=form.instance.values.all(),
event=self.event
)
form.nested.append(inner_formset)
def is_valid(self):
result = super(BaseNestedFormset, self).is_valid()
if self.is_bound:
# look at any nested formsets, as well
for form in self.forms:
if not self._should_delete_form(form):
for n in form.nested:
result = result and n.is_valid()
return result
def save(self, commit=True):
result = super(BaseNestedFormset, self).save(commit=commit)
for form in self.forms:
if not self._should_delete_form(form):
for n in form.nested:
n.save(commit=commit)
return result
def nestedformset_factory(model, nested_formset, form=ModelForm,
formset=BaseNestedFormset, fk_name=None, fields=None,
exclude=None, extra=3, can_order=False,
can_delete=True, max_num=None,
formfield_callback=None, widgets=None,
validate_max=False, localized_fields=None,
labels=None, help_texts=None, error_messages=None):
kwargs = {
'form': form,
'formfield_callback': formfield_callback,
'formset': formset,
'extra': extra,
'can_delete': can_delete,
'can_order': can_order,
'fields': fields,
'exclude': exclude,
'max_num': max_num,
'widgets': widgets,
'validate_max': validate_max,
'localized_fields': localized_fields,
'labels': labels,
'help_texts': help_texts,
'error_messages': error_messages,
}
nfs_class = modelformset_factory(model, **kwargs)
nfs_class.nested_formset_class = []
for f in nested_formset:
nfs_class.nested_formset_class.append(f)
return nfs_class
class NestedInnerI18nInlineFormSet(I18nFormSet):
"""A formset for child objects related to a parent."""
def __init__(self, data=None, files=None, instance=None,
save_as_new=False, prefix=None, queryset=None, **kwargs):
if instance is None:
self.instance = self.fk.rel.to()
else:
self.instance = instance
self.save_as_new = save_as_new
if queryset is None:
if self.instance is not None:
queryset = getattr(self.instance, self.fk.related_query_name()).all()
else:
queryset = self.model._default_manager
if self.instance.pk is not None:
qs = queryset
else:
qs = self.model._default_manager.none()
super().__init__(data, files, prefix=prefix, queryset=qs, **kwargs)
@property
def empty_form(self):
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__inner_prefix__'),
empty_permitted=True,
event=self.event
)
self.add_fields(form, None)
return form

View File

@@ -1,15 +1,13 @@
import copy
from django import forms
from django.forms import BooleanField
from django.forms import BooleanField, ModelMultipleChoiceField
from django.utils.translation import ugettext_lazy as _
from pretix.base.forms import I18nModelForm
from pretix.base.models import (
Item, ItemCategory, ItemVariation, Property, PropertyValue, Question,
Quota,
Item, ItemCategory, ItemVariation, Question, Quota,
)
from pretix.control.forms import TolerantFormsetModelForm, VariationsField
class CategoryForm(I18nModelForm):
@@ -21,24 +19,6 @@ class CategoryForm(I18nModelForm):
]
class PropertyForm(I18nModelForm):
class Meta:
model = Property
localized_fields = '__all__'
fields = [
'name',
]
class PropertyValueForm(TolerantFormsetModelForm):
class Meta:
model = PropertyValue
localized_fields = '__all__'
fields = [
'value',
]
class QuestionForm(I18nModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -75,13 +55,14 @@ class QuotaForm(I18nModelForm):
active_variations = set()
for item in items:
if len(item.properties.all()) > 0:
self.fields['item_%s' % item.id] = VariationsField(
item, label=_("Activate for"),
if len(item.variations.all()) > 0:
self.fields['item_%s' % item.id] = ModelMultipleChoiceField(
label=_("Activate for"),
required=False,
initial=active_variations
initial=active_variations,
queryset=item.variations.all(),
widget=forms.CheckboxSelectMultiple
)
self.fields['item_%s' % item.id].set_item(item)
else:
self.fields['item_%s' % item.id] = BooleanField(
label=_("Activate"),
@@ -125,6 +106,7 @@ class ItemVariationForm(I18nModelForm):
model = ItemVariation
localized_fields = '__all__'
fields = [
'value',
'active',
'default_price',
]

View File

@@ -6,7 +6,6 @@
<h1>{% trans "Modify product:" %} {{ object.name }}</h1>
<ul class="nav nav-pills">
<li {% if "event.item" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item' organizer=request.event.organizer.slug event=request.event.slug item=object.id %}">{% trans "General information" %}</a></li>
<li {% if "event.item.properties" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item.properties' organizer=request.event.organizer.slug event=request.event.slug item=object.id %}">{% trans "Properties" %}</a></li>
<li {% if "event.item.variations" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item.variations' organizer=request.event.organizer.slug event=request.event.slug item=object.id %}">{% trans "Variations" %}</a></li>
</ul>
{% else %}

View File

@@ -1,149 +0,0 @@
{% extends "pretixcontrol/item/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load formset_tags %}
{% block inside %}
<form class="form-horizontal branches" method="post" action="">
{% csrf_token %}
{% if state %}
<div class="alert alert-{{ state.class }}">{{ state.message|safe }}</div>
{% endif %}
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
{% for form in formset %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
{% bootstrap_field form.name layout='inline' form_group_class="" %}
</h4>
</div>
<div class="panel-body">
{% bootstrap_form_errors form %}
<div class="nested-formset" data-formset-prefix="{{ form.nested.0.prefix }}">
<div data-nested-formset-body>
{{ form.nested.0.management_form }}
{% for f in form.nested.0 %}
<div class="form-group" data-nested-formset-form>
{{ f.id }}
<div class="col-sm-9">
{% bootstrap_field f.value form_group_class="" layout="inline" %}
</div>
<div class="sr-only">
{% bootstrap_field f.ORDER form_group_class="" layout="inline" %}
{% bootstrap_field f.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-3 text-right">
<button type="button" class="btn btn-default"
data-nested-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default"
data-nested-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-nested-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-nested-formset-empty-form>
{% escapescript %}
<div class="form-group" data-nested-formset-form>
{{ form.nested.0.empty_form.id }}
<div class="col-sm-9">
{% bootstrap_field form.nested.0.empty_form.value form_group_class="" layout="inline" %}
</div>
<div class="sr-only">
{% bootstrap_field form.nested.0.empty_form.ORDER form_group_class="" layout="inline" %}
{% bootstrap_field form.nested.0.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-3 text-right">
<button type="button" class="btn btn-default"
data-nested-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default"
data-nested-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-nested-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endescapescript %}
</script>
<p style="margin-top: 10px">
<button type="button" class="btn btn-default" data-nested-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new value" %}</button>
</p>
</div>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
{% bootstrap_field formset.empty_form.name layout='inline' form_group_class="" %}
</h4>
</div>
<div class="panel-body">
<div class="nested-formset" data-formset-prefix="{{ formset.empty_form.nested.0.prefix }}">
<div data-nested-formset-body>
{{ formset.empty_form.nested.0.management_form }}
</div>
{{ '<script type="form-template" data-nested-formset-empty-form>' }}
<div class="form-group" data-nested-formset-form>
{{ formset.empty_form.nested.0.empty_form.id }}
<div class="col-sm-9">
{% bootstrap_field formset.empty_form.nested.0.empty_form.value form_group_class="" layout="inline" %}
</div>
<div class="sr-only">
{% bootstrap_field formset.empty_form.nested.0.empty_form.ORDER form_group_class="" layout="inline" %}
{% bootstrap_field formset.empty_form.nested.0.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-3 text-right">
<button type="button" class="btn btn-default"
data-nested-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default"
data-nested-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-nested-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{{ '</script>' }}
<p style="margin-top: 10px">
<button type="button" class="btn btn-default" data-nested-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new value" %}</button>
</p>
</div>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new property" %}</button>
</p>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,87 @@
{% extends "pretixcontrol/item/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load formset_tags %}
{% block inside %}
<form class="form-horizontal branches" method="post" action="">
{% csrf_token %}
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
{% for form in formset %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-9">
{% bootstrap_field form.value layout='inline' form_group_class="" %}
</div>
<div class="col-md-3 text-right">
<button type="button" class="btn btn-default" data-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default" data-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_form_errors form %}
{% bootstrap_field form.active layout='horizontal' %}
{% bootstrap_field form.default_price layout='horizontal' %}
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-9">
{% bootstrap_field formset.empty_form.value layout='inline' form_group_class="" %}
</div>
<div class="col-md-3 text-right">
<button type="button" class="btn btn-default" data-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default" data-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_field formset.empty_form.active layout='horizontal' %}
{% bootstrap_field formset.empty_form.default_price layout='horizontal' %}
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new variation" %}</button>
</p>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -1,6 +0,0 @@
{% extends "pretixcontrol/item/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<p><em>{% trans "You have to define and select propreties to be able to configure variations." %}</em></p>
{% endblock %}

View File

@@ -1,35 +0,0 @@
{% extends "pretixcontrol/item/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<form action="" method="post">
{% csrf_token %}
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>{{ properties.0 }}</th>
<th>{% trans "Active" %}</th>
<th>{% trans "Price" %}</th>
</tr>
</thead>
<tbody>
{% for form in forms %}
{% bootstrap_form_errors form type='all' layout='inline' %}
<tr>
<td>{{ form.values.0 }}</td>
<td>{% bootstrap_field form.active layout='inline' %}</td>
<td>{% bootstrap_field form.default_price layout='inline' %} {{ form.default_price.errors }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -1,51 +0,0 @@
{% extends "pretixcontrol/item/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<form action="" method="post">
{% csrf_token %}
{% for major in forms %}
{% if major.row %}
<h3>{{ major.row }}</h3>
{% endif %}
<div class="table-responsive">
<table class="table variation-matrix">
<thead>
<tr>
<th></th>
{% for val in properties.1.values.all %}
<th>{{ val.value }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for sub in major.forms %}
<tr>
<td>{{ sub.row.value }}</td>
{% for form in sub.forms %}
<td>
<div class="row">
<div class="col-sm-5">
{% bootstrap_field form.active layout='inline' %}
</div>
<div class="col-sm-7">
{% bootstrap_field form.default_price layout='inline' %}
</div>
</div>
{{ form.default_price.errors }}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -30,8 +30,6 @@ urlpatterns = [
url(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
url(r'^items/(?P<item>\d+)/variations$', item.ItemVariations.as_view(),
name='event.item.variations'),
url(r'^items/(?P<item>\d+)/properties$', item.ItemProperties.as_view(),
name='event.item.properties'),
url(r'^items/(?P<item>\d+)/up$', item.item_move_up, name='event.items.up'),
url(r'^items/(?P<item>\d+)/down$', item.item_move_down, name='event.items.down'),
url(r'^items/(?P<item>\d+)/delete$', item.ItemDelete.as_view(), name='event.items.delete'),

View File

@@ -1,9 +1,7 @@
from itertools import product
from django.contrib import messages
from django.core.urlresolvers import resolve, reverse
from django.db import transaction
from django.forms.models import inlineformset_factory
from django.forms.models import ModelMultipleChoiceField, inlineformset_factory
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import redirect
from django.utils.functional import cached_property
@@ -13,16 +11,12 @@ from django.views.generic.base import TemplateView
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import DeleteView
from pretix.base.i18n import I18nFormSet
from pretix.base.models import (
Item, ItemCategory, ItemVariation, Property, PropertyValue, Question,
Quota,
)
from pretix.control.forms import (
NestedInnerI18nInlineFormSet, VariationsField, nestedformset_factory,
Item, ItemCategory, ItemVariation, Question, Quota,
)
from pretix.control.forms.item import (
CategoryForm, ItemFormGeneral, ItemVariationForm, PropertyForm,
PropertyValueForm, QuestionForm, QuotaForm,
CategoryForm, ItemFormGeneral, ItemVariationForm, QuestionForm, QuotaForm,
)
from pretix.control.permissions import (
EventPermissionRequiredMixin, event_permission_required,
@@ -350,7 +344,7 @@ class QuotaList(ListView):
class QuotaEditorMixin:
@cached_property
def items(self) -> "List[Item]":
return list(self.request.event.items.all().prefetch_related("properties", "variations"))
return list(self.request.event.items.all().prefetch_related("variations"))
def get_form(self, form_class=QuotaForm):
if not hasattr(self, '_form'):
@@ -376,7 +370,7 @@ class QuotaEditorMixin:
for item in self.items:
field = form.fields['item_%s' % item.id]
data = form.cleaned_data['item_%s' % item.id]
if isinstance(field, VariationsField):
if isinstance(field, ModelMultipleChoiceField):
for v in data:
selected_variations.append(v)
if data and item not in items:
@@ -548,264 +542,59 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
return super().form_valid(form)
class ItemProperties(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_items'
template_name = 'pretixcontrol/item/properties.html'
def get_success_url(self) -> str:
return reverse('control:event.item.properties', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
'item': self.get_object().id,
})
def get_inner_formset_class(self):
formsetclass = inlineformset_factory(
Property, PropertyValue,
form=PropertyValueForm,
formset=NestedInnerI18nInlineFormSet,
can_order=True, extra=0
)
return formsetclass
def get_outer_formset(self):
formsetclass = nestedformset_factory(
Property, [self.get_inner_formset_class()],
form=PropertyForm, can_order=False, can_delete=True, extra=0
)
formset = formsetclass(self.request.POST if self.request.method == "POST" else None,
queryset=Property.objects.filter(item=self.object).prefetch_related('values'),
event=self.request.event)
return formset
def get_context_data(self, **kwargs):
self.object = self.get_object()
ctx = super().get_context_data(**kwargs)
ctx['formset'] = self.get_outer_formset()
return ctx
@transaction.atomic()
def form_valid(self, formset):
for f in formset:
f.instance.event = self.request.event
f.instance.item = self.get_object()
is_created = not f.instance.pk
f.instance.save()
if f.has_changed() and not is_created:
change_data = {
k: f.cleaned_data.get(k) for k in f.changed_data
}
change_data['id'] = f.instance.pk
f.instance.item.log_action(
'pretix.event.item.property.changed', user=self.request.user, data=change_data
)
elif is_created:
change_data = dict(f.cleaned_data)
change_data['id'] = f.instance.pk
f.instance.item.log_action(
'pretix.event.item.property.added', user=self.request.user, data=change_data
)
for n in f.nested:
for fn in n.deleted_forms:
f.instance.item.log_action(
'pretix.event.item.property.value.deleted', user=self.request.user, data={
'id': fn.instance.pk
}
)
fn.instance.delete()
fn.instance.pk = None
for i, fn in enumerate(n.ordered_forms + [ef for ef in n.extra_forms if ef not in n.ordered_forms]):
fn.instance.position = i
fn.instance.prop = f.instance
fn.save()
if f.has_changed():
change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
change_data['id'] = f.instance.pk
f.instance.item.log_action(
'pretix.event.item.property.value.changed', user=self.request.user, data=change_data
)
for form in n.extra_forms:
if not form.has_changed():
continue
if n.can_delete and n._should_delete_form(form):
continue
change_data = dict(f.cleaned_data)
n.save_new(form)
change_data['id'] = form.instance.pk
f.instance.item.log_action(
'pretix.event.item.property.value.added', user=self.request.user, data=change_data
)
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
def post(self, request, *args, **kwargs):
self.object = self.get_object()
formset = self.get_outer_formset()
if formset.is_valid():
return self.form_valid(formset)
else:
return self.get(request, *args, **kwargs)
class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_items'
template_name = 'pretixcontrol/item/variations.html'
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.id) for i in values]),
)
else:
inst = ItemVariation(item=self.object)
inst.item_id = self.object.id
inst.creation = True
form = ItemVariationForm(
data,
instance=inst,
prefix=",".join([str(i.id) 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.id for v in sorted(values, key=lambda v: v.prop.id)
if v.prop.id != prop2.id
]
def sort(v):
# Given a list of variations, this will sort them by their position
# on the x-axis
return v[prop2.id].sortkey
# 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.all() for prop in self.properties[2:]]):
grids = []
for val1 in prop1.values.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([str(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.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)
@cached_property
def formset(self):
formsetclass = inlineformset_factory(
Item, ItemVariation,
form=ItemVariationForm, formset=I18nFormSet,
can_order=True, can_delete=True, extra=0
)
return formsetclass(self.request.POST if self.request.method == "POST" else None,
queryset=ItemVariation.objects.filter(item=self.get_object()),
event=self.request.event)
def post(self, request, *args, **kwargs):
self.main(request, *args, **kwargs)
context = self.get_context_data(object=self.object)
valid = True
with transaction.atomic():
for form in self.forms_flat:
if form.is_valid() and form.has_changed():
form.save()
change_data = {
k: form.cleaned_data.get(k) for k in form.changed_data
}
change_data['id'] = form.instance.pk
self.object.log_action(
'pretix.event.item.variation.changed', user=self.request.user, data=change_data
if self.formset.is_valid():
for form in self.formset.deleted_forms:
if not form.instance.pk:
continue
self.get_object().log_action(
'pretix.event.item.variation.deleted', user=self.request.user, data={
'id': form.instance.pk
}
)
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)
elif not form.is_valid and form.has_changed():
valid = False
if valid:
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
return self.render_to_response(context)
form.instance.delete()
form.instance.pk = None
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']
for i, form in enumerate(self.formset.ordered_forms + [
ef for ef in self.formset.extra_forms if (ef not in self.formset.ordered_forms and ef not in
self.formset.deleted_forms)
]):
form.instance.position = i
form.instance.item = self.get_object()
created = not form.instance.pk
form.save()
if form.has_changed():
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
change_data['id'] = form.instance.pk
self.get_object().log_action(
'pretix.event.item.variation.changed' if not created else
'pretix.event.item.variation.added',
user=self.request.user, data=change_data
)
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
return self.get(request, *args, **kwargs)
def get_success_url(self) -> str:
return reverse('control:event.item.variations', kwargs={
@@ -815,9 +604,9 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
})
def get_context_data(self, **kwargs) -> dict:
self.object = self.get_object()
context = super().get_context_data(**kwargs)
context['forms'] = self.forms
context['properties'] = self.properties
context['formset'] = self.formset
return context
@@ -835,13 +624,13 @@ class ItemDelete(EventPermissionRequiredMixin, DeleteView):
def is_allowed(self) -> bool:
return not self.get_object().positions.exists()
def get_object(self, queryset=None) -> Property:
def get_object(self, queryset=None) -> Item:
if not hasattr(self, 'object') or not self.object:
try:
self.object = self.request.event.items.get(
id=self.kwargs['item']
)
except Property.DoesNotExist:
except Item.DoesNotExist:
raise Http404(_("The requested product does not exist."))
return self.object

View File

@@ -126,8 +126,7 @@ class OrderDetail(OrderView):
).select_related(
'item', 'variation'
).prefetch_related(
'variation__values', 'variation__values__prop', 'item__questions',
'answers'
'item__questions', 'answers'
)
# Group items of the same variation