From a02ea45dba1ee1034d4e8c100f27cce9bf0a8b4e Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 16 Jul 2019 14:02:27 +0200 Subject: [PATCH] Allow quotas to "close" when once full (#1344) * Model * Some UI * API and logging * Permission check * Add tests * Move option around --- doc/api/resources/quotas.rst | 30 +++++++++++++++---- src/pretix/api/serializers/item.py | 2 +- src/pretix/api/views/item.py | 13 ++++++++ .../migrations/0128_auto_20190715_1510.py | 26 ++++++++++++++++ src/pretix/base/models/event.py | 5 ++++ src/pretix/base/models/items.py | 19 +++++++++++- src/pretix/control/forms/item.py | 3 +- src/pretix/control/logdisplay.py | 2 ++ .../items/fragment_quota_availability.html | 4 ++- .../templates/pretixcontrol/items/quota.html | 22 +++++++++++++- .../pretixcontrol/items/quota_edit.html | 6 ++++ .../templates/pretixcontrol/items/quotas.html | 2 +- src/pretix/control/views/item.py | 29 ++++++++++++++++++ src/tests/api/test_items.py | 28 ++++++++++++++++- src/tests/base/test_models.py | 18 +++++++++++ src/tests/control/test_items.py | 22 ++++++++++++++ 16 files changed, 219 insertions(+), 12 deletions(-) create mode 100644 src/pretix/base/migrations/0128_auto_20190715_1510.py diff --git a/doc/api/resources/quotas.rst b/doc/api/resources/quotas.rst index 3d899decf..70b2d2cc7 100644 --- a/doc/api/resources/quotas.rst +++ b/doc/api/resources/quotas.rst @@ -20,12 +20,22 @@ size integer The size of the items list of integers List of item IDs this quota acts on. variations list of integers List of item variation IDs this quota acts on. subevent integer ID of the date inside an event series this quota belongs to (or ``null``). +close_when_sold_out boolean If ``true``, the quota will "close" as soon as it is + sold out once. Even if tickets become available again, + they will not be sold unless the quota is set to open + again. +closed boolean Whether the quota is currently closed (see above + field). ===================================== ========================== ======================================================= .. versionchanged:: 1.10 The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added. +.. versionchanged:: 3.0 + + The attributes ``close_when_sold_out`` and ``closed`` have been added. + Endpoints --------- @@ -61,7 +71,9 @@ Endpoints "size": 200, "items": [1, 2], "variations": [1, 4, 5, 7], - "subevent": null + "subevent": null, + "close_when_sold_out": false, + "closed": false } ] } @@ -102,7 +114,9 @@ Endpoints "size": 200, "items": [1, 2], "variations": [1, 4, 5, 7], - "subevent": null + "subevent": null, + "close_when_sold_out": false, + "closed": false } :param organizer: The ``slug`` field of the organizer to fetch @@ -130,7 +144,9 @@ Endpoints "size": 200, "items": [1, 2], "variations": [1, 4, 5, 7], - "subevent": null + "subevent": null, + "close_when_sold_out": false, + "closed": false } **Example response**: @@ -147,7 +163,9 @@ Endpoints "size": 200, "items": [1, 2], "variations": [1, 4, 5, 7], - "subevent": null + "subevent": null, + "close_when_sold_out": false, + "closed": false } :param organizer: The ``slug`` field of the organizer of the event/item to create a quota for @@ -200,7 +218,9 @@ Endpoints 1, 2 ], - "subevent": null + "subevent": null, + "close_when_sold_out": false, + "closed": false } :param organizer: The ``slug`` field of the organizer to modify diff --git a/src/pretix/api/serializers/item.py b/src/pretix/api/serializers/item.py index c0cf2f5ad..2a4d35a9c 100644 --- a/src/pretix/api/serializers/item.py +++ b/src/pretix/api/serializers/item.py @@ -285,7 +285,7 @@ class QuotaSerializer(I18nAwareModelSerializer): class Meta: model = Quota - fields = ('id', 'name', 'size', 'items', 'variations', 'subevent') + fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out') def validate(self, data): data = super().validate(data) diff --git a/src/pretix/api/views/item.py b/src/pretix/api/views/item.py index a06088e56..cb976b652 100644 --- a/src/pretix/api/views/item.py +++ b/src/pretix/api/views/item.py @@ -473,6 +473,19 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet): # This costs us a few cycles on save, but avoids thousands of lines in our log. return + if original_data['closed'] is True and serializer.instance.closed is False: + serializer.instance.log_action( + 'pretix.event.quota.opened', + user=self.request.user, + auth=self.request.auth, + ) + elif original_data['closed'] is False and serializer.instance.closed is True: + serializer.instance.log_action( + 'pretix.event.quota.closed', + user=self.request.user, + auth=self.request.auth, + ) + serializer.instance.log_action( 'pretix.event.quota.changed', user=self.request.user, diff --git a/src/pretix/base/migrations/0128_auto_20190715_1510.py b/src/pretix/base/migrations/0128_auto_20190715_1510.py new file mode 100644 index 000000000..3eefeb8ed --- /dev/null +++ b/src/pretix/base/migrations/0128_auto_20190715_1510.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.1 on 2019-07-15 15:10 + +import django.db.models.deletion +from django.db import migrations, models + +import pretix.base.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0127_auto_20190711_0705'), + ] + + operations = [ + migrations.AddField( + model_name='quota', + name='close_when_sold_out', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='quota', + name='closed', + field=models.BooleanField(default=False), + ), + ] diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 7eb67dd1b..cb025b73f 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -518,6 +518,11 @@ class Event(EventMixin, LoggedModel): vars = list(q.variations.all()) q.pk = None q.event = self + q.cached_availability_state = None + q.cached_availability_number = None + q.cached_availability_paid_orders = None + q.cached_availability_time = None + q.closed = False q.save() for i in items: if i.pk in item_map: diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 9635e4782..74488e994 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -1269,6 +1269,15 @@ class Quota(LoggedModel): cached_availability_paid_orders = models.PositiveIntegerField(null=True, blank=True) cached_availability_time = models.DateTimeField(null=True, blank=True) + close_when_sold_out = models.BooleanField( + verbose_name=_('Close this quota permanently once it is sold out'), + help_text=_('If you enable this, when the quota is sold out once, no more tickets will be sold, ' + 'even if tickets become available again through cancellations or expiring orders. Of course, ' + 'you can always re-open it manually.'), + default=False + ) + closed = models.BooleanField(default=False) + objects = ScopedManager(organizer='event__organizer') class Meta: @@ -1334,6 +1343,11 @@ class Quota(LoggedModel): count_waitinglist=count_waitinglist): res = resp + if res[0] <= Quota.AVAILABILITY_ORDERED and self.close_when_sold_out and not self.closed: + self.closed = True + self.save(update_fields=['closed']) + self.log_action('pretix.event.quota.closed') + self.event.cache.delete('item_quota_cache') rewrite_cache = count_waitinglist and ( not self.cache_is_hot(now_dt) or res[0] > self.cached_availability_state @@ -1358,8 +1372,11 @@ class Quota(LoggedModel): _cache['_count_waitinglist'] = count_waitinglist return res - def _availability(self, now_dt: datetime=None, count_waitinglist=True): + def _availability(self, now_dt: datetime=None, count_waitinglist=True, ignore_closed=False): now_dt = now_dt or now() + if self.closed and not ignore_closed: + return Quota.AVAILABILITY_ORDERED, 0 + size_left = self.size if size_left is None: return Quota.AVAILABILITY_OK, None diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index 43cae3c87..2d002bcf2 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -169,7 +169,8 @@ class QuotaForm(I18nModelForm): fields = [ 'name', 'size', - 'subevent' + 'subevent', + 'close_when_sold_out' ] field_classes = { 'subevent': SafeModelChoiceField, diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index aa23ae6b9..57d7e6766 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -263,6 +263,8 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs): 'pretix.event.quota.added': _('The quota has been added.'), 'pretix.event.quota.deleted': _('The quota has been deleted.'), 'pretix.event.quota.changed': _('The quota has been changed.'), + 'pretix.event.quota.closed': _('The quota has closed.'), + 'pretix.event.quota.opened': _('The quota has been re-opened.'), 'pretix.event.category.added': _('The category has been added.'), 'pretix.event.category.deleted': _('The category has been deleted.'), 'pretix.event.category.changed': _('The category has been changed.'), diff --git a/src/pretix/control/templates/pretixcontrol/items/fragment_quota_availability.html b/src/pretix/control/templates/pretixcontrol/items/fragment_quota_availability.html index aa797ac8e..217a238fe 100644 --- a/src/pretix/control/templates/pretixcontrol/items/fragment_quota_availability.html +++ b/src/pretix/control/templates/pretixcontrol/items/fragment_quota_availability.html @@ -1,5 +1,7 @@ {% load i18n %} -{% if availability.0 == 10 %} +{% if closed %} + {% trans "Closed" %} +{% elif availability.0 == 10 %} {% trans "Sold out (pending orders)" %} {% elif availability.0 == 100 %} {% if availability.1 != None %} diff --git a/src/pretix/control/templates/pretixcontrol/items/quota.html b/src/pretix/control/templates/pretixcontrol/items/quota.html index 4c0107548..7d9ae7e64 100644 --- a/src/pretix/control/templates/pretixcontrol/items/quota.html +++ b/src/pretix/control/templates/pretixcontrol/items/quota.html @@ -20,6 +20,27 @@ {{ quota.subevent.name }} – {{ quota.subevent.get_date_range_display }}

{% endif %} + +
+ {% csrf_token %} + {% if quota.closed %} + {% if closed_and_sold_out %} +
+ + {% trans "This quota is sold out and closed. Even if tickets become available e.g. through cancellations, they will not become available again unless you manually re-open the quota on this page." %} +
+
+ {% else %} +
+ + {% trans "This quota is closed since it has been sold out before. Tickets are theoretically available, but will not be sold unless you manually re-open the quota." %} +
+
+ {% endif %} + {% endif %} +
{% trans "Usage overview" %} @@ -30,7 +51,6 @@
{% trans "Availability calculation" %} -
{% trans "Total quota" %}
diff --git a/src/pretix/control/templates/pretixcontrol/items/quota_edit.html b/src/pretix/control/templates/pretixcontrol/items/quota_edit.html index 787cd6653..1a126cbab 100644 --- a/src/pretix/control/templates/pretixcontrol/items/quota_edit.html +++ b/src/pretix/control/templates/pretixcontrol/items/quota_edit.html @@ -24,6 +24,8 @@ {% if form.subevent %} {% bootstrap_field form.subevent layout="control" %} {% endif %} + +
{% trans "Items" %}

{% blocktrans trimmed %} @@ -35,6 +37,10 @@

{% bootstrap_field form.itemvars layout="control" %}
+
+ {% trans "Advanced options" %} + {% bootstrap_field form.close_when_sold_out layout="control" %} +