mirror of
https://github.com/pretix/pretix.git
synced 2026-05-10 16:04:02 +00:00
Enforce start and end time of presale (#15)
This commit is contained in:
@@ -482,6 +482,20 @@ class Event(Versionable):
|
|||||||
"""
|
"""
|
||||||
return SettingsProxy(self, type=EventSetting, parent=self.organizer)
|
return SettingsProxy(self, type=EventSetting, parent=self.organizer)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def presale_has_ended(self):
|
||||||
|
if self.presale_end and now() > self.presale_end:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def presale_is_running(self):
|
||||||
|
if self.presale_start and now() < self.presale_start:
|
||||||
|
return False
|
||||||
|
if self.presale_end and now() > self.presale_end:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class EventPermission(Versionable):
|
class EventPermission(Versionable):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ DEFAULTS = {
|
|||||||
'default': 'True',
|
'default': 'True',
|
||||||
'type': bool
|
'type': bool
|
||||||
},
|
},
|
||||||
|
'presale_start_show_date': {
|
||||||
|
'default': 'True',
|
||||||
|
'type': bool
|
||||||
|
},
|
||||||
|
'show_items_outside_presale_period': {
|
||||||
|
'default': 'True',
|
||||||
|
'type': bool
|
||||||
|
},
|
||||||
'timezone': {
|
'timezone': {
|
||||||
'default': settings.TIME_ZONE,
|
'default': settings.TIME_ZONE,
|
||||||
'type': str
|
'type': str
|
||||||
|
|||||||
@@ -28,7 +28,9 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Timeline" %}</legend>
|
<legend>{% trans "Timeline" %}</legend>
|
||||||
{% bootstrap_field form.presale_start layout="horizontal" %}
|
{% bootstrap_field form.presale_start layout="horizontal" %}
|
||||||
|
{% bootstrap_field sform.presale_start_show_date layout="horizontal" %}
|
||||||
{% bootstrap_field form.presale_end layout="horizontal" %}
|
{% bootstrap_field form.presale_end layout="horizontal" %}
|
||||||
|
{% bootstrap_field sform.show_items_outside_presale_period layout="horizontal" %}
|
||||||
{% bootstrap_field sform.payment_term_days layout="horizontal" %}
|
{% bootstrap_field sform.payment_term_days layout="horizontal" %}
|
||||||
{% bootstrap_field sform.payment_term_last layout="horizontal" %}
|
{% bootstrap_field sform.payment_term_last layout="horizontal" %}
|
||||||
{% bootstrap_field sform.payment_term_accept_late layout="horizontal" %}
|
{% bootstrap_field sform.payment_term_accept_late layout="horizontal" %}
|
||||||
|
|||||||
@@ -55,6 +55,16 @@ class EventSettingsForm(SettingsForm):
|
|||||||
label='Payment term in days',
|
label='Payment term in days',
|
||||||
help_text=_("The number of days after placing an order the user has to pay to preserve his reservation."),
|
help_text=_("The number of days after placing an order the user has to pay to preserve his reservation."),
|
||||||
)
|
)
|
||||||
|
show_items_outside_presale_period = forms.BooleanField(
|
||||||
|
label=_("Show items outside presale period"),
|
||||||
|
help_text=_("Show item details before presale has started and after presale has ended"),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
presale_start_show_date = forms.BooleanField(
|
||||||
|
label=_("Show start date"),
|
||||||
|
help_text=_("Show the presale start date before presale has started"),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
payment_term_last = forms.DateTimeField(
|
payment_term_last = forms.DateTimeField(
|
||||||
label='Last date of payments',
|
label='Last date of payments',
|
||||||
help_text=_("The last date any payments are accepted. This has precedence over the number of "
|
help_text=_("The last date any payments are accepted. This has precedence over the number of "
|
||||||
|
|||||||
@@ -30,97 +30,118 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post"
|
{% if not event.presale_is_running %}
|
||||||
action="{% url "presale:event.cart.add" organizer=request.event.organizer.slug event=request.event.slug %}?next={{ request.path_info|urlencode }}">
|
<div class="alert alert-info">
|
||||||
{% csrf_token %}
|
{% if event.presale_has_ended %}
|
||||||
{% for tup in items_by_category %}
|
{% blocktrans trimmed %}
|
||||||
<section>
|
The presale period for this event is over.
|
||||||
{% if tup.0 %}<h3>{{ tup.0.name }}</h3>{% endif %}
|
{% endblocktrans %}
|
||||||
{% for item in tup.1 %}
|
{% elif event.settings.presale_start_show_date %}
|
||||||
{% if item.has_variations %}
|
{% blocktrans trimmed with date=event.presale_start|date time=event.presale_start|time %}
|
||||||
<div class="item-with-variations">
|
The presale for this event will start on {{ date }} at {{ time }}.
|
||||||
<div class="row-fluid product-row headline">
|
{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
The presale for this event has not yet started.
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if event.presale_is_running or event.settings.show_items_outside_presale_period %}
|
||||||
|
<form method="post"
|
||||||
|
action="{% url "presale:event.cart.add" organizer=request.event.organizer.slug event=request.event.slug %}?next={{ request.path_info|urlencode }}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for tup in items_by_category %}
|
||||||
|
<section>
|
||||||
|
{% if tup.0 %}<h3>{{ tup.0.name }}</h3>{% endif %}
|
||||||
|
{% for item in tup.1 %}
|
||||||
|
{% if item.has_variations %}
|
||||||
|
<div class="item-with-variations">
|
||||||
|
<div class="row-fluid product-row headline">
|
||||||
|
<div class="col-md-8 col-xs-12">
|
||||||
|
<a href="javascript:void();" data-toggle="variations">
|
||||||
|
<strong>{{ item.name }}</strong>
|
||||||
|
</a>
|
||||||
|
{% if item.short_description %}<p>{{ item.short_description }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 col-xs-6 price">
|
||||||
|
{% blocktrans trimmed with minprice=item.min_price|floatformat:2 currency=event.currency %}
|
||||||
|
from {{ currency }} {{ minprice }}
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 col-xs-6 availability-box">
|
||||||
|
<a href="javascript:void();" data-toggle="variations" class="js-only">
|
||||||
|
{% trans "Show variants" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
<div class="variations">
|
||||||
|
{% for var in item.available_variations %}
|
||||||
|
<div class="row-fluid product-row variation">
|
||||||
|
<div class="col-md-8 col-xs-12">
|
||||||
|
{{ var }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 col-xs-6 price">
|
||||||
|
{{ event.currency }} {{ var.price|floatformat:2 }}
|
||||||
|
{% if item.tax_rate %}
|
||||||
|
<br /><small>{% blocktrans trimmed with rate=item.tax_rate %}
|
||||||
|
incl. {{ rate }}% taxes
|
||||||
|
{% endblocktrans %}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if var.cached_availability.0 == 100 %}
|
||||||
|
<div class="col-md-2 col-xs-6 availability-box available">
|
||||||
|
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
|
||||||
|
max="{{ var.cached_availability.1 }}"
|
||||||
|
name="variation_{{ item.identity }}_{{ var.variation.identity }}">
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% include "pretixpresale/event/fragment_availability.html" with avail=var.cached_availability.0 %}
|
||||||
|
{% endif %}
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="row-fluid product-row simple">
|
||||||
<div class="col-md-8 col-xs-12">
|
<div class="col-md-8 col-xs-12">
|
||||||
<a href="javascript:void();" data-toggle="variations">
|
<strong>{{ item.name }}</strong>
|
||||||
<strong>{{ item.name }}</strong>
|
|
||||||
</a>
|
|
||||||
{% if item.short_description %}<p>{{ item.short_description }}</p>{% endif %}
|
{% if item.short_description %}<p>{{ item.short_description }}</p>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 col-xs-6 price">
|
<div class="col-md-2 col-xs-6 price">
|
||||||
{% blocktrans trimmed with minprice=item.min_price|floatformat:2 currency=event.currency %}
|
{{ event.currency }} {{ item.price }}
|
||||||
from {{ currency }} {{ minprice }}
|
{% if item.tax_rate %}
|
||||||
{% endblocktrans %}
|
<br /><small>{% blocktrans trimmed with rate=item.tax_rate %}
|
||||||
</div>
|
incl. {{ rate }}% taxes
|
||||||
<div class="col-md-2 col-xs-6 availability-box">
|
{% endblocktrans %}</small>
|
||||||
<a href="javascript:void();" data-toggle="variations" class="js-only">
|
{% endif %}
|
||||||
{% trans "Show variants" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% if item.cached_availability.0 == 100 %}
|
||||||
|
<div class="col-md-2 col-xs-6 availability-box available">
|
||||||
|
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
|
||||||
|
max="{{ item.cached_availability.1 }}" name="item_{{ item.identity }}">
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% include "pretixpresale/event/fragment_availability.html" with avail=item.cached_availability.0 %}
|
||||||
|
{% endif %}
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="variations">
|
{% endif %}
|
||||||
{% for var in item.available_variations %}
|
{% endfor %}
|
||||||
<div class="row-fluid product-row variation">
|
</section>
|
||||||
<div class="col-md-8 col-xs-12">
|
{% endfor %}
|
||||||
{{ var }}
|
{% if event.presale_is_running %}
|
||||||
</div>
|
<div class="row-fluid checkout-button-row">
|
||||||
<div class="col-md-2 col-xs-6 price">
|
<div class="col-md-4 col-md-offset-8">
|
||||||
{{ event.currency }} {{ var.price|floatformat:2 }}
|
<button class="btn btn-block btn-primary btn-lg" type="submit">
|
||||||
{% if item.tax_rate %}
|
<i class="fa fa-shopping-cart"></i> {% trans "Add to cart" %}
|
||||||
<br /><small>{% blocktrans trimmed with rate=item.tax_rate %}
|
</button>
|
||||||
incl. {{ rate }}% taxes
|
</div>
|
||||||
{% endblocktrans %}</small>
|
<div class="clearfix"></div>
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
{% if var.cached_availability.0 == 100 %}
|
</form>
|
||||||
<div class="col-md-2 col-xs-6 availability-box available">
|
{% endif %}
|
||||||
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
|
|
||||||
max="{{ var.cached_availability.1 }}"
|
|
||||||
name="variation_{{ item.identity }}_{{ var.variation.identity }}">
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{% include "pretixpresale/event/fragment_availability.html" with avail=var.cached_availability.0 %}
|
|
||||||
{% endif %}
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="row-fluid product-row simple">
|
|
||||||
<div class="col-md-8 col-xs-12">
|
|
||||||
<strong>{{ item.name }}</strong>
|
|
||||||
{% if item.short_description %}<p>{{ item.short_description }}</p>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2 col-xs-6 price">
|
|
||||||
{{ event.currency }} {{ item.price }}
|
|
||||||
{% if item.tax_rate %}
|
|
||||||
<br /><small>{% blocktrans trimmed with rate=item.tax_rate %}
|
|
||||||
incl. {{ rate }}% taxes
|
|
||||||
{% endblocktrans %}</small>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if item.cached_availability.0 == 100 %}
|
|
||||||
<div class="col-md-2 col-xs-6 availability-box available">
|
|
||||||
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
|
|
||||||
max="{{ item.cached_availability.1 }}" name="item_{{ item.identity }}">
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{% include "pretixpresale/event/fragment_availability.html" with avail=item.cached_availability.0 %}
|
|
||||||
{% endif %}
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</section>
|
|
||||||
{% endfor %}
|
|
||||||
<div class="row-fluid checkout-button-row">
|
|
||||||
<div class="col-md-4 col-md-offset-8">
|
|
||||||
<button class="btn btn-block btn-primary btn-lg" type="submit">
|
|
||||||
<i class="fa fa-shopping-cart"></i> {% trans "Add to cart" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -4,8 +4,10 @@ from django.contrib.auth.views import redirect_to_login
|
|||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
||||||
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 pretix.base.models import CartPosition
|
from pretix.base.models import CartPosition
|
||||||
from pretix.base.signals import register_payment_providers
|
from pretix.base.signals import register_payment_providers
|
||||||
|
|||||||
@@ -99,6 +99,13 @@ class CartAdd(EventViewMixin, CartActionMixin, View):
|
|||||||
self.msg_some_unavailable = False
|
self.msg_some_unavailable = False
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
if request.event.presale_start and now() < request.event.presale_start:
|
||||||
|
messages.error(request, _('The presale period not yet started.'))
|
||||||
|
return redirect(self.get_failure_url())
|
||||||
|
if request.event.presale_end and now() > request.event.presale_end:
|
||||||
|
messages.error(request, _('The presale period has ended.'))
|
||||||
|
return redirect(self.get_failure_url())
|
||||||
|
|
||||||
self.items = self._items_from_post_data()
|
self.items = self._items_from_post_data()
|
||||||
|
|
||||||
# We do not use EventLoginRequiredMixin here, as we want to store stuff into the
|
# We do not use EventLoginRequiredMixin here, as we want to store stuff into the
|
||||||
|
|||||||
@@ -355,10 +355,11 @@ class OrderConfirm(EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin, Ch
|
|||||||
quota_ok = False
|
quota_ok = False
|
||||||
break
|
break
|
||||||
if quota_ok:
|
if quota_ok:
|
||||||
cp = cp.clone()
|
if not self.request.event.presale_end or now() < self.request.event.presale_end:
|
||||||
cartpos[i] = cp
|
cp = cp.clone()
|
||||||
cp.expires = now() + timedelta(minutes=self.request.event.settings.get('reservation_time', as_type=int))
|
cartpos[i] = cp
|
||||||
cp.save()
|
cp.expires = now() + timedelta(minutes=self.request.event.settings.get('reservation_time', as_type=int))
|
||||||
|
cp.save()
|
||||||
else:
|
else:
|
||||||
cp.delete() # Sorry!
|
cp.delete() # Sorry!
|
||||||
if not self.msg_some_unavailable: # Everything went well
|
if not self.msg_some_unavailable: # Everything went well
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import time
|
import time
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
from pretix.base.models import Item, Organizer, Event, ItemCategory, Quota, Property, PropertyValue, ItemVariation, User
|
from pretix.base.models import Item, Organizer, Event, ItemCategory, Quota, Property, PropertyValue, ItemVariation, User
|
||||||
from tests.base import BrowserTest
|
from tests.base import BrowserTest
|
||||||
@@ -186,3 +187,56 @@ class LoginTest(EventTestMixin, TestCase):
|
|||||||
'/%s/%s/login' % (self.orga.slug, self.event.slug),
|
'/%s/%s/login' % (self.orga.slug, self.event.slug),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class DeadlineTest(EventTestMixin, TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
def test_not_yet_started(self):
|
||||||
|
self.event.presale_start = now() + datetime.timedelta(days=1)
|
||||||
|
self.event.save()
|
||||||
|
response = self.client.get(
|
||||||
|
'/%s/%s/' % (self.orga.slug, self.event.slug)
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn('alert-info', response.rendered_content)
|
||||||
|
self.assertNotIn('checkout-button-row', response.rendered_content)
|
||||||
|
response = self.client.post(
|
||||||
|
'/%s/%s/cart/add' % (self.orga.slug, self.event.slug),
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
self.assertIn('alert-danger', response.rendered_content)
|
||||||
|
self.assertIn('not yet started', response.rendered_content)
|
||||||
|
|
||||||
|
def test_over(self):
|
||||||
|
self.event.presale_end = now() - datetime.timedelta(days=1)
|
||||||
|
self.event.save()
|
||||||
|
response = self.client.get(
|
||||||
|
'/%s/%s/' % (self.orga.slug, self.event.slug)
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn('alert-info', response.rendered_content)
|
||||||
|
self.assertNotIn('checkout-button-row', response.rendered_content)
|
||||||
|
response = self.client.post(
|
||||||
|
'/%s/%s/cart/add' % (self.orga.slug, self.event.slug),
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
self.assertIn('alert-danger', response.rendered_content)
|
||||||
|
self.assertIn('is over', response.rendered_content)
|
||||||
|
|
||||||
|
def test_in_time(self):
|
||||||
|
self.event.presale_start = now() - datetime.timedelta(days=1)
|
||||||
|
self.event.presale_end = now() + datetime.timedelta(days=1)
|
||||||
|
self.event.save()
|
||||||
|
response = self.client.get(
|
||||||
|
'/%s/%s/' % (self.orga.slug, self.event.slug)
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertNotIn('alert-info', response.rendered_content)
|
||||||
|
self.assertIn('checkout-button-row', response.rendered_content)
|
||||||
|
response = self.client.post(
|
||||||
|
'/%s/%s/cart/add' % (self.orga.slug, self.event.slug)
|
||||||
|
)
|
||||||
|
self.assertNotEqual(response.status_code, 403)
|
||||||
|
|||||||
Reference in New Issue
Block a user