mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
tixlcontrol: Quota UI
This commit is contained in:
@@ -11,7 +11,7 @@ class VersionedBaseModelForm(BaseModelForm):
|
||||
if self.instance.pk is not None and isinstance(self.instance, Versionable):
|
||||
if self.has_changed():
|
||||
self.instance = self.instance.clone()
|
||||
super().save(commit)
|
||||
return super().save(commit)
|
||||
|
||||
|
||||
class VersionedModelForm(six.with_metaclass(ModelFormMetaclass, VersionedBaseModelForm)):
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from itertools import product
|
||||
import copy
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
@@ -6,11 +8,54 @@ from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, Permis
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.template.defaultfilters import date as _date
|
||||
from django.core.validators import RegexValidator
|
||||
from versions.models import Versionable, VersionedForeignKey, VersionedManyToManyField
|
||||
import six
|
||||
from versions.models import Versionable as BaseVersionable
|
||||
from versions.models import VersionedForeignKey, VersionedManyToManyField, get_utc_now
|
||||
|
||||
from tixlbase.types import VariationDict
|
||||
|
||||
|
||||
class Versionable(BaseVersionable):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def clone_shallow(self, forced_version_date=None):
|
||||
"""
|
||||
This behaves like clone(), but misses all the Many2Many-relation-handling. This is
|
||||
a performance optimization for cases in which we have to handle the Many2Many relations
|
||||
by handy anyways.
|
||||
"""
|
||||
if not self.pk:
|
||||
raise ValueError('Instance must be saved before it can be cloned')
|
||||
|
||||
if self.version_end_date:
|
||||
raise ValueError('This is a historical item and can not be cloned.')
|
||||
|
||||
if forced_version_date:
|
||||
if not self.version_start_date <= forced_version_date <= get_utc_now():
|
||||
raise ValueError('The clone date must be between the version start date and now.')
|
||||
else:
|
||||
forced_version_date = get_utc_now()
|
||||
|
||||
earlier_version = self
|
||||
|
||||
later_version = copy.copy(earlier_version)
|
||||
later_version.version_end_date = None
|
||||
later_version.version_start_date = forced_version_date
|
||||
|
||||
# set earlier_version's ID to a new UUID so the clone (later_version) can
|
||||
# get the old one -- this allows 'head' to always have the original
|
||||
# id allowing us to get at all historic foreign key relationships
|
||||
earlier_version.id = six.u(str(uuid.uuid4()))
|
||||
earlier_version.version_end_date = forced_version_date
|
||||
earlier_version.save()
|
||||
|
||||
later_version.save()
|
||||
|
||||
return later_version
|
||||
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
"""
|
||||
This is the user manager for our custom user model. See the User
|
||||
@@ -614,18 +659,18 @@ class Item(Versionable):
|
||||
if use_cache and hasattr(self, '_get_all_variations_cache'):
|
||||
return self._get_all_variations_cache
|
||||
|
||||
all_variations = self.variations.current.all().prefetch_related("values")
|
||||
all_properties = self.properties.current.all().prefetch_related("values")
|
||||
all_variations = self.variations.all().prefetch_related("values")
|
||||
all_properties = self.properties.all().prefetch_related("values")
|
||||
variations_cache = {}
|
||||
for var in all_variations:
|
||||
key = []
|
||||
for v in var.values.current.all():
|
||||
for v in var.values.all():
|
||||
key.append((v.prop_id, v.identity))
|
||||
key = tuple(sorted(key))
|
||||
variations_cache[key] = var
|
||||
|
||||
result = []
|
||||
for comb in product(*[prop.values.current.all() for prop in all_properties]):
|
||||
for comb in product(*[prop.values.all() for prop in all_properties]):
|
||||
if len(comb) == 0:
|
||||
result.append(VariationDict())
|
||||
continue
|
||||
@@ -772,8 +817,8 @@ class Quota(Versionable):
|
||||
and no more than 100 of them will be VIP tickets (but 450 normal and 50
|
||||
VIP tickets will be fine).
|
||||
|
||||
As always, a quota can not only be tied to an item, but also to a specific
|
||||
variation. We follow the general rule here: If there are no variations
|
||||
As always, a quota can not only be tied to an item, but also to specific
|
||||
variations. We follow the general rule here: If there are no variations
|
||||
speficied, the quota applies to all of them, and if there are variations
|
||||
specified, the quota applies to those.
|
||||
|
||||
@@ -797,10 +842,12 @@ class Quota(Versionable):
|
||||
items = VersionedManyToManyField(
|
||||
Item,
|
||||
verbose_name=_("Item"),
|
||||
related_name="quotas",
|
||||
blank=True
|
||||
)
|
||||
variations = VariationsField(
|
||||
ItemVariation,
|
||||
related_name="quotas",
|
||||
blank=True,
|
||||
verbose_name=_("Variations")
|
||||
)
|
||||
|
||||
@@ -5,14 +5,23 @@ $(function () {
|
||||
reorderMode: 'animate'
|
||||
});
|
||||
$(document).on("click", ".variations .variations-select-all", function (e) {
|
||||
$(this).parent().parent().find("input[type=checkbox]").prop("checked", true);
|
||||
$(this).parent().parent().find("input[type=checkbox]").prop("checked", true).change();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
});
|
||||
$(document).on("click", ".variations .variations-select-none", function (e) {
|
||||
$(this).parent().parent().find("input[type=checkbox]").prop("checked", false);
|
||||
$(this).parent().parent().find("input[type=checkbox]").prop("checked", false).change();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
});
|
||||
if ($(".items-on-quota").length) {
|
||||
$(".items-on-quota .panel").each(function () {
|
||||
var $panel = $(this);
|
||||
$panel.toggleClass("panel-success", $panel.find("input:checked").length > 0);
|
||||
$(this).find("input").change(function () {
|
||||
$panel.toggleClass("panel-success", $panel.find("input:checked").length > 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
$('.collapse').collapse();
|
||||
});
|
||||
|
||||
@@ -47,6 +47,10 @@ td > .form-group > .checkbox {
|
||||
}
|
||||
}
|
||||
|
||||
.panel .form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.container ul.nav-pills {
|
||||
margin: 20px 0;
|
||||
}
|
||||
@@ -56,6 +60,13 @@ td > .form-group > .checkbox {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-quotas td ul {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
@media (min-width: @screen-sm-min) {
|
||||
.variation-matrix > tbody > tr > td {
|
||||
line-height: 34px;
|
||||
|
||||
@@ -8,6 +8,14 @@
|
||||
<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=item.identity %}">{% trans "Variations" %}</a></li>
|
||||
<li {% if "event.item.restrictions" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item.restrictions' organizer=request.event.organizer.slug event=request.event.slug item=item.identity %}">{% trans "Restrictions" %}</a></li>
|
||||
</ul>
|
||||
{% if item.identity and not item.quotas.exists %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
Please note, that your item will <strong>not</strong> be available for sale until you added your item
|
||||
to an existing or newly created quota.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block inside %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<h4 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#accordion"
|
||||
href="#collapse{{ f.prefix }}">
|
||||
Test
|
||||
{{ set.title }}
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
{% block content %}
|
||||
<ul class="nav nav-pills">
|
||||
<li {% if "event.items" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.items' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Items" %}</a></li>
|
||||
<li {% if "event.items.quotas" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.items.quotas' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Quotas" %}</a></li>
|
||||
<li {% if "event.items.categories" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.items.categories' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Categories" %}</a></li>
|
||||
<li {% if "event.items.properties" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.items.properties' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Properties" %}</a></li>
|
||||
<li {% if "event.items.questions" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.items.questions' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Questions" %}</a></li>
|
||||
<li {% if "event.items.quotas" in url_name %}class="active"{% endif %}><a href="{% url 'control:event.items.quotas' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Quotas" %}</a></li>
|
||||
</ul>
|
||||
{% block inside %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,6 +15,35 @@
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.size layout="horizontal" %}
|
||||
<legend>{% trans "Items" %}</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Please select the items or item variations this quota should be applied to. If you apply two
|
||||
quotas to the same items, it will only be available if <strong>both</strong> quotas have capacity
|
||||
left.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div class="panel-group items-on-quota">
|
||||
{% for item in items %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#accordion"
|
||||
href="#collapse{{ item.identity }}">
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapse{{ item.identity }}" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<div class="form-horizontal">
|
||||
{% bootstrap_field item.field layout="horizontal" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<p>
|
||||
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new quota" %}</a>
|
||||
</p>
|
||||
<table class="table table-hover">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Quota name" %}</th>
|
||||
@@ -33,7 +33,15 @@
|
||||
{% for q in quotas %}
|
||||
<tr>
|
||||
<td><strong><a href="{% url "control:event.items.quotas.edit" organizer=request.event.organizer.slug event=request.event.slug quota=q.identity %}">{{ q.name }}</a></strong></td>
|
||||
<td></td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for item in q.items.all %}
|
||||
<li><a href="
|
||||
{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.identity %}"
|
||||
>{{ item.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
<td>{{ q.size }}</td>
|
||||
<td></td>
|
||||
<td class="text-right"><a href="{% url "control:event.items.quotas.delete" organizer=request.event.organizer.slug event=request.event.slug quota=q.identity %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a></td>
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 as _
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from tixlbase.forms import VersionedModelForm
|
||||
|
||||
from tixlbase.models import ItemVariation, PropertyValue, Item
|
||||
@@ -301,7 +301,7 @@ class VariationsField(forms.ModelMultipleChoiceField):
|
||||
if self.required and not value:
|
||||
raise ValidationError(self.error_messages['required'], code='required')
|
||||
elif not self.required and not value:
|
||||
return self.queryset.none()
|
||||
return []
|
||||
if not isinstance(value, (list, tuple)):
|
||||
raise ValidationError(self.error_messages['list'], code='list')
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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
|
||||
@@ -9,13 +11,14 @@ 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 tixlbase.forms import VersionedModelForm
|
||||
|
||||
from tixlbase.models import (
|
||||
Item, ItemCategory, Property, ItemVariation, PropertyValue, Question, Quota
|
||||
)
|
||||
Item, ItemCategory, Property, ItemVariation, PropertyValue, Question, Quota,
|
||||
Versionable)
|
||||
from tixlcontrol.permissions import EventPermissionRequiredMixin, event_permission_required
|
||||
from tixlcontrol.views.forms import TolerantFormsetModelForm
|
||||
from tixlcontrol.views.forms import TolerantFormsetModelForm, VariationsField
|
||||
from tixlcontrol.signals import restriction_formset
|
||||
|
||||
|
||||
@@ -431,10 +434,45 @@ class QuotaList(ListView):
|
||||
def get_queryset(self):
|
||||
return Quota.objects.current.filter(
|
||||
event=self.request.event
|
||||
)
|
||||
).prefetch_related("items")
|
||||
|
||||
|
||||
class QuotaForm(VersionedModelForm):
|
||||
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
|
||||
@@ -445,7 +483,53 @@ class QuotaForm(VersionedModelForm):
|
||||
]
|
||||
|
||||
|
||||
class QuotaCreate(EventPermissionRequiredMixin, CreateView):
|
||||
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 tixlbase.models.Versionable.clone_shallow()
|
||||
# items = self.object.items.all()
|
||||
# variations = self.object.variations.all()
|
||||
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 = 'tixlcontrol/items/quota.html'
|
||||
@@ -463,7 +547,7 @@ class QuotaCreate(EventPermissionRequiredMixin, CreateView):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class QuotaUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
class QuotaUpdate(EventPermissionRequiredMixin, QuotaEditorMixin, UpdateView):
|
||||
model = Quota
|
||||
form_class = QuotaForm
|
||||
template_name = 'tixlcontrol/items/quota.html'
|
||||
|
||||
Reference in New Issue
Block a user