Re-added the ability to restrict a product sale by time

This commit is contained in:
Raphael Michel
2015-12-06 18:18:54 +01:00
parent 4a1122a862
commit e70485e7fb
9 changed files with 149 additions and 22 deletions

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0004_auto_20151024_0848'),
]
operations = [
migrations.AddField(
model_name='item',
name='available_from',
field=models.DateTimeField(null=True, help_text='This product will not be sold before the given date.', blank=True, verbose_name='Available from'),
),
migrations.AddField(
model_name='item',
name='available_until',
field=models.DateTimeField(null=True, help_text='This product will not be sold after the given date.', blank=True, verbose_name='Available to'),
),
]

View File

@@ -1,6 +1,5 @@
import sys
from datetime import datetime
from decimal import Decimal
from itertools import product
from django.db import models
@@ -8,7 +7,7 @@ from django.db.models import Q, Case, Count, Sum, When
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from typing import List, Tuple, Union
from typing import List, Tuple
from versions.models import VersionedForeignKey, VersionedManyToManyField
from pretix.base.i18n import I18nCharField, I18nTextField
@@ -98,6 +97,10 @@ class Item(Versionable):
:type admission: bool
:param picture: A product picture to be shown next to the product description.
:type picture: File
:param available_from: The date this product goes on sale
:type available_from: datetime
:param available_until: The date until when the product is on sale
:type available_until: datetime
"""
@@ -152,6 +155,16 @@ class Item(Versionable):
null=True, blank=True,
upload_to=itempicture_upload_to
)
available_from = models.DateTimeField(
verbose_name=_("Available from"),
null=True, blank=True,
help_text=_('This product will not be sold before the given date.')
)
available_until = models.DateTimeField(
verbose_name=_("Available until"),
null=True, blank=True,
help_text=_('This product will not be sold after the given date.')
)
class Meta:
verbose_name = _("Product")
@@ -171,6 +184,19 @@ class Item(Versionable):
if self.event:
self.event.get_cache().clear()
def is_available(self) -> bool:
"""
Returns whether this item is available according to its ``active`` flag
and its ``available_from`` and ``available_until`` fields
"""
if not self.active:
return False
if self.available_from and self.available_from > now():
return False
if self.available_until and self.available_until < now():
return False
return True
def get_all_variations(self, use_cache: bool=False) -> List[VariationDict]:
"""
This method returns a list containing all variations of this

View File

@@ -93,7 +93,7 @@ def _add_items(event: Event, items: List[Tuple[str, Optional[str], int]],
# 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 len(quotas) == 0 or not item.active:
if len(quotas) == 0 or not item.is_available():
err = err or error_messages['unavailable']
continue

View File

@@ -137,6 +137,8 @@ class ItemFormGeneral(VersionedModelForm):
'picture',
'default_price',
'tax_rate',
'available_from',
'available_until',
]

View File

@@ -18,6 +18,11 @@
{% bootstrap_field form.default_price layout="horizontal" %}
{% bootstrap_field form.tax_rate layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Availability" %}</legend>
{% bootstrap_field form.available_from layout="horizontal" %}
{% bootstrap_field form.available_until layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}

View File

@@ -1,6 +1,7 @@
import sys
from django.db.models import Count
from django.db.models import Q, Count
from django.utils.timezone import now
from django.views.generic import TemplateView
from pretix.presale.views import CartMixin, EventViewMixin
@@ -13,7 +14,9 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView):
context = super().get_context_data(**kwargs)
# Fetch all items
items = self.request.event.items.all().filter(
active=True
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
).select_related(
'category', # for re-grouping
).prefetch_related(

View File

@@ -425,6 +425,40 @@ class ItemCategoryTest(TestCase):
assert c2 > c1
class ItemTest(TestCase):
"""
This test case tests various methods around the item model.
"""
@classmethod
def setUpTestData(cls):
o = Organizer.objects.create(name='Dummy', slug='dummy')
cls.event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now(),
)
def test_is_available(self):
i = Item.objects.create(
event=self.event, name="Ticket", default_price=23,
active=True, available_until=now() + timedelta(days=1),
)
assert i.is_available()
i.available_from = now() - timedelta(days=1)
assert i.is_available()
i.available_from = now() + timedelta(days=1)
i.available_until = None
assert not i.is_available()
i.available_from = None
i.available_until = now() - timedelta(days=1)
assert not i.is_available()
i.available_from = None
i.available_until = None
assert i.is_available()
i.active = False
assert not i.is_available()
class CachedFileTestCase(TestCase):
def test_file_handling(self):
cf = CachedFile()

View File

@@ -1,12 +1,10 @@
import datetime
import time
from datetime import timedelta
from bs4 import BeautifulSoup
from django.conf import settings
from django.test import TestCase
from django.utils.timezone import now
from tests.base import BrowserTest
from pretix.base.models import (
CartPosition, Event, Item, ItemCategory, ItemVariation, Organizer,
@@ -158,6 +156,31 @@ class CartTest(CartTestMixin, TestCase):
self.assertIn('no longer available', doc.select('.alert-danger')[0].text)
self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).exists())
def test_in_time_available(self):
self.ticket.available_until = now() + timedelta(days=2)
self.ticket.available_from = now() - timedelta(days=2)
self.ticket.save()
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_' + self.ticket.identity: '1',
}, follow=True)
self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 1)
def test_no_longer_available(self):
self.ticket.available_until = now() - timedelta(days=2)
self.ticket.save()
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_' + self.ticket.identity: '1',
}, follow=True)
self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 0)
def test_not_yet_available(self):
self.ticket.available_from = now() + timedelta(days=2)
self.ticket.save()
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_' + self.ticket.identity: '1',
}, follow=True)
self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 0)
def test_max_items(self):
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
@@ -262,21 +285,6 @@ class CartTest(CartTestMixin, TestCase):
self.assertIn('no longer available', doc.select('.alert-danger')[0].text)
self.assertFalse(CartPosition.objects.current.filter(identity=cp1.identity).exists())
def test_restriction_ok(self):
self.event.plugins = 'tests.testdummy'
self.event.save()
self.event.settings.testdummy_available = 'yes'
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_' + self.ticket.identity: '1',
}, follow=True)
self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug),
target_status_code=200)
objs = list(CartPosition.objects.current.filter(cart_id=self.session_key, event=self.event))
self.assertEqual(len(objs), 1)
self.assertEqual(objs[0].item, self.ticket)
self.assertIsNone(objs[0].variation)
self.assertEqual(objs[0].price, 23)
def test_remove_simple(self):
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,

View File

@@ -54,6 +54,31 @@ class ItemDisplayTest(EventTestMixin, BrowserTest):
self.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug))
self.assertIn("Early-bird", self.driver.find_element_by_css_selector("section .product-row:first-child").text)
def test_timely_available(self):
q = Quota.objects.create(event=self.event, name='Quota', size=2)
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True,
available_until=now() + datetime.timedelta(days=2),
available_from=now() - datetime.timedelta(days=2))
q.items.add(item)
self.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug))
self.assertIn("Early-bird", self.driver.find_element_by_css_selector("body").text)
def test_no_longer_available(self):
q = Quota.objects.create(event=self.event, name='Quota', size=2)
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True,
available_until=now() - datetime.timedelta(days=2))
q.items.add(item)
self.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug))
self.assertNotIn("Early-bird", self.driver.find_element_by_css_selector("body").text)
def test_not_yet_available(self):
q = Quota.objects.create(event=self.event, name='Quota', size=2)
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True,
available_from=now() + datetime.timedelta(days=2))
q.items.add(item)
self.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug))
self.assertNotIn("Early-bird", self.driver.find_element_by_css_selector("body").text)
def test_simple_with_category(self):
c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0)
q = Quota.objects.create(event=self.event, name='Quota', size=2)