mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Fixed #108 -- Removed the restrictions system
This commit is contained in:
@@ -2,8 +2,8 @@ from .auth import User
|
||||
from .base import CachedFile, Versionable, cachedfile_name
|
||||
from .event import Event, EventLock, EventPermission, EventSetting
|
||||
from .items import (
|
||||
BaseRestriction, Item, ItemCategory, ItemVariation, Property,
|
||||
PropertyValue, Question, Quota, VariationsField, itempicture_upload_to,
|
||||
Item, ItemCategory, ItemVariation, Property, PropertyValue, Question,
|
||||
Quota, VariationsField, itempicture_upload_to,
|
||||
)
|
||||
from .orders import (
|
||||
CachedTicket, CartPosition, ObjectWithAnswers, Order, OrderPosition,
|
||||
@@ -14,7 +14,7 @@ from .organizer import Organizer, OrganizerPermission, OrganizerSetting
|
||||
__all__ = [
|
||||
'Versionable', 'User', 'CachedFile', 'Organizer', 'OrganizerPermission', 'Event', 'EventPermission',
|
||||
'ItemCategory', 'Item', 'Property', 'PropertyValue', 'ItemVariation', 'VariationsField', 'Question',
|
||||
'BaseRestriction', 'Quota', 'Order', 'CachedTicket', 'QuestionAnswer', 'ObjectWithAnswers', 'OrderPosition',
|
||||
'Quota', 'Order', 'CachedTicket', 'QuestionAnswer', 'ObjectWithAnswers', 'OrderPosition',
|
||||
'CartPosition', 'EventSetting', 'OrganizerSetting', 'EventLock', 'cachedfile_name', 'itempicture_upload_to',
|
||||
'generate_secret'
|
||||
]
|
||||
|
||||
@@ -80,8 +80,6 @@ class Item(Versionable):
|
||||
An item is a thing which can be sold. It belongs to an event and may or may not belong to a category.
|
||||
Items are often also called 'products' but are named 'items' internally due to historic reasons.
|
||||
|
||||
It has a default price which might by overriden by restrictions.
|
||||
|
||||
:param event: The event this belongs to.
|
||||
:type event: Event
|
||||
:param category: The category this belongs to. May be null.
|
||||
@@ -249,10 +247,9 @@ class Item(Versionable):
|
||||
def get_all_available_variations(self, use_cache: bool=False):
|
||||
"""
|
||||
This method returns a list of all variations which are theoretically
|
||||
possible for sale. It DOES call all activated restriction plugins, and it
|
||||
DOES only return variations which DO have an ItemVariation object, as all
|
||||
variations without one CAN NOT be part of a Quota and therefore can never
|
||||
be available for sale. The only exception is the empty variation
|
||||
possible for sale. It DOES only return variations which DO have an ItemVariation
|
||||
object, as all variations without one CAN NOT be part of a Quota and therefore can
|
||||
never be available for sale. The only exception is the empty variation
|
||||
for items without properties, which never has an ItemVariation object.
|
||||
|
||||
This DOES NOT take into account quotas itself. Use ``is_available`` on the
|
||||
@@ -268,39 +265,18 @@ class Item(Versionable):
|
||||
if use_cache and hasattr(self, '_get_all_available_variations_cache'):
|
||||
return self._get_all_available_variations_cache
|
||||
|
||||
from pretix.base.signals import determine_availability
|
||||
|
||||
variations = self._get_all_generated_variations()
|
||||
responses = determine_availability.send(
|
||||
self.event, item=self,
|
||||
variations=variations, context=None,
|
||||
cache=self.event.get_cache()
|
||||
)
|
||||
|
||||
for i, var in enumerate(variations):
|
||||
var['available'] = var['variation'].active if 'variation' in var else True
|
||||
if 'variation' in var:
|
||||
if var['variation'].default_price:
|
||||
if var['variation'].default_price is not None:
|
||||
var['price'] = var['variation'].default_price
|
||||
else:
|
||||
var['price'] = self.default_price
|
||||
else:
|
||||
var['price'] = self.default_price
|
||||
|
||||
# It is possible, that *multiple* restriction plugins change the default price.
|
||||
# In this case, the cheapest one wins. As soon as there is a restriction
|
||||
# that changes the price, the default price has no effect.
|
||||
|
||||
newprice = None
|
||||
for rec, response in responses:
|
||||
if 'available' in response[i] and not response[i]['available']:
|
||||
var['available'] = False
|
||||
break
|
||||
if 'price' in response[i] and response[i]['price'] is not None \
|
||||
and (newprice is None or response[i]['price'] < newprice):
|
||||
newprice = response[i]['price']
|
||||
var['price'] = newprice or var['price']
|
||||
|
||||
variations = [var for var in variations if var['available']]
|
||||
|
||||
self._get_all_available_variations_cache = variations
|
||||
@@ -322,38 +298,6 @@ class Item(Versionable):
|
||||
return min([q.availability() for q in self.quotas.all()],
|
||||
key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize))
|
||||
|
||||
def check_restrictions(self):
|
||||
"""
|
||||
This method is used to determine whether this ItemVariation is restricted
|
||||
in sale by any restriction plugins.
|
||||
|
||||
:returns:
|
||||
|
||||
* ``False``, if the item is unavailable
|
||||
* the item's price, otherwise
|
||||
|
||||
:raises ValueError: if you call this on an item which has properties associated with it.
|
||||
Please use the method on the ItemVariation object you are interested in.
|
||||
"""
|
||||
if self.properties.count() > 0: # NOQA
|
||||
raise ValueError('Do not call this directly on items which have properties '
|
||||
'but call this on their ItemVariation objects')
|
||||
from pretix.base.signals import determine_availability
|
||||
|
||||
vd = VariationDict()
|
||||
responses = determine_availability.send(
|
||||
self.event, item=self,
|
||||
variations=[vd], context=None,
|
||||
cache=self.event.get_cache()
|
||||
)
|
||||
price = self.default_price
|
||||
for rec, response in responses:
|
||||
if 'available' in response[0] and not response[0]['available']:
|
||||
return False
|
||||
elif 'price' in response[0] and response[0]['price'] is not None and response[0]['price'] < price:
|
||||
price = response[0]['price']
|
||||
return price
|
||||
|
||||
|
||||
class Property(Versionable):
|
||||
"""
|
||||
@@ -466,8 +410,6 @@ class ItemVariation(Versionable):
|
||||
values by creating an ItemVariation object for them with active set to
|
||||
False.
|
||||
|
||||
Restrictions can be not only set to items but also directly to variations.
|
||||
|
||||
:param item: The item this variation belongs to
|
||||
:type item: Item
|
||||
:param values: A set of ``PropertyValue`` objects defining this variation
|
||||
@@ -531,31 +473,6 @@ class ItemVariation(Versionable):
|
||||
vd['variation'] = self
|
||||
return vd
|
||||
|
||||
def check_restrictions(self) -> Union[bool, Decimal]:
|
||||
"""
|
||||
This method is used to determine whether this ItemVariation is restricted
|
||||
in sale by any restriction plugins.
|
||||
|
||||
:returns:
|
||||
|
||||
* ``False``, if the item is unavailable
|
||||
* the item's price, otherwise
|
||||
"""
|
||||
from pretix.base.signals import determine_availability
|
||||
|
||||
responses = determine_availability.send(
|
||||
self.item.event, item=self.item,
|
||||
variations=[self.to_variation_dict()], context=None,
|
||||
cache=self.item.event.get_cache()
|
||||
)
|
||||
price = self.default_price if self.default_price is not None else self.item.default_price
|
||||
for rec, response in responses:
|
||||
if 'available' in response[0] and not response[0]['available']:
|
||||
return False
|
||||
elif 'price' in response[0] and response[0]['price'] is not None and response[0]['price'] < price:
|
||||
price = response[0]['price']
|
||||
return price
|
||||
|
||||
def add_values_from_string(self, pk):
|
||||
"""
|
||||
Add values to this ItemVariation using a serialized string of the form
|
||||
@@ -672,47 +589,6 @@ class Question(Versionable):
|
||||
self.event.get_cache().clear()
|
||||
|
||||
|
||||
class BaseRestriction(Versionable):
|
||||
"""
|
||||
A restriction is the abstract concept of a rule that limits the availability
|
||||
of Items or ItemVariations. This model is just an abstract base class to be
|
||||
extended by restriction plugins.
|
||||
"""
|
||||
event = VersionedForeignKey(
|
||||
Event,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="restrictions_%(app_label)s_%(class)s",
|
||||
verbose_name=_("Event"),
|
||||
)
|
||||
item = VersionedForeignKey(
|
||||
Item,
|
||||
blank=True, null=True,
|
||||
verbose_name=_("Item"),
|
||||
related_name="restrictions_%(app_label)s_%(class)s",
|
||||
)
|
||||
variations = VariationsField(
|
||||
'pretixbase.ItemVariation',
|
||||
blank=True,
|
||||
verbose_name=_("Variations"),
|
||||
related_name="restrictions_%(app_label)s_%(class)s",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
verbose_name = _("Restriction")
|
||||
verbose_name_plural = _("Restrictions")
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
|
||||
|
||||
class Quota(Versionable):
|
||||
"""
|
||||
A quota is a "pool of tickets". It is there to limit the number of items
|
||||
|
||||
@@ -90,15 +90,10 @@ def _add_items(event: Event, items: List[Tuple[str, Optional[str], int]],
|
||||
item = items_cache[i[0]]
|
||||
variation = variations_cache[i[1]] if i[1] is not None else None
|
||||
|
||||
# Execute restriction plugins to check whether they (a) change the price or
|
||||
# (b) make the item/variation unavailable. If neither is the case, check_restriction
|
||||
# will correctly return the default price
|
||||
price = item.check_restrictions() if variation is None else variation.check_restrictions()
|
||||
|
||||
# Fetch all quotas. If there are no quotas, this item is not allowed to be sold.
|
||||
quotas = list(item.quotas.all()) if variation is None else list(variation.quotas.all())
|
||||
|
||||
if price is False or len(quotas) == 0 or not item.active:
|
||||
if len(quotas) == 0 or not item.active:
|
||||
err = err or error_messages['unavailable']
|
||||
continue
|
||||
|
||||
@@ -121,11 +116,15 @@ def _add_items(event: Event, items: List[Tuple[str, Optional[str], int]],
|
||||
# Recreating
|
||||
cp = i[3].clone()
|
||||
cp.expires = expiry
|
||||
cp.price = price
|
||||
cp.price = item.default_price if variation is None else (
|
||||
variation.default_price if variation.default_price is not None else item.default_price)
|
||||
cp.save()
|
||||
else:
|
||||
CartPosition.objects.create(
|
||||
event=event, item=item, variation=variation, price=price, expires=expiry,
|
||||
event=event, item=item, variation=variation,
|
||||
price=item.default_price if variation is None else (
|
||||
variation.default_price if variation.default_price is not None else item.default_price),
|
||||
expires=expiry,
|
||||
cart_id=cart_id
|
||||
)
|
||||
return err
|
||||
|
||||
@@ -103,7 +103,8 @@ def _check_positions(event: Event, dt: datetime, positions: List[CartPosition]):
|
||||
if cp.expires >= dt:
|
||||
# Other checks are not necessary
|
||||
continue
|
||||
price = cp.item.check_restrictions() if cp.variation is None else cp.variation.check_restrictions()
|
||||
price = cp.item.default_price if cp.variation is None else (
|
||||
cp.variation.default_price if cp.variation.default_price is not None else cp.item.default_price)
|
||||
if price is False or len(quotas) == 0:
|
||||
err = err or error_messages['unavailable']
|
||||
cp.delete()
|
||||
|
||||
@@ -46,15 +46,6 @@ class EventPluginSignal(django.dispatch.Signal):
|
||||
responses.append((receiver, response))
|
||||
return responses
|
||||
|
||||
"""
|
||||
This signal is sent out every time some component of pretix wants to know whether a specific
|
||||
item or variation is available for sell. The item will only be sold, if all (active) receivers
|
||||
return a positive result (see plugin API documentation for details).
|
||||
"""
|
||||
determine_availability = EventPluginSignal(
|
||||
providing_args=["item", "variations", "context", "cache"]
|
||||
)
|
||||
|
||||
"""
|
||||
This signal is sent out to get all known payment providers. Receivers should return a
|
||||
subclass of pretix.base.payment.BasePaymentProvider
|
||||
|
||||
@@ -99,60 +99,6 @@ class TolerantFormsetModelForm(VersionedModelForm):
|
||||
return False
|
||||
|
||||
|
||||
class RestrictionForm(TolerantFormsetModelForm):
|
||||
"""
|
||||
The restriction form provides useful functionality for all forms
|
||||
representing a restriction instance. To be concret, this form does
|
||||
the necessary magic to make the 'variations' field work correctly
|
||||
and look beautiful.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'item' in kwargs:
|
||||
self.item = kwargs['item']
|
||||
del kwargs['item']
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'variations' in self.fields and isinstance(self.fields['variations'], VariationsField):
|
||||
self.fields['variations'].set_item(self.item)
|
||||
|
||||
|
||||
class RestrictionInlineFormset(forms.BaseInlineFormSet):
|
||||
"""
|
||||
This is the base class you should use for any formset you return
|
||||
from a ``restriction_formset`` signal receiver that contains
|
||||
RestrictionForm objects as its forms, as it correcly handles the
|
||||
necessary item parameter for the RestrictionForm. While this could
|
||||
be achieved with a regular formset, this also adds a
|
||||
``initialized_empty_form`` method which is the only way to correctly
|
||||
render a working empty form for a JavaScript-enabled restriction formset.
|
||||
"""
|
||||
|
||||
def __init__(self, data=None, files=None, instance=None,
|
||||
save_as_new=False, prefix=None, queryset=None, **kwargs):
|
||||
super().__init__(
|
||||
data, files, instance, save_as_new, prefix, queryset, **kwargs
|
||||
)
|
||||
if isinstance(self.instance, Item):
|
||||
self.queryset = self.queryset.as_of().prefetch_related("variations")
|
||||
|
||||
def initialized_empty_form(self):
|
||||
form = self.form(
|
||||
auto_id=self.auto_id,
|
||||
prefix=self.add_prefix('__prefix__'),
|
||||
empty_permitted=True,
|
||||
item=self.instance
|
||||
)
|
||||
self.add_fields(form, None)
|
||||
return form
|
||||
|
||||
def _construct_form(self, i, **kwargs):
|
||||
kwargs['item'] = self.instance
|
||||
return super()._construct_form(i, **kwargs)
|
||||
|
||||
class Meta:
|
||||
exclude = ['item']
|
||||
|
||||
|
||||
def selector(values, prop):
|
||||
# Given an iterable of PropertyValue objects, this will return a
|
||||
# list of their primary keys, ordered by the primary keys of the
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<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.identity %}">{% 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.identity %}">{% 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.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=object.identity %}">{% trans "Restrictions" %}</a></li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<h1>{% trans "Create product" %}</h1>
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
{% extends "pretixcontrol/item/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load formset_tags %}
|
||||
{% block inside %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
In this area, you can choose of a set of "restriction types" to restrict the availability of your product with
|
||||
certain conditions.
|
||||
{% endblocktrans %}</p>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% for set in formsets %}
|
||||
<fieldset>
|
||||
<legend>{{ set.title }}</legend>
|
||||
{% bootstrap_formset_errors set %}
|
||||
<p>{{ set.description }}</p>
|
||||
<div data-formset class="restriction-formset" data-formset-prefix="{{ set.formset.prefix }}">
|
||||
<div data-formset-body class="panel-group collapsible" id="accordion_{{ set.formset.prefix }}">
|
||||
{{ set.formset.management_form }}
|
||||
{% for f in set.formset %}
|
||||
<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{{ f.prefix }}">
|
||||
{{ set.title }}
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapse{{ f.prefix }}" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<div class="form-horizontal">
|
||||
{% bootstrap_form f layout="horizontal" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<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__prefix__">
|
||||
{% trans "New restriction" %}
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapse__prefix__" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<div class="form-horizontal">
|
||||
{% bootstrap_form set.formset.initialized_empty_form layout="horizontal" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<button type="button" class="btn btn-default" data-formset-add><i
|
||||
class="fa fa-plus"></i> {% trans "Add a new restriction" %}</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -30,8 +30,6 @@ urlpatterns = [
|
||||
url(r'^items/(?P<item>[0-9a-f-]+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
|
||||
url(r'^items/(?P<item>[0-9a-f-]+)/variations$', item.ItemVariations.as_view(),
|
||||
name='event.item.variations'),
|
||||
url(r'^items/(?P<item>[0-9a-f-]+)/restrictions$', item.ItemRestrictions.as_view(),
|
||||
name='event.item.restrictions'),
|
||||
url(r'^items/(?P<item>[0-9a-f-]+)/properties$', item.ItemProperties.as_view(),
|
||||
name='event.item.properties'),
|
||||
url(r'^items/(?P<item>[0-9a-f-]+)/up$', item.item_move_up, name='event.items.up'),
|
||||
|
||||
@@ -740,68 +740,6 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
|
||||
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()
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
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
|
||||
})
|
||||
|
||||
|
||||
class ItemDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = Item
|
||||
template_name = 'pretixcontrol/item/delete.html'
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix import __version__ as version
|
||||
from pretix.base.plugins import PluginType
|
||||
|
||||
|
||||
class TimeRestrictionApp(AppConfig):
|
||||
name = 'pretix.plugins.timerestriction'
|
||||
verbose_name = _("Time restriction")
|
||||
|
||||
class PretixPluginMeta:
|
||||
type = PluginType.RESTRICTION
|
||||
name = _("Restriction by time")
|
||||
author = _("the pretix team")
|
||||
version = version
|
||||
description = _("This plugin adds the possibility to restrict the sale " +
|
||||
"of a given product or variation to a certain timeframe " +
|
||||
"or change its price during a certain period.")
|
||||
|
||||
def ready(self):
|
||||
from . import signals # NOQA
|
||||
|
||||
default_app_config = 'pretix.plugins.timerestriction.TimeRestrictionApp'
|
||||
@@ -1,38 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import versions.models
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '__first__'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TimeRestriction',
|
||||
fields=[
|
||||
('id', models.CharField(serialize=False, max_length=36, primary_key=True)),
|
||||
('identity', models.CharField(max_length=36)),
|
||||
('version_start_date', models.DateTimeField()),
|
||||
('version_end_date', models.DateTimeField(null=True, blank=True, default=None)),
|
||||
('version_birth_date', models.DateTimeField()),
|
||||
('timeframe_from', models.DateTimeField(verbose_name='Start of time frame')),
|
||||
('timeframe_to', models.DateTimeField(verbose_name='End of time frame')),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=7, null=True, blank=True, verbose_name='Price in time frame')),
|
||||
('event', versions.models.VersionedForeignKey(verbose_name='Event', to='pretixbase.Event', related_name='restrictions_timerestriction_timerestriction')),
|
||||
('item', versions.models.VersionedForeignKey(related_name='restrictions_timerestriction_timerestriction', null=True, verbose_name='Item', to='pretixbase.Item', blank=True)),
|
||||
('variations', pretix.base.models.VariationsField(related_name='restrictions_timerestriction_timerestriction', blank=True, to='pretixbase.ItemVariation', verbose_name='Variations')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Restrictions',
|
||||
'abstract': False,
|
||||
'verbose_name': 'Restriction',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import BaseRestriction
|
||||
|
||||
|
||||
class TimeRestriction(BaseRestriction):
|
||||
"""
|
||||
This restriction makes an item or variation only available
|
||||
within a given time frame. The price of the item can be modified
|
||||
during this time frame.
|
||||
"""
|
||||
|
||||
timeframe_from = models.DateTimeField(
|
||||
verbose_name=_("Start of time frame"),
|
||||
)
|
||||
timeframe_to = models.DateTimeField(
|
||||
verbose_name=_("End of time frame"),
|
||||
)
|
||||
price = models.DecimalField(
|
||||
null=True, blank=True,
|
||||
max_digits=7, decimal_places=2,
|
||||
verbose_name=_("Price in time frame"),
|
||||
)
|
||||
@@ -1,154 +0,0 @@
|
||||
from django.dispatch import receiver
|
||||
from django.forms.models import inlineformset_factory
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Item
|
||||
from pretix.base.signals import determine_availability
|
||||
from pretix.control.forms import RestrictionForm, RestrictionInlineFormset
|
||||
from pretix.control.signals import restriction_formset
|
||||
|
||||
from .models import TimeRestriction
|
||||
|
||||
|
||||
# The maximum validity of our cached values is the next date, one of our
|
||||
# timeframe_from or tiemframe_to actions happens
|
||||
def timediff(restrictions):
|
||||
for r in restrictions:
|
||||
if r.timeframe_from >= now():
|
||||
yield (r.timeframe_from - now()).total_seconds()
|
||||
if r.timeframe_to >= now():
|
||||
yield (r.timeframe_to - now()).total_seconds()
|
||||
|
||||
|
||||
@receiver(determine_availability, dispatch_uid="restriction_time")
|
||||
def availability_handler(sender, **kwargs):
|
||||
# Handle the signal's input arguments
|
||||
item = kwargs['item']
|
||||
variations = kwargs['variations']
|
||||
cache = kwargs['cache']
|
||||
context = kwargs['context'] # NOQA
|
||||
|
||||
# Fetch all restriction objects applied to this item
|
||||
restrictions = list(TimeRestriction.objects.current.filter(
|
||||
item=item,
|
||||
).prefetch_related('variations'))
|
||||
|
||||
# If we do not know anything about this item, we are done here.
|
||||
if len(restrictions) == 0:
|
||||
return variations
|
||||
|
||||
# IMPORTANT:
|
||||
# We need to make a two-level deep copy of the variations list before we
|
||||
# modify it, becuase we need to to copy the dictionaries. Otherwise, we'll
|
||||
# interfere with other plugins.
|
||||
variations = [d.copy() for d in variations]
|
||||
|
||||
try:
|
||||
cache_validity = min(timediff(restrictions))
|
||||
except ValueError:
|
||||
# empty sequence
|
||||
# If we get here, there are restrictions available but nothing will
|
||||
# change about them any more. If it were not for the case of no
|
||||
# restriction for the base item but restrictions for special
|
||||
# variations, we could quit here with 'item not available'.
|
||||
cache_validity = 3600
|
||||
|
||||
# Walk through all variations we are asked for
|
||||
for v in variations:
|
||||
var_restrictions = []
|
||||
for restriction in restrictions:
|
||||
applied_to = list(restriction.variations.current.all())
|
||||
|
||||
# Only take this restriction into consideration if it
|
||||
# is directly applied to this variation or if the item
|
||||
# has no variations
|
||||
if v.empty() or ('variation' in v and v['variation'] in applied_to):
|
||||
var_restrictions.append(restriction)
|
||||
|
||||
if not var_restrictions:
|
||||
v['available'] = True
|
||||
v['price'] = None
|
||||
continue
|
||||
|
||||
# If this point is reached, there ARE time restrictions for this item
|
||||
# Therefore, it is only available inside one of the timeframes, but not
|
||||
# without any timeframe.
|
||||
available = False
|
||||
|
||||
# Make up some unique key for this variation
|
||||
cachekey = 'timerestriction:%s:%s' % (
|
||||
item.identity,
|
||||
v.identify(),
|
||||
)
|
||||
|
||||
# Fetch from cache, if available
|
||||
cached = cache.get(cachekey)
|
||||
if cached is not None:
|
||||
v['available'] = (cached.split(":")[0] == 'True')
|
||||
try:
|
||||
v['price'] = float(cached.split(":")[1])
|
||||
except ValueError:
|
||||
v['price'] = None
|
||||
continue
|
||||
|
||||
# Walk through all restriction objects applied to this item
|
||||
prices = []
|
||||
for restriction in var_restrictions:
|
||||
if restriction.timeframe_from <= now() <= restriction.timeframe_to:
|
||||
# Selling this item is currently possible
|
||||
available = True
|
||||
prices.append(restriction.price)
|
||||
|
||||
# Use the lowest of all prices set by restrictions
|
||||
prices = [p for p in prices if p is not None]
|
||||
price = min(prices) if prices else None
|
||||
|
||||
v['available'] = available
|
||||
v['price'] = price
|
||||
cache.set(
|
||||
cachekey,
|
||||
'%s:%s' % (
|
||||
'True' if available else 'False',
|
||||
str(price) if price else ''
|
||||
),
|
||||
cache_validity
|
||||
)
|
||||
|
||||
return variations
|
||||
|
||||
|
||||
class TimeRestrictionForm(RestrictionForm):
|
||||
|
||||
class Meta:
|
||||
model = TimeRestriction
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'variations',
|
||||
'timeframe_from',
|
||||
'timeframe_to',
|
||||
'price',
|
||||
]
|
||||
|
||||
|
||||
@receiver(restriction_formset, dispatch_uid="restriction_time_formset")
|
||||
def formset_handler(sender, **kwargs):
|
||||
formset = inlineformset_factory(
|
||||
Item,
|
||||
TimeRestriction,
|
||||
formset=RestrictionInlineFormset,
|
||||
form=TimeRestrictionForm,
|
||||
can_order=False,
|
||||
can_delete=True,
|
||||
extra=0,
|
||||
)
|
||||
|
||||
return {
|
||||
'title': _('Restriction by time'),
|
||||
'formsetclass': formset,
|
||||
'prefix': 'timerestriction',
|
||||
'description': 'If you use this restriction type, the system will only sell variations which are covered '
|
||||
'by at least one of the timeframes you define below. You can also change the price of '
|
||||
'variations for within the given timeframe. Please note, that if you change the price of '
|
||||
'variations here, this will overrule the price set in the "Variations" section.'
|
||||
}
|
||||
@@ -149,7 +149,6 @@ INSTALLED_APPS = [
|
||||
'compressor',
|
||||
'bootstrap3',
|
||||
'djangoformsetjs',
|
||||
'pretix.plugins.timerestriction',
|
||||
'pretix.plugins.banktransfer',
|
||||
'pretix.plugins.stripe',
|
||||
'pretix.plugins.paypal',
|
||||
|
||||
Reference in New Issue
Block a user