Add variation descriptions and allow to order addons

This commit is contained in:
Raphael Michel
2017-05-02 00:12:22 +02:00
parent 55ce83a642
commit d389e4390f
11 changed files with 174 additions and 18 deletions

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-05-01 21:16
from __future__ import unicode_literals
import i18nfield.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0056_auto_20170414_1044'),
]
operations = [
migrations.AlterModelOptions(
name='itemaddon',
options={'ordering': ('position', 'pk')},
),
migrations.AddField(
model_name='itemaddon',
name='position',
field=models.PositiveIntegerField(default=0, verbose_name='Position'),
),
migrations.AddField(
model_name='itemvariation',
name='description',
field=i18nfield.fields.I18nTextField(blank=True, help_text='This is shown below the variation name in lists.', null=True, verbose_name='Description'),
),
]

View File

@@ -310,6 +310,8 @@ class ItemVariation(models.Model):
:type item: Item
:param value: A string defining this variation
:type value: str
:param description: A short description
:type description: str
:param active: Whether this variation is being sold.
:type active: bool
:param default_price: This variation's default price
@@ -327,6 +329,11 @@ class ItemVariation(models.Model):
default=True,
verbose_name=_("Active"),
)
description = I18nTextField(
verbose_name=_("Description"),
help_text=_("This is shown below the variation name in lists."),
null=True, blank=True,
)
position = models.PositiveIntegerField(
default=0,
verbose_name=_("Position")
@@ -413,9 +420,14 @@ class ItemAddOn(models.Model):
default=1,
verbose_name=_('Maximum number')
)
position = models.PositiveIntegerField(
default=0,
verbose_name=_("Position")
)
class Meta:
unique_together = (('base_item', 'addon_category'),)
ordering = ('position', 'pk')
def clean(self):
if self.max_count < self.min_count:

View File

@@ -211,6 +211,7 @@ class ItemVariationForm(I18nModelForm):
'value',
'active',
'default_price',
'description',
]

View File

@@ -24,6 +24,7 @@
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
@@ -31,8 +32,12 @@
<div class="col-sm-8">
<h3 class="panel-title">{% trans "Add-On" %}</h3>
</div>
<div class="col-sm-4">
<button type="button" class="btn btn-xs btn-danger pull-right" data-formset-delete-button>
<div class="col-sm-4 text-right">
<button type="button" class="btn btn-xs btn-default" data-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-xs btn-default" data-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
@@ -51,6 +56,7 @@
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
@@ -59,7 +65,11 @@
<h3 class="panel-title">{% trans "Add-On" %}</h3>
</div>
<div class="col-sm-4">
<button type="button" class="btn btn-xs btn-danger pull-right" data-formset-delete-button>
<button type="button" class="btn btn-xs btn-default" data-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-xs btn-default" data-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>

View File

@@ -37,6 +37,7 @@
{% bootstrap_form_errors form %}
{% bootstrap_field form.active layout='horizontal' %}
{% bootstrap_field form.default_price layout='horizontal' %}
{% bootstrap_field form.description layout='horizontal' %}
</div>
</div>
{% endfor %}
@@ -69,6 +70,7 @@
<div class="panel-body form-horizontal">
{% bootstrap_field formset.empty_form.active layout='horizontal' %}
{% bootstrap_field formset.empty_form.default_price layout='horizontal' %}
{% bootstrap_field formset.empty_form.description layout='horizontal' %}
</div>
</div>
{% endescapescript %}

View File

@@ -945,7 +945,7 @@ class ItemAddOns(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
formsetclass = inlineformset_factory(
Item, ItemAddOn,
form=ItemAddOnForm, formset=ItemAddOnsFormSet,
can_order=False, can_delete=True, extra=0
can_order=True, can_delete=True, extra=0
)
return formsetclass(self.request.POST if self.request.method == "POST" else None,
queryset=ItemAddOn.objects.filter(base_item=self.get_object()),
@@ -965,12 +965,13 @@ class ItemAddOns(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
form.instance.delete()
form.instance.pk = None
forms = [
ef for ef in self.formset.extra_forms + self.formset.initial_forms
if ef not in self.formset.deleted_forms
forms = 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
]
for i, form in enumerate(forms):
form.instance.base_item = self.get_object()
form.instance.position = i
created = not form.instance.pk
form.save()
if form.has_changed():

View File

@@ -3,12 +3,17 @@ from decimal import Decimal
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Count, Prefetch, Q
from django.forms.widgets import RadioChoiceInput, RadioFieldRenderer
from django.utils.encoding import force_text
from django.utils.formats import number_format
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import ItemVariation, Question
from pretix.base.models.orders import InvoiceAddress
from pretix.base.templatetags.rich_text import rich_text
class ContactForm(forms.Form):
@@ -146,6 +151,63 @@ class QuestionsForm(forms.Form):
self.fields['question_%s' % q.id] = field
# The following will get totally different once Django 1.11 is integrated
class AddOnVariationSelectInput(RadioChoiceInput):
def __init__(self, name, value, attrs, choice, index):
super().__init__(name, value, attrs, choice, index)
self.description = force_text(choice[2])
def render(self, name=None, value=None, attrs=None):
if self.id_for_label:
label_for = format_html(' for="{}"', self.id_for_label)
else:
label_for = ''
attrs = dict(self.attrs, **attrs) if attrs else self.attrs
if self.description:
return format_html(
'<label{}>{} {}</label> <span class="fa fa-info-circle toggle-variation-description"></span>'
'<div class="variation-description addon-variation-description">{}</div>',
label_for, self.tag(attrs), self.choice_label,
rich_text(str(self.description))
)
else:
return format_html(
'<label{}>{} {}</label>',
label_for, self.tag(attrs), self.choice_label,
)
class AddOnVariationSelectRenderer(RadioFieldRenderer):
choice_input_class = AddOnVariationSelectInput
def render(self):
id_ = self.attrs.get('id')
output = []
for i, choice in enumerate(self.choices):
w = self.choice_input_class(self.name, self.value, self.attrs.copy(), choice, i)
output.append(format_html(self.inner_html, choice_value=force_text(w), sub_widgets=''))
return format_html(
self.outer_html,
id_attr=format_html(' id="{}"', id_) if id_ else '',
content=mark_safe('\n'.join(output)),
)
class AddOnVariationSelect(forms.RadioSelect):
renderer = AddOnVariationSelectRenderer
class AddOnVariationField(forms.ChoiceField):
def valid_value(self, value):
text_value = force_text(value)
for k, v, d in self.choices:
if value == k or text_value == force_text(k):
return True
return False
class AddOnsForm(forms.Form):
"""
This form class is responsible for selecting add-ons to a product in the cart.
@@ -232,18 +294,18 @@ class AddOnsForm(forms.Form):
for i in items:
if i.has_variations:
choices = [('', _('no selection'))]
choices = [('', _('no selection'), '')]
for v in i.available_variations:
cached_availability = v.check_quotas(_cache=quota_cache)
choices.append((v.pk, self._label(event, v, cached_availability)))
choices.append((v.pk, self._label(event, v, cached_availability), v.description))
field = forms.ChoiceField(
field = AddOnVariationField(
choices=choices,
label=i.name,
required=False,
widget=forms.RadioSelect,
help_text=i.description,
initial=current_addons.get(i.pk)
widget=AddOnVariationSelect,
help_text=rich_text(str(i.description)),
initial=current_addons.get(i.pk),
)
else:
cached_availability = i.check_quotas(_cache=quota_cache)
@@ -251,7 +313,7 @@ class AddOnsForm(forms.Form):
label=self._label(event, i, cached_availability),
required=False,
initial=i.pk in current_addons,
help_text=i.description
help_text=rich_text(str(i.description)),
)
self.fields['item_%s' % i.pk] = field

View File

@@ -101,7 +101,10 @@
<a href="#" data-toggle="variations">
<strong>{{ item.name }}</strong>
</a>
{% if item.description %}<p>{{ item.description|localize|rich_text }}</p>
{% if item.description %}
<div class="product-description">
{{ item.description|localize|rich_text }}
</div>
{% endif %}
{% if item.min_per_order %}
<p><small>
@@ -134,6 +137,11 @@
<div class="row-fluid product-row variation">
<div class="col-md-8 col-xs-12">
{{ var }}
{% if var.description %}
<div class="variation-description">
{{ var.description|localize|rich_text }}
</div>
{% endif %}
{% if event.settings.show_quota_left %}
{% include "pretixpresale/event/fragment_quota_left.html" with avail=var.cached_availability %}
{% endif %}
@@ -201,7 +209,10 @@
{% endif %}
<strong>{{ item.name }}</strong>
{% if item.description %}
<p class="description">{{ item.description|localize|rich_text }}</p>{% endif %}
<div class="product-description">
{{ item.description|localize|rich_text }}
</div>
{% endif %}
{% if event.settings.show_quota_left %}
{% include "pretixpresale/event/fragment_quota_left.html" with avail=item.cached_availability %}
{% endif %}

View File

@@ -37,7 +37,10 @@
{% endif %}
<strong>{{ item.name }}</strong>
{% if item.description %}
<p>{{ item.description|localize|rich_text }}</p>{% endif %}
<div class="product-description">
{{ item.description|localize|rich_text }}
</div>
{% endif %}
</div>
<div class="col-md-2 col-xs-6 price">
{% if item.min_price != item.max_price or item.free_price %}
@@ -57,6 +60,11 @@
<div class="row-fluid product-row variation">
<div class="col-md-8 col-xs-12">
{{ var }}
{% if var.description %}
<div class="variation-description">
{{ var.description|localize|rich_text }}
</div>
{% endif %}
</div>
<div class="col-md-2 col-xs-6 price">
{% if item.free_price %}
@@ -116,7 +124,10 @@
{% endif %}
<strong>{{ item.name }}</strong>
{% if item.description %}
<p class="description">{{ item.description|localize|rich_text }}</p>{% endif %}
<div class="product-description">
{{ item.description|localize|rich_text }}
</div>
{% endif %}
</div>
<div class="col-md-2 col-xs-6 price">
{% if item.free_price %}

View File

@@ -20,6 +20,7 @@ $(function () {
$($(this).attr("data-target")).collapse('show');
});
$(".js-only").removeClass("js-only");
$(".js-hidden").hide();
$(".variations-collapsed").hide();
$("a[data-toggle=variations]").click(function (e) {
$(this).parent().parent().parent().find(".variations").slideToggle();
@@ -37,6 +38,12 @@ $(function () {
$('[data-toggle="tooltip"]').tooltip();
$("#ajaxerr").on("click", ".ajaxerr-close", ajaxErrDialog.hide);
// AddOns
$('.addon-variation-description').hide();
$('.toggle-variation-description').click(function () {
$(this).parent().find('.addon-variation-description').slideToggle();
});
// Copy answers
$(".js-copy-answers").click(function (e) {

View File

@@ -47,9 +47,18 @@
}
}
}
.radio .variation-description {
padding-left: 20px;
}
.variation-description {
color: lighten($text-color, 25%);
}
.voucher-row {
margin-top: 10px;
}
.toggle-variation-description {
cursor: pointer;
}
#voucher-toggle {
display: none;
}