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 3d899decf5..70b2d2cc74 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 c0cf2f5adf..2a4d35a9c0 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 a06088e568..cb976b652e 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 0000000000..3eefeb8ed3
--- /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 7eb67dd1bd..cb025b73f8 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 9635e47827..74488e9945 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 43cae3c872..2d002bcf29 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 aa23ae6b9d..57d7e6766b 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 aa797ac8ec..217a238fe7 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 4c01075480..7d9ae7e648 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 %}
+
+
@@ -30,7 +51,6 @@
-
{% 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 787cd66532..1a126cbabc 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 %}
+
+
+