forked from CGM_Public/pretix_original
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4abfb0bdf | ||
|
|
d32bd717b7 | ||
|
|
6e6b75d55e | ||
|
|
50b5f760bb | ||
|
|
9ab2e61c31 | ||
|
|
4876a0b61f | ||
|
|
56bbcb65c3 | ||
|
|
5bb1cb498f | ||
|
|
6bf23b0fdd | ||
|
|
5deb1a8c69 | ||
|
|
1523137300 | ||
|
|
04ef097eb1 | ||
|
|
a5d4434a64 |
@@ -94,7 +94,9 @@ If you want the user to return to your application after the payment is complete
|
|||||||
"Plugins". Enable the plugin "Redirection from order page". Then, go to the new page "Settings", then "Redirection".
|
"Plugins". Enable the plugin "Redirection from order page". Then, go to the new page "Settings", then "Redirection".
|
||||||
Enter the base URL of your web application. This will allow you to redirect to pages under this base URL later on.
|
Enter the base URL of your web application. This will allow you to redirect to pages under this base URL later on.
|
||||||
For example, if you want users to be redirected to ``https://example.org/order/return?tx_id=1234``, you could now
|
For example, if you want users to be redirected to ``https://example.org/order/return?tx_id=1234``, you could now
|
||||||
either enter ``https://example.org`` or ``https://example.org/order/``.
|
either enter ``https://example.org/order/`` or ``https://example.org/``.
|
||||||
|
Please note that in the latter case the trailing slash is required, ``https://example.org`` is not allowed to prevent.
|
||||||
|
Only base URLs with a secure (``https://``) or local (``http://localhost``) origin are permitted.
|
||||||
|
|
||||||
The user will be redirected back to your page instead of pretix' order confirmation page after the payment,
|
The user will be redirected back to your page instead of pretix' order confirmation page after the payment,
|
||||||
**regardless of whether it was successful or not**. We will append an ``error=…`` query parameter with an error
|
**regardless of whether it was successful or not**. We will append an ``error=…`` query parameter with an error
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ internal_id string Can be used for
|
|||||||
contact_name string Contact person (or ``null``)
|
contact_name string Contact person (or ``null``)
|
||||||
contact_name_parts object of strings Decomposition of contact name (i.e. given name, family name)
|
contact_name_parts object of strings Decomposition of contact name (i.e. given name, family name)
|
||||||
contact_email string Contact person email address (or ``null``)
|
contact_email string Contact person email address (or ``null``)
|
||||||
|
contact_cc_email string Copy email addresses, can be multiple separated by comma (or ``null``)
|
||||||
booth string Booth number (or ``null``). Maximum 100 characters.
|
booth string Booth number (or ``null``). Maximum 100 characters.
|
||||||
locale string Locale for communication with the exhibitor.
|
locale string Locale for communication with the exhibitor.
|
||||||
access_code string Access code for the exhibitor to access their data or use the lead scanning app (read-only).
|
access_code string Access code for the exhibitor to access their data or use the lead scanning app (read-only).
|
||||||
@@ -109,6 +110,7 @@ Endpoints
|
|||||||
"title": "Dr"
|
"title": "Dr"
|
||||||
},
|
},
|
||||||
"contact_email": "johnson@as.example.org",
|
"contact_email": "johnson@as.example.org",
|
||||||
|
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||||
"booth": "A2",
|
"booth": "A2",
|
||||||
"locale": "de",
|
"locale": "de",
|
||||||
"access_code": "VKHZ2FU84",
|
"access_code": "VKHZ2FU84",
|
||||||
@@ -162,6 +164,7 @@ Endpoints
|
|||||||
"title": "Dr"
|
"title": "Dr"
|
||||||
},
|
},
|
||||||
"contact_email": "johnson@as.example.org",
|
"contact_email": "johnson@as.example.org",
|
||||||
|
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||||
"booth": "A2",
|
"booth": "A2",
|
||||||
"locale": "de",
|
"locale": "de",
|
||||||
"access_code": "VKHZ2FU84",
|
"access_code": "VKHZ2FU84",
|
||||||
@@ -365,6 +368,7 @@ Endpoints
|
|||||||
"title": "Dr"
|
"title": "Dr"
|
||||||
},
|
},
|
||||||
"contact_email": "johnson@as.example.org",
|
"contact_email": "johnson@as.example.org",
|
||||||
|
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||||
"booth": "A2",
|
"booth": "A2",
|
||||||
"locale": "de",
|
"locale": "de",
|
||||||
"allow_lead_scanning": true,
|
"allow_lead_scanning": true,
|
||||||
@@ -394,6 +398,7 @@ Endpoints
|
|||||||
"title": "Dr"
|
"title": "Dr"
|
||||||
},
|
},
|
||||||
"contact_email": "johnson@as.example.org",
|
"contact_email": "johnson@as.example.org",
|
||||||
|
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||||
"booth": "A2",
|
"booth": "A2",
|
||||||
"locale": "de",
|
"locale": "de",
|
||||||
"access_code": "VKHZ2FU84",
|
"access_code": "VKHZ2FU84",
|
||||||
@@ -454,6 +459,7 @@ Endpoints
|
|||||||
"title": "Dr"
|
"title": "Dr"
|
||||||
},
|
},
|
||||||
"contact_email": "johnson@as.example.org",
|
"contact_email": "johnson@as.example.org",
|
||||||
|
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||||
"booth": "A2",
|
"booth": "A2",
|
||||||
"locale": "de",
|
"locale": "de",
|
||||||
"access_code": "VKHZ2FU84",
|
"access_code": "VKHZ2FU84",
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2024.2.0"
|
__version__ = "2024.3.0.dev0"
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class InvoiceExporterMixin:
|
|||||||
('', _('All payment providers')),
|
('', _('All payment providers')),
|
||||||
] + [
|
] + [
|
||||||
(k, v.verbose_name) for k, v in self.event.get_payment_providers().items()
|
(k, v.verbose_name) for k, v in self.event.get_payment_providers().items()
|
||||||
|
if not v.is_meta
|
||||||
],
|
],
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_('Only include invoices for orders that have at least one payment attempt '
|
help_text=_('Only include invoices for orders that have at least one payment attempt '
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 4.2.9 on 2024-01-30 11:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0256_itemvariation_unavail_modes"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="item",
|
||||||
|
name="default_price",
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=13),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -80,6 +80,15 @@ from .organizer import Organizer, Team
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def annotate_with_time_based_properties(events_or_subevents, now_dt):
|
||||||
|
print("annotate_with_time_based_properties", now_dt)
|
||||||
|
for e_s in events_or_subevents:
|
||||||
|
if e_s:
|
||||||
|
e_s.presale_is_running = e_s.presale_is_running_by_time(now_dt)
|
||||||
|
e_s.presale_has_ended = e_s.presale_has_ended_by_time(now_dt)
|
||||||
|
return events_or_subevents
|
||||||
|
|
||||||
|
|
||||||
class EventMixin:
|
class EventMixin:
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
|
if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
|
||||||
@@ -229,17 +238,17 @@ class EventMixin:
|
|||||||
else:
|
else:
|
||||||
return self.presale_end
|
return self.presale_end
|
||||||
|
|
||||||
@property
|
def presale_has_ended_by_time(self, now_dt: datetime=None):
|
||||||
def presale_has_ended(self):
|
|
||||||
"""
|
"""
|
||||||
Is true, when ``presale_end`` is set and in the past.
|
Is true, when ``presale_end`` is set and in the past.
|
||||||
"""
|
"""
|
||||||
|
now_dt = now_dt or now()
|
||||||
if self.effective_presale_end:
|
if self.effective_presale_end:
|
||||||
return now() > self.effective_presale_end
|
return now_dt > self.effective_presale_end
|
||||||
elif self.date_to:
|
elif self.date_to:
|
||||||
return now() > self.date_to
|
return now_dt > self.date_to
|
||||||
else:
|
else:
|
||||||
return now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date()
|
return now_dt.astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def effective_presale_start(self):
|
def effective_presale_start(self):
|
||||||
@@ -253,18 +262,21 @@ class EventMixin:
|
|||||||
else:
|
else:
|
||||||
return self.presale_start
|
return self.presale_start
|
||||||
|
|
||||||
@property
|
def presale_is_running_by_time(self, now_dt: datetime=None):
|
||||||
def presale_is_running(self):
|
|
||||||
"""
|
"""
|
||||||
Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not
|
Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not
|
||||||
set or in the past.
|
set or in the past.
|
||||||
"""
|
"""
|
||||||
if self.effective_presale_start and now() < self.effective_presale_start:
|
now_dt = now_dt or now()
|
||||||
|
if self.effective_presale_start and now_dt < self.effective_presale_start:
|
||||||
return False
|
return False
|
||||||
return not self.presale_has_ended
|
return not self.presale_has_ended_by_time(now_dt)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def event_microdata(self):
|
def event_microdata(self):
|
||||||
|
if self.settings.event_microdata:
|
||||||
|
return self.settings.event_microdata
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
eventdict = {
|
eventdict = {
|
||||||
@@ -680,12 +692,12 @@ class Event(EventMixin, LoggedModel):
|
|||||||
|
|
||||||
return qs_annotated
|
return qs_annotated
|
||||||
|
|
||||||
@property
|
def presale_has_ended_by_time(self, now_dt: datetime = None):
|
||||||
def presale_has_ended(self):
|
now_dt = now_dt or now()
|
||||||
if self.has_subevents:
|
if self.has_subevents:
|
||||||
return self.presale_end and now() > self.presale_end
|
return self.presale_end and now_dt > self.presale_end
|
||||||
else:
|
else:
|
||||||
return super().presale_has_ended
|
return super().presale_has_ended_by_time(now_dt)
|
||||||
|
|
||||||
def delete_all_orders(self, really=False):
|
def delete_all_orders(self, really=False):
|
||||||
from .checkin import Checkin
|
from .checkin import Checkin
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ class Item(LoggedModel):
|
|||||||
help_text=_("If this product has multiple variations, you can set different prices for each of the "
|
help_text=_("If this product has multiple variations, you can set different prices for each of the "
|
||||||
"variations. If a variation does not have a special price or if you do not have variations, "
|
"variations. If a variation does not have a special price or if you do not have variations, "
|
||||||
"this price will be used."),
|
"this price will be used."),
|
||||||
max_digits=13, decimal_places=2, null=True
|
max_digits=13, decimal_places=2,
|
||||||
)
|
)
|
||||||
free_price = models.BooleanField(
|
free_price = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
|
|||||||
@@ -351,9 +351,6 @@ class Voucher(LoggedModel):
|
|||||||
'variations.'))
|
'variations.'))
|
||||||
if variation and not item.variations.filter(pk=variation.pk).exists():
|
if variation and not item.variations.filter(pk=variation.pk).exists():
|
||||||
raise ValidationError(_('This variation does not belong to this product.'))
|
raise ValidationError(_('This variation does not belong to this product.'))
|
||||||
if item.has_variations and not variation and data.get('block_quota'):
|
|
||||||
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
|
|
||||||
'Otherwise it might be unclear which quotas to block.'))
|
|
||||||
if item.category and item.category.is_addon:
|
if item.category and item.category.is_addon:
|
||||||
raise ValidationError(_('It is currently not possible to create vouchers for add-on products.'))
|
raise ValidationError(_('It is currently not possible to create vouchers for add-on products.'))
|
||||||
elif block_quota:
|
elif block_quota:
|
||||||
@@ -431,7 +428,15 @@ class Voucher(LoggedModel):
|
|||||||
elif old_instance.variation:
|
elif old_instance.variation:
|
||||||
quotas |= set(old_instance.variation.quotas.filter(subevent=old_instance.subevent))
|
quotas |= set(old_instance.variation.quotas.filter(subevent=old_instance.subevent))
|
||||||
elif old_instance.item:
|
elif old_instance.item:
|
||||||
quotas |= set(old_instance.item.quotas.filter(subevent=old_instance.subevent))
|
if old_instance.item.has_variations:
|
||||||
|
quotas |= set(
|
||||||
|
Quota.objects.filter(pk__in=Quota.variations.through.objects.filter(
|
||||||
|
itemvariation__item=old_instance.item,
|
||||||
|
quota__subevent=old_instance.subevent,
|
||||||
|
).values('quota_id'))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
quotas |= set(old_instance.item.quotas.filter(subevent=old_instance.subevent))
|
||||||
return quotas
|
return quotas
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -446,13 +451,19 @@ class Voucher(LoggedModel):
|
|||||||
|
|
||||||
if quota:
|
if quota:
|
||||||
new_quotas = {quota}
|
new_quotas = {quota}
|
||||||
elif item and item.has_variations and not variation:
|
|
||||||
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
|
|
||||||
'Otherwise it might be unclear which quotas to block.'))
|
|
||||||
elif item and variation:
|
elif item and variation:
|
||||||
new_quotas = set(variation.quotas.filter(subevent=data.get('subevent')))
|
new_quotas = set(variation.quotas.filter(subevent=data.get('subevent')))
|
||||||
elif item and not item.has_variations:
|
elif item and not item.has_variations:
|
||||||
new_quotas = set(item.quotas.filter(subevent=data.get('subevent')))
|
new_quotas = set(item.quotas.filter(subevent=data.get('subevent')))
|
||||||
|
elif item and item.has_variations:
|
||||||
|
new_quotas = set(
|
||||||
|
Quota.objects.filter(
|
||||||
|
pk__in=Quota.variations.through.objects.filter(
|
||||||
|
itemvariation__item=old_instance.item,
|
||||||
|
quota__subevent=data.get('subevent'),
|
||||||
|
).values('quota_id')
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
|
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
|
||||||
'tickets.'))
|
'tickets.'))
|
||||||
|
|||||||
@@ -383,6 +383,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
if event:
|
if event:
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
event = Event.objects.get(id=event)
|
event = Event.objects.get(id=event)
|
||||||
|
organizer = event.organizer
|
||||||
backend = event.get_mail_backend()
|
backend = event.get_mail_backend()
|
||||||
cm = lambda: scope(organizer=event.organizer) # noqa
|
cm = lambda: scope(organizer=event.organizer) # noqa
|
||||||
elif organizer:
|
elif organizer:
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ class QuotaAvailability:
|
|||||||
self._count_waitinglist = count_waitinglist
|
self._count_waitinglist = count_waitinglist
|
||||||
self._ignore_closed = ignore_closed
|
self._ignore_closed = ignore_closed
|
||||||
self._full_results = full_results
|
self._full_results = full_results
|
||||||
self._item_to_quotas = defaultdict(list)
|
self._item_to_quotas = defaultdict(set)
|
||||||
self._var_to_quotas = defaultdict(list)
|
self._var_to_quotas = defaultdict(set)
|
||||||
self._early_out = early_out
|
self._early_out = early_out
|
||||||
self._quota_objects = {}
|
self._quota_objects = {}
|
||||||
self.results = {}
|
self.results = {}
|
||||||
@@ -243,13 +243,16 @@ class QuotaAvailability:
|
|||||||
quota_id__in=[q.pk for q in quotas]
|
quota_id__in=[q.pk for q in quotas]
|
||||||
).values('quota_id', 'item_id')
|
).values('quota_id', 'item_id')
|
||||||
for m in q_items:
|
for m in q_items:
|
||||||
self._item_to_quotas[m['item_id']].append(self._quota_objects[m['quota_id']])
|
self._item_to_quotas[m['item_id']].add(self._quota_objects[m['quota_id']])
|
||||||
|
|
||||||
q_vars = Quota.variations.through.objects.filter(
|
q_vars = Quota.variations.through.objects.filter(
|
||||||
quota_id__in=[q.pk for q in quotas]
|
quota_id__in=[q.pk for q in quotas]
|
||||||
).values('quota_id', 'itemvariation_id')
|
).values('quota_id', 'itemvariation_id', 'itemvariation__item_id')
|
||||||
for m in q_vars:
|
for m in q_vars:
|
||||||
self._var_to_quotas[m['itemvariation_id']].append(self._quota_objects[m['quota_id']])
|
self._var_to_quotas[m['itemvariation_id']].add(self._quota_objects[m['quota_id']])
|
||||||
|
# We can't be 100% certain that a quota, when it is connected to a variation, is also always connected to
|
||||||
|
# the parent item, so we double-check here just to be sure.
|
||||||
|
self._item_to_quotas[m['itemvariation__item_id']].add(self._quota_objects[m['quota_id']])
|
||||||
|
|
||||||
self._compute_orders(quotas, q_items, q_vars, size_left)
|
self._compute_orders(quotas, q_items, q_vars, size_left)
|
||||||
|
|
||||||
@@ -378,7 +381,10 @@ class QuotaAvailability:
|
|||||||
Q(
|
Q(
|
||||||
Q(
|
Q(
|
||||||
Q(variation_id__isnull=True) &
|
Q(variation_id__isnull=True) &
|
||||||
Q(item_id__in={i['item_id'] for i in q_items if i['quota_id'] in quota_ids})
|
Q(item_id__in=(
|
||||||
|
{i['item_id'] for i in q_items if i['quota_id'] in quota_ids} |
|
||||||
|
{i['itemvariation__item_id'] for i in q_vars if i['quota_id'] in quota_ids}
|
||||||
|
))
|
||||||
) | Q(
|
) | Q(
|
||||||
variation_id__in={i['itemvariation_id'] for i in q_vars if i['quota_id'] in quota_ids}
|
variation_id__in={i['itemvariation_id'] for i in q_vars if i['quota_id'] in quota_ids}
|
||||||
) | Q(
|
) | Q(
|
||||||
|
|||||||
@@ -123,8 +123,6 @@ class ClearableBasenameFileInput(forms.ClearableFileInput):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
if hasattr(self.file, 'display_name'):
|
|
||||||
return self.file.display_name
|
|
||||||
return self.file.name
|
return self.file.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -32,9 +32,14 @@ def clean_filename(fname):
|
|||||||
|
|
||||||
"Terms.pdf" → "Terms.pdf.OybgvyAH.22c0583727d5bc.pdf"
|
"Terms.pdf" → "Terms.pdf.OybgvyAH.22c0583727d5bc.pdf"
|
||||||
|
|
||||||
This function reverses this operation:
|
This function reverses this operation (leaving names without doubled extension as-is):
|
||||||
|
|
||||||
"Terms.pdf.OybgvyAH.22c0583727d5bc.pdf" → "Terms.pdf"
|
"Terms.pdf.OybgvyAH.22c0583727d5bc.pdf" → "Terms.pdf"
|
||||||
|
"Terms.pdf" → "Terms.pdf"
|
||||||
"""
|
"""
|
||||||
ext = '.' + fname.split('.')[-1]
|
ext = '.' + fname.split('.')[-1]
|
||||||
return fname.rsplit(ext + ".", 1)[0] + ext
|
parts = fname.rsplit(ext + ".", 1)
|
||||||
|
if len(parts) == 1:
|
||||||
|
return parts[0]
|
||||||
|
else:
|
||||||
|
return parts[0] + ext
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -71,11 +71,15 @@ def returnurl_process_request(sender, request, **kwargs):
|
|||||||
u = request.GET.get('return_url')
|
u = request.GET.get('return_url')
|
||||||
if not sender.settings.returnurl_prefix:
|
if not sender.settings.returnurl_prefix:
|
||||||
raise PermissionDenied('No return URL prefix set.')
|
raise PermissionDenied('No return URL prefix set.')
|
||||||
elif not u.startswith(sender.settings.returnurl_prefix):
|
elif not check_against_prefix_list(u, sender.settings.returnurl_prefix):
|
||||||
raise PermissionDenied('Invalid return URL.')
|
raise PermissionDenied('Invalid return URL.')
|
||||||
request.session[key] = u
|
request.session[key] = u
|
||||||
|
|
||||||
|
|
||||||
|
def check_against_prefix_list(u, allowlist):
|
||||||
|
return any(u.startswith(allow.strip()) for allow in allowlist.split("\n") if allow.strip() != "")
|
||||||
|
|
||||||
|
|
||||||
@receiver(nav_event_settings, dispatch_uid='returnurl_nav')
|
@receiver(nav_event_settings, dispatch_uid='returnurl_nav')
|
||||||
def navbar_info(sender, request, **kwargs):
|
def navbar_info(sender, request, **kwargs):
|
||||||
url = resolve(request.path_info)
|
url = resolve(request.path_info)
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import re
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -31,10 +33,14 @@ from pretix.control.views.event import (
|
|||||||
|
|
||||||
|
|
||||||
class ReturnSettingsForm(SettingsForm):
|
class ReturnSettingsForm(SettingsForm):
|
||||||
returnurl_prefix = forms.URLField(
|
returnurl_prefix = forms.RegexField(
|
||||||
label=_("Base redirection URL"),
|
label=_("Base redirection URLs"),
|
||||||
help_text=_("Redirection will only be allowed to URLs that start with this prefix."),
|
help_text=_("Redirection will only be allowed to URLs that start with one of these prefixes. "
|
||||||
|
"Enter one or more allowed URL prefix per line. "
|
||||||
|
"URL prefixes must include a slash after the hostname."),
|
||||||
required=False,
|
required=False,
|
||||||
|
widget=forms.Textarea,
|
||||||
|
regex=re.compile(r'^((https://.*/.*|http://localhost[:/].*)\n*)*$')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -545,6 +545,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
|||||||
)
|
)
|
||||||
if getattr(self.request, 'customer', None) else None
|
if getattr(self.request, 'customer', None) else None
|
||||||
),
|
),
|
||||||
|
now_dt=self.request.now_dt,
|
||||||
)
|
)
|
||||||
item_cache[ckey] = items
|
item_cache[ckey] = items
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class WaitingListForm(forms.ModelForm):
|
|||||||
)
|
)
|
||||||
if customer else None
|
if customer else None
|
||||||
),
|
),
|
||||||
|
now_dt=request.now_dt,
|
||||||
)
|
)
|
||||||
for i in items:
|
for i in items:
|
||||||
if not i.allow_waitinglist:
|
if not i.allow_waitinglist:
|
||||||
|
|||||||
@@ -32,8 +32,11 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
from dateutil.parser import parse
|
||||||
|
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.urls import resolve
|
from django.urls import resolve
|
||||||
|
from django.utils.timezone import now
|
||||||
from django_scopes import scope
|
from django_scopes import scope
|
||||||
|
|
||||||
from pretix.base.channels import WebshopSalesChannel
|
from pretix.base.channels import WebshopSalesChannel
|
||||||
@@ -79,3 +82,27 @@ class EventMiddleware:
|
|||||||
response = response.render()
|
response = response.render()
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class TimeMachineMiddleware:
|
||||||
|
def __init__(self, get_response=None):
|
||||||
|
self.get_response = get_response
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
if hasattr(request, 'event') and hasattr(request, '_namespace') and request._namespace == 'presale' and \
|
||||||
|
'time_machine' in request.COOKIES and \
|
||||||
|
request.user.has_event_permission(request.organizer, request.event, 'can_change_event_settings', request):
|
||||||
|
print("setting now_dt from cookie")
|
||||||
|
request.now_dt = parse(request.COOKIES['time_machine'])
|
||||||
|
request.now_dt_is_fake = True
|
||||||
|
else:
|
||||||
|
print("setting now_dt to now",
|
||||||
|
"hasevent?",hasattr(request, 'event') ,
|
||||||
|
"namespace?",hasattr(request, '_namespace') and request._namespace,
|
||||||
|
"cookies?",request.COOKIES)
|
||||||
|
request.now_dt = now()
|
||||||
|
|
||||||
|
return self.get_response(request)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if request.now_dt_is_fake %}
|
||||||
|
<div class="offline-banner">
|
||||||
|
<div class="container">
|
||||||
|
<span class="fa fa-user-secret" aria-hidden="true"></span>
|
||||||
|
{% trans "You are currently using the time machine. The ticket shop is rendered as if it were" %} {{ request.now_dt }}
|
||||||
|
<a href="#">
|
||||||
|
{% trans "Go back to current time" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="container page-header-links {% if event.settings.theme_color_background|upper != "#FFFFFF" or event_logo_image_large %}page-header-links-outside{% endif %}">
|
<div class="container page-header-links {% if event.settings.theme_color_background|upper != "#FFFFFF" or event_logo_image_large %}page-header-links-outside{% endif %}">
|
||||||
{% if event.settings.locales|length > 1 or request.organizer.settings.customer_accounts %}
|
{% if event.settings.locales|length > 1 or request.organizer.settings.customer_accounts %}
|
||||||
{% if event.settings.theme_color_background|upper != "#FFFFFF" or event_logo_image_large %}
|
{% if event.settings.theme_color_background|upper != "#FFFFFF" or event_logo_image_large %}
|
||||||
|
|||||||
@@ -569,6 +569,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
|
|||||||
testmode=self.request.event.testmode
|
testmode=self.request.event.testmode
|
||||||
) if getattr(self.request, 'customer', None) else None
|
) if getattr(self.request, 'customer', None) else None
|
||||||
),
|
),
|
||||||
|
now_dt=self.request.now_dt,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Calculate how many options the user still has. If there is only one option, we can
|
# Calculate how many options the user still has. If there is only one option, we can
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class CheckoutView(View):
|
|||||||
messages.error(request, _("Your cart is empty"))
|
messages.error(request, _("Your cart is empty"))
|
||||||
return self.redirect(self.get_index_url(self.request))
|
return self.redirect(self.get_index_url(self.request))
|
||||||
|
|
||||||
if not request.event.presale_is_running:
|
if not request.event.presale_is_running_by_time(request.now_dt):
|
||||||
messages.error(request, _("The booking period for this event is over or has not yet started."))
|
messages.error(request, _("The booking period for this event is over or has not yet started."))
|
||||||
return self.redirect(self.get_index_url(self.request))
|
return self.redirect(self.get_index_url(self.request))
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ from pretix.base.channels import get_all_sales_channels
|
|||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
ItemVariation, Quota, SeatCategoryMapping, Voucher,
|
ItemVariation, Quota, SeatCategoryMapping, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.models.event import Event, SubEvent
|
from pretix.base.models.event import Event, SubEvent, annotate_with_time_based_properties
|
||||||
from pretix.base.models.items import (
|
from pretix.base.models.items import (
|
||||||
ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
|
ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
|
||||||
)
|
)
|
||||||
@@ -105,7 +105,8 @@ def item_group_by_category(items):
|
|||||||
|
|
||||||
def get_grouped_items(event, subevent=None, voucher=None, channel='web', require_seat=0, base_qs=None, allow_addons=False,
|
def get_grouped_items(event, subevent=None, voucher=None, channel='web', require_seat=0, base_qs=None, allow_addons=False,
|
||||||
quota_cache=None, filter_items=None, filter_categories=None, memberships=None,
|
quota_cache=None, filter_items=None, filter_categories=None, memberships=None,
|
||||||
ignore_hide_sold_out_for_item_ids=None):
|
ignore_hide_sold_out_for_item_ids=None, now_dt: datetime=None):
|
||||||
|
now_dt = now_dt or now()
|
||||||
base_qs_set = base_qs is not None
|
base_qs_set = base_qs is not None
|
||||||
base_qs = base_qs if base_qs is not None else event.items
|
base_qs = base_qs if base_qs is not None else event.items
|
||||||
|
|
||||||
@@ -119,8 +120,8 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
|
|||||||
requires_seat = Value(0, output_field=IntegerField())
|
requires_seat = Value(0, output_field=IntegerField())
|
||||||
|
|
||||||
variation_q = (
|
variation_q = (
|
||||||
Q(Q(available_from__isnull=True) | Q(available_from__lte=now()) | Q(available_from_mode='info')) &
|
Q(Q(available_from__isnull=True) | Q(available_from__lte=now_dt) | Q(available_from_mode='info')) &
|
||||||
Q(Q(available_until__isnull=True) | Q(available_until__gte=now()) | Q(available_until_mode='info'))
|
Q(Q(available_until__isnull=True) | Q(available_until__gte=now_dt) | Q(available_until_mode='info'))
|
||||||
)
|
)
|
||||||
if not voucher or not voucher.show_hidden_items:
|
if not voucher or not voucher.show_hidden_items:
|
||||||
variation_q &= Q(hide_without_voucher=False)
|
variation_q &= Q(hide_without_voucher=False)
|
||||||
@@ -137,8 +138,8 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
|
|||||||
subevent_disabled=Exists(
|
subevent_disabled=Exists(
|
||||||
SubEventItemVariation.objects.filter(
|
SubEventItemVariation.objects.filter(
|
||||||
Q(disabled=True)
|
Q(disabled=True)
|
||||||
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=now()))
|
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=now_dt))
|
||||||
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=now())),
|
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=now_dt)),
|
||||||
variation_id=OuterRef('pk'),
|
variation_id=OuterRef('pk'),
|
||||||
subevent=subevent,
|
subevent=subevent,
|
||||||
)
|
)
|
||||||
@@ -209,8 +210,8 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
|
|||||||
subevent_disabled=Exists(
|
subevent_disabled=Exists(
|
||||||
SubEventItem.objects.filter(
|
SubEventItem.objects.filter(
|
||||||
Q(disabled=True)
|
Q(disabled=True)
|
||||||
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=now()))
|
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=now_dt))
|
||||||
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=now())),
|
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=now_dt)),
|
||||||
item_id=OuterRef('pk'),
|
item_id=OuterRef('pk'),
|
||||||
subevent=subevent,
|
subevent=subevent,
|
||||||
)
|
)
|
||||||
@@ -306,7 +307,7 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
|
|||||||
item._remove = True
|
item._remove = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
item.current_unavailability_reason = item.unavailability_reason(has_voucher=voucher, subevent=subevent)
|
item.current_unavailability_reason = item.unavailability_reason(now_dt=now_dt, has_voucher=voucher, subevent=subevent)
|
||||||
|
|
||||||
item.description = str(item.description)
|
item.description = str(item.description)
|
||||||
for recv, resp in item_description.send(sender=event, item=item, variation=None, subevent=subevent):
|
for recv, resp in item_description.send(sender=event, item=item, variation=None, subevent=subevent):
|
||||||
@@ -422,7 +423,7 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
|
|||||||
if not display_add_to_cart:
|
if not display_add_to_cart:
|
||||||
display_add_to_cart = not item.requires_seat and var.order_max > 0
|
display_add_to_cart = not item.requires_seat and var.order_max > 0
|
||||||
|
|
||||||
var.current_unavailability_reason = var.unavailability_reason(has_voucher=voucher, subevent=subevent)
|
var.current_unavailability_reason = var.unavailability_reason(now_dt=now_dt, has_voucher=voucher, subevent=subevent)
|
||||||
|
|
||||||
item.original_price = (
|
item.original_price = (
|
||||||
item.tax(item.original_price, currency=event.currency, include_bundled=True,
|
item.tax(item.original_price, currency=event.currency, include_bundled=True,
|
||||||
@@ -535,6 +536,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
|||||||
|
|
||||||
context['ev'] = self.subevent or self.request.event
|
context['ev'] = self.subevent or self.request.event
|
||||||
context['subevent'] = self.subevent
|
context['subevent'] = self.subevent
|
||||||
|
annotate_with_time_based_properties([self.request.event, self.subevent], self.request.now_dt)
|
||||||
|
|
||||||
# Show voucher option if an event is selected and vouchers exist
|
# Show voucher option if an event is selected and vouchers exist
|
||||||
vouchers_exist = self.request.event.cache.get('vouchers_exist')
|
vouchers_exist = self.request.event.cache.get('vouchers_exist')
|
||||||
@@ -543,10 +545,10 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
|||||||
self.request.event.cache.set('vouchers_exist', vouchers_exist)
|
self.request.event.cache.set('vouchers_exist', vouchers_exist)
|
||||||
context['show_vouchers'] = context['vouchers_exist'] = vouchers_exist and (
|
context['show_vouchers'] = context['vouchers_exist'] = vouchers_exist and (
|
||||||
(self.request.event.has_subevents and not self.subevent) or
|
(self.request.event.has_subevents and not self.subevent) or
|
||||||
context['ev'].presale_is_running
|
context['ev'].presale_is_running_by_time(self.request.now_dt)
|
||||||
)
|
)
|
||||||
|
|
||||||
context['allow_waitinglist'] = self.request.event.settings.waiting_list_enabled and context['ev'].presale_is_running
|
context['allow_waitinglist'] = self.request.event.settings.waiting_list_enabled and context['ev'].presale_is_running_by_time(self.request.now_dt)
|
||||||
|
|
||||||
if not self.request.event.has_subevents or self.subevent:
|
if not self.request.event.has_subevents or self.subevent:
|
||||||
# Fetch all items
|
# Fetch all items
|
||||||
@@ -562,6 +564,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
|||||||
testmode=self.request.event.testmode
|
testmode=self.request.event.testmode
|
||||||
) if getattr(self.request, 'customer', None) else None
|
) if getattr(self.request, 'customer', None) else None
|
||||||
),
|
),
|
||||||
|
now_dt=self.request.now_dt
|
||||||
)
|
)
|
||||||
|
|
||||||
context['waitinglist_seated'] = False
|
context['waitinglist_seated'] = False
|
||||||
@@ -612,7 +615,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
|||||||
|
|
||||||
context['show_cart'] = (
|
context['show_cart'] = (
|
||||||
context['cart']['positions'] and (
|
context['cart']['positions'] and (
|
||||||
self.request.event.has_subevents or self.request.event.presale_is_running
|
self.request.event.has_subevents or self.request.event.presale_is_running_by_time(self.request.now_dt)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if self.request.event.settings.redirect_to_checkout_directly:
|
if self.request.event.settings.redirect_to_checkout_directly:
|
||||||
@@ -679,6 +682,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
|||||||
limit_before, after, ebd, set(), self.request.event,
|
limit_before, after, ebd, set(), self.request.event,
|
||||||
self.kwargs.get('cart_namespace'),
|
self.kwargs.get('cart_namespace'),
|
||||||
voucher,
|
voucher,
|
||||||
|
now_dt=self.request.now_dt,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Hide names of subevents in event series where it is always the same. No need to show the name of the museum thousands of times
|
# Hide names of subevents in event series where it is always the same. No need to show the name of the museum thousands of times
|
||||||
@@ -738,6 +742,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
|||||||
limit_before, after, ebd, set(), self.request.event,
|
limit_before, after, ebd, set(), self.request.event,
|
||||||
self.kwargs.get('cart_namespace'),
|
self.kwargs.get('cart_namespace'),
|
||||||
voucher,
|
voucher,
|
||||||
|
now_dt=self.request.now_dt,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Hide names of subevents in event series where it is always the same. No need to show the name of the museum thousands of times
|
# Hide names of subevents in event series where it is always the same. No need to show the name of the museum thousands of times
|
||||||
@@ -776,7 +781,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
|||||||
future_only=self.request.event.settings.event_calendar_future_only
|
future_only=self.request.event.settings.event_calendar_future_only
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
context['subevent_list'] = self.request.event.subevents_sorted(
|
context['subevent_list'] = annotate_with_time_based_properties(self.request.event.subevents_sorted(
|
||||||
filter_qs_by_attr(
|
filter_qs_by_attr(
|
||||||
self.request.event.subevents_annotated(
|
self.request.event.subevents_annotated(
|
||||||
self.request.sales_channel.identifier,
|
self.request.sales_channel.identifier,
|
||||||
@@ -784,7 +789,8 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
|||||||
).using(settings.DATABASE_REPLICA),
|
).using(settings.DATABASE_REPLICA),
|
||||||
self.request
|
self.request
|
||||||
)
|
)
|
||||||
)
|
), self.request.now_dt)
|
||||||
|
|
||||||
if self.request.event.settings.event_list_available_only and not voucher:
|
if self.request.event.settings.event_list_available_only and not voucher:
|
||||||
context['subevent_list'] = [
|
context['subevent_list'] = [
|
||||||
se for se in context['subevent_list']
|
se for se in context['subevent_list']
|
||||||
|
|||||||
@@ -1311,7 +1311,8 @@ class OrderChangeMixin:
|
|||||||
)
|
)
|
||||||
if self.order.customer else None
|
if self.order.customer else None
|
||||||
),
|
),
|
||||||
ignore_hide_sold_out_for_item_ids={k[0] for k in current_addon_products.keys()}
|
ignore_hide_sold_out_for_item_ids={k[0] for k in current_addon_products.keys()},
|
||||||
|
now_dt=self.request.now_dt,
|
||||||
)
|
)
|
||||||
item_cache[ckey] = items
|
item_cache[ckey] = items
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ from pretix.base.i18n import language
|
|||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Event, EventMetaValue, Organizer, Quota, SubEvent, SubEventMetaValue,
|
Event, EventMetaValue, Organizer, Quota, SubEvent, SubEventMetaValue,
|
||||||
)
|
)
|
||||||
|
from pretix.base.models.event import annotate_with_time_based_properties
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.helpers.compat import date_fromisocalendar
|
from pretix.helpers.compat import date_fromisocalendar
|
||||||
from pretix.helpers.daterange import daterange
|
from pretix.helpers.daterange import daterange
|
||||||
@@ -549,14 +550,16 @@ def add_events_for_days(request, baseqs, before, after, ebd, timezones):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def add_subevents_for_days(qs, before, after, ebd, timezones, event=None, cart_namespace=None, voucher=None):
|
def add_subevents_for_days(qs, before, after, ebd, timezones, event=None, cart_namespace=None, voucher=None, now_dt=None):
|
||||||
|
print("add_subevents_for_days", now_dt)
|
||||||
|
now_dt = now_dt or now()
|
||||||
qs = qs.filter(active=True, is_public=True).filter(
|
qs = qs.filter(active=True, is_public=True).filter(
|
||||||
Q(Q(date_to__gte=before) & Q(date_from__lte=after)) |
|
Q(Q(date_to__gte=before) & Q(date_from__lte=after)) |
|
||||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=before) & Q(date_from__lte=after))
|
Q(Q(date_to__isnull=True) & Q(date_from__gte=before) & Q(date_from__lte=after))
|
||||||
).order_by(
|
).order_by(
|
||||||
'date_from'
|
'date_from'
|
||||||
)
|
)
|
||||||
|
qs = annotate_with_time_based_properties(qs, now_dt)
|
||||||
quotas_to_compute = []
|
quotas_to_compute = []
|
||||||
for se in qs:
|
for se in qs:
|
||||||
if se.presale_is_running:
|
if se.presale_is_running:
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ class WidgetAPIProductList(EventListMixin, View):
|
|||||||
testmode=self.request.event.testmode
|
testmode=self.request.event.testmode
|
||||||
) if getattr(self.request, 'customer', None) else None
|
) if getattr(self.request, 'customer', None) else None
|
||||||
),
|
),
|
||||||
|
now_dt=self.request.now_dt,
|
||||||
)
|
)
|
||||||
|
|
||||||
grps = []
|
grps = []
|
||||||
@@ -409,11 +410,11 @@ class WidgetAPIProductList(EventListMixin, View):
|
|||||||
availability['color'] = 'none'
|
availability['color'] = 'none'
|
||||||
availability['text'] = gettext('More info')
|
availability['text'] = gettext('More info')
|
||||||
availability['reason'] = 'unknown'
|
availability['reason'] = 'unknown'
|
||||||
elif ev.presale_is_running:
|
elif ev.presale_is_running_by_time(self.request.now_dt):
|
||||||
availability['color'] = 'green'
|
availability['color'] = 'green'
|
||||||
availability['text'] = gettext('Book now')
|
availability['text'] = gettext('Book now')
|
||||||
availability['reason'] = 'ok'
|
availability['reason'] = 'ok'
|
||||||
elif ev.presale_has_ended:
|
elif ev.presale_has_ended_by_time(self.request.now_dt):
|
||||||
availability['color'] = 'red'
|
availability['color'] = 'red'
|
||||||
availability['text'] = gettext('Sale over')
|
availability['text'] = gettext('Sale over')
|
||||||
availability['reason'] = 'over'
|
availability['reason'] = 'over'
|
||||||
|
|||||||
@@ -443,6 +443,7 @@ MIDDLEWARE = [
|
|||||||
'pretix.base.middleware.LocaleMiddleware',
|
'pretix.base.middleware.LocaleMiddleware',
|
||||||
'pretix.base.middleware.SecurityMiddleware',
|
'pretix.base.middleware.SecurityMiddleware',
|
||||||
'pretix.presale.middleware.EventMiddleware',
|
'pretix.presale.middleware.EventMiddleware',
|
||||||
|
'pretix.presale.middleware.TimeMachineMiddleware',
|
||||||
'pretix.api.middleware.ApiScopeMiddleware',
|
'pretix.api.middleware.ApiScopeMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -967,8 +967,14 @@
|
|||||||
float: none;
|
float: none;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
.pretix-widget-item-info-col:after {
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
.pretix-widget-item-price-col, .pretix-widget-item-availability-col {
|
.pretix-widget-item-price-col, .pretix-widget-item-availability-col {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
|
min-width: 140px;
|
||||||
}
|
}
|
||||||
.pretix-widget-action {
|
.pretix-widget-action {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -934,7 +934,7 @@ def test_cartpos_create_with_voucher_unknown(token_client, organizer, event, ite
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_cartpos_create_with_voucher_invalid_item(token_client, organizer, event, item, quota):
|
def test_cartpos_create_with_voucher_invalid_item(token_client, organizer, event, item, quota):
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
item2 = event.items.create(name="item2")
|
item2 = event.items.create(name="item2", default_price=0)
|
||||||
voucher = event.vouchers.create(code="FOOBAR", item=item2)
|
voucher = event.vouchers.create(code="FOOBAR", item=item2)
|
||||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||||
res['item'] = item.pk
|
res['item'] = item.pk
|
||||||
|
|||||||
@@ -283,6 +283,29 @@ class QuotaTestCase(BaseQuotaTestCase):
|
|||||||
v.save()
|
v.save()
|
||||||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
||||||
|
|
||||||
|
@classscope(attr='o')
|
||||||
|
def test_voucher_all_variations(self):
|
||||||
|
self.quota.variations.add(self.var1)
|
||||||
|
self.quota.size = 1
|
||||||
|
self.quota.save()
|
||||||
|
|
||||||
|
self.quota2 = Quota.objects.create(name="Test", size=2, event=self.event)
|
||||||
|
self.quota2.variations.add(self.var2)
|
||||||
|
|
||||||
|
self.quota3 = Quota.objects.create(name="Test", size=2, event=self.event)
|
||||||
|
self.quota3.variations.add(self.var3)
|
||||||
|
|
||||||
|
v = Voucher.objects.create(item=self.item2, event=self.event)
|
||||||
|
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||||||
|
self.assertEqual(self.var2.check_quotas(), (Quota.AVAILABILITY_OK, 2))
|
||||||
|
self.assertEqual(self.var3.check_quotas(), (Quota.AVAILABILITY_OK, 2))
|
||||||
|
|
||||||
|
v.block_quota = True
|
||||||
|
v.save()
|
||||||
|
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
|
||||||
|
self.assertEqual(self.var2.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||||||
|
self.assertEqual(self.var3.check_quotas(), (Quota.AVAILABILITY_OK, 2))
|
||||||
|
|
||||||
@classscope(attr='o')
|
@classscope(attr='o')
|
||||||
def test_voucher_quota(self):
|
def test_voucher_quota(self):
|
||||||
self.quota.variations.add(self.var1)
|
self.quota.variations.add(self.var1)
|
||||||
@@ -979,9 +1002,8 @@ class VoucherTestCase(BaseQuotaTestCase):
|
|||||||
|
|
||||||
@classscope(attr='o')
|
@classscope(attr='o')
|
||||||
def test_voucher_specify_variation_for_block_quota(self):
|
def test_voucher_specify_variation_for_block_quota(self):
|
||||||
with self.assertRaises(ValidationError):
|
v = Voucher(item=self.item2, block_quota=True, event=self.event)
|
||||||
v = Voucher(item=self.item2, block_quota=True, event=self.event)
|
v.clean()
|
||||||
v.clean()
|
|
||||||
|
|
||||||
@classscope(attr='o')
|
@classscope(attr='o')
|
||||||
def test_voucher_no_item_but_variation(self):
|
def test_voucher_no_item_but_variation(self):
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ class VoucherFormTest(SoupTestMixin, TransactionTestCase):
|
|||||||
self._create_voucher({
|
self._create_voucher({
|
||||||
'itemvar': '%d' % self.shirt.pk,
|
'itemvar': '%d' % self.shirt.pk,
|
||||||
'block_quota': 'on'
|
'block_quota': 'on'
|
||||||
}, expected_failure=True)
|
})
|
||||||
|
|
||||||
def test_create_blocking_item_voucher_quota_full_invalid(self):
|
def test_create_blocking_item_voucher_quota_full_invalid(self):
|
||||||
self.quota_shirts.size = 0
|
self.quota_shirts.size = 0
|
||||||
|
|||||||
Reference in New Issue
Block a user