forked from CGM_Public/pretix_original
Re-added the ability to restrict a product sale by time
This commit is contained in:
24
src/pretix/base/migrations/0005_auto_20151206_1652.py
Normal file
24
src/pretix/base/migrations/0005_auto_20151206_1652.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal
|
|
||||||
from itertools import product
|
from itertools import product
|
||||||
|
|
||||||
from django.db import models
|
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.functional import cached_property
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
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 versions.models import VersionedForeignKey, VersionedManyToManyField
|
||||||
|
|
||||||
from pretix.base.i18n import I18nCharField, I18nTextField
|
from pretix.base.i18n import I18nCharField, I18nTextField
|
||||||
@@ -98,6 +97,10 @@ class Item(Versionable):
|
|||||||
:type admission: bool
|
:type admission: bool
|
||||||
:param picture: A product picture to be shown next to the product description.
|
:param picture: A product picture to be shown next to the product description.
|
||||||
:type picture: File
|
: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,
|
null=True, blank=True,
|
||||||
upload_to=itempicture_upload_to
|
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:
|
class Meta:
|
||||||
verbose_name = _("Product")
|
verbose_name = _("Product")
|
||||||
@@ -171,6 +184,19 @@ class Item(Versionable):
|
|||||||
if self.event:
|
if self.event:
|
||||||
self.event.get_cache().clear()
|
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]:
|
def get_all_variations(self, use_cache: bool=False) -> List[VariationDict]:
|
||||||
"""
|
"""
|
||||||
This method returns a list containing all variations of this
|
This method returns a list containing all variations of this
|
||||||
|
|||||||
@@ -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.
|
# 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())
|
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']
|
err = err or error_messages['unavailable']
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ class ItemFormGeneral(VersionedModelForm):
|
|||||||
'picture',
|
'picture',
|
||||||
'default_price',
|
'default_price',
|
||||||
'tax_rate',
|
'tax_rate',
|
||||||
|
'available_from',
|
||||||
|
'available_until',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,11 @@
|
|||||||
{% bootstrap_field form.default_price layout="horizontal" %}
|
{% bootstrap_field form.default_price layout="horizontal" %}
|
||||||
{% bootstrap_field form.tax_rate layout="horizontal" %}
|
{% bootstrap_field form.tax_rate layout="horizontal" %}
|
||||||
</fieldset>
|
</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">
|
<div class="form-group submit-group">
|
||||||
<button type="submit" class="btn btn-primary btn-save">
|
<button type="submit" class="btn btn-primary btn-save">
|
||||||
{% trans "Save" %}
|
{% trans "Save" %}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import sys
|
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 django.views.generic import TemplateView
|
||||||
|
|
||||||
from pretix.presale.views import CartMixin, EventViewMixin
|
from pretix.presale.views import CartMixin, EventViewMixin
|
||||||
@@ -13,7 +14,9 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView):
|
|||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
# Fetch all items
|
# Fetch all items
|
||||||
items = self.request.event.items.all().filter(
|
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(
|
).select_related(
|
||||||
'category', # for re-grouping
|
'category', # for re-grouping
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
|
|||||||
@@ -425,6 +425,40 @@ class ItemCategoryTest(TestCase):
|
|||||||
assert c2 > c1
|
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):
|
class CachedFileTestCase(TestCase):
|
||||||
def test_file_handling(self):
|
def test_file_handling(self):
|
||||||
cf = CachedFile()
|
cf = CachedFile()
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import time
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from tests.base import BrowserTest
|
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Event, Item, ItemCategory, ItemVariation, Organizer,
|
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.assertIn('no longer available', doc.select('.alert-danger')[0].text)
|
||||||
self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).exists())
|
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):
|
def test_max_items(self):
|
||||||
CartPosition.objects.create(
|
CartPosition.objects.create(
|
||||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
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.assertIn('no longer available', doc.select('.alert-danger')[0].text)
|
||||||
self.assertFalse(CartPosition.objects.current.filter(identity=cp1.identity).exists())
|
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):
|
def test_remove_simple(self):
|
||||||
CartPosition.objects.create(
|
CartPosition.objects.create(
|
||||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
|
|||||||
@@ -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.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)
|
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):
|
def test_simple_with_category(self):
|
||||||
c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0)
|
c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0)
|
||||||
q = Quota.objects.create(event=self.event, name='Quota', size=2)
|
q = Quota.objects.create(event=self.event, name='Quota', size=2)
|
||||||
|
|||||||
Reference in New Issue
Block a user