Compare commits

...
Author SHA1 Message Date
Kara Engelhardt 77adaa69fe Implement basic font fallback
pretixprint-PR: https://github.com/pretix/libpretixprint/pull/10
2026-09-28 17:43:41 +02:00
pajowu c875d758f9 Fix crash on too old time machine date (Z#23245824) (#6599)
* Fix crash on too old time machine date (Z#23245824)

dateutil.parser seems to reject dates with second-precision timezone-offsets, which datetime produces for some dates aroung the year 200. datetime.fromisoformat has no problem parsing them

* Review comments

* Add min date
2026-09-28 17:28:26 +02:00
Martin Gross 2ebdd048c2 PayPal2: Drop maximum length of client ID (Rel: #6424) 2026-09-28 16:12:55 +02:00
Raphael Michel ca40b09080 Fix API documentation bug 2026-09-28 15:46:41 +02:00
pajowuandRaphael Michel 558bf910fd Use fragment_product_list in voucher redemption view (Z#23246929) (#6567)
* Use fragment_product_list in voucher redemption view (Z#23246929)

* Formatting

* Fix usage in templates

* Review comments

* handle form prefix in fragment_product_list.html

---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-09-28 13:01:15 +02:00
pajowu 7e1e472691 Monkeypatch stock csrfmiddleware (#6401)
* Monkeypatch stock csrfmiddleware

* Add test for ensure_csrf_cookie
2026-09-28 11:46:23 +02:00
Richard Schreiber b06c9ef463 Control: do not fail on missing reason when user confirm (#6602) 2026-09-28 11:43:52 +02:00
pajowu 2af8d29ca9 Add pdf render tests (#6566)
* Add pdf tests

* pdf tests: Save temporary pdfs for failed test debugging

* Review comments
2026-09-25 16:54:58 +02:00
Raphael Michel 2791d5e49e Celery: Fix missing log if workers are recycled due to RAM usage (#6588) 2026-09-25 08:56:49 +02:00
28 changed files with 1181 additions and 489 deletions
+1
View File
@@ -42,6 +42,7 @@ recursive-include src *.py
recursive-include src *.svg
recursive-include src *.txt
recursive-include src Makefile
recursive-include src *.pdf
recursive-exclude doc *
recursive-exclude deployment *
+2 -2
View File
@@ -53,6 +53,8 @@ Checking a ticket in
Defaults to ``false`` in which case the server will determine the language (currently
the event default language, might change in the future with support for the
``Accept-Language`` header).
:<json boolean simulate: Do not actually perform the check-in, only simulate the response. The ``position`` response
object will not reflect the simulated changes.
:>json string status: ``"ok"``, ``"incomplete"``, ``"exchange"``, or ``"error"``
:>json string reason: Reason code, only set on status ``"error"``, see below for possible values.
:>json string reason_explanation: Human-readable explanation, only set on status ``"error"`` and reason ``"rules"``, can be null.
@@ -71,8 +73,6 @@ Checking a ticket in
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
:>json object media_policy: Reusable media policy (see documentation on items), only set on status ``"exchange"``.
:>json object media_type: Reusable media type (see documentation on items), only set on status ``"exchange"``.
:>json boolean simulate: Do not actually perform the check-in, only simulate the response. The ``position`` response
object will not reflect the simulated changes.
**Example request**:
+1
View File
@@ -129,6 +129,7 @@ dev = [
"pytest==9.1.*",
"playwright",
"responses",
"pypdfium2"
]
[project.entry-points."distutils.commands"]
+33 -18
View File
@@ -84,7 +84,8 @@ from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.phone_format import phone_format
from pretix.helpers.daterange import datetimerange
from pretix.helpers.reportlab import (
ThumbnailingImageReader, register_ttf_font_if_new, reshaper,
ThumbnailingImageReader, find_font_supporting_text,
register_ttf_font_if_new, reshaper,
)
from pretix.presale.style import get_fonts
@@ -827,7 +828,10 @@ class Renderer:
else:
self.bg_bytes = None
self.bg_pdf = None
self.event_fonts = list(get_fonts(event, pdf_support_required=True).keys()) + ['Open Sans']
event_fonts = get_fonts(event, pdf_support_required=True) | {'Open Sans': {"bold", "italic", "bolditalic"}}
# sorted by font name to match ordering of libpretixprint
self.event_fonts = dict(sorted(event_fonts.items(), key=lambda x: x[0]))
@classmethod
def _register_fonts(cls, event: Event = None):
@@ -1026,7 +1030,25 @@ class Renderer:
)
canvas.restoreState()
def _text_paragraph(self, op: OrderPosition, order: Order, o: dict, legacy_lineheight=False, override_fontsize=None):
def _prepare_text_paragraph_text(self, op: OrderPosition, order: Order, o: dict):
# add an almost-invisible space &hairsp; after hyphens as word-wrap in ReportLab only works on space chars
text = conditional_escape(
self._get_text_content(op, order, o) or "",
).replace("\n", "<br/>\n").replace("-", "-&hairsp;")
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
text = "<br/>".join(get_display(reshaper.reshape(l)) for l in text.split("<br/>"))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
return text
def _get_text_paragraph_font(self, o: dict, text: str):
font = o['fontfamily']
# Since pdfmetrics.registerFont is global, we want to make sure that no one tries to sneak in a font, they
@@ -1040,6 +1062,14 @@ class Renderer:
if o['italic']:
font += ' I'
font = find_font_supporting_text(self.event_fonts, text, font)
return font
def _text_paragraph(self, op: OrderPosition, order: Order, o: dict, legacy_lineheight=False, override_fontsize=None):
text = self._prepare_text_paragraph_text(op, order, o)
font = self._get_text_paragraph_font(o, text)
fontsize = override_fontsize if override_fontsize is not None else float(o['fontsize'])
try:
ad = getAscentDescent(font, fontsize)
@@ -1068,21 +1098,6 @@ class Renderer:
alignment=align_map[o['align']],
splitLongWords=o.get('splitlongwords', True),
)
# add an almost-invisible space &hairsp; after hyphens as word-wrap in ReportLab only works on space chars
text = conditional_escape(
self._get_text_content(op, order, o) or "",
).replace("\n", "<br/>\n").replace("-", "-&hairsp;")
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
text = "<br/>".join(get_display(reshaper.reshape(l)) for l in text.split("<br/>"))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
p = Paragraph(text, style=style) # not using AutoEscapeParagraph is safe as we escape above
return p, ad, lineheight
+2 -2
View File
@@ -21,8 +21,8 @@
#
import contextvars
from contextlib import contextmanager
from datetime import datetime
from dateutil.parser import parse
from django.utils.timezone import now
from pretix.base.auth import has_event_access_permission
@@ -34,7 +34,7 @@ timemachine_now_var = contextvars.ContextVar('timemachine_now', default=None)
def time_machine_now_assigned_from_request(request):
if hasattr(request, 'event') and f'timemachine_now_dt:{request.event.pk}' in request.session and \
request.event.testmode and has_event_access_permission(request):
request.now_dt = parse(request.session[f'timemachine_now_dt:{request.event.pk}'])
request.now_dt = datetime.fromisoformat(request.session[f'timemachine_now_dt:{request.event.pk}'])
request.now_dt_is_fake = True
else:
request.now_dt = now()
+16
View File
@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
import logging
import multiprocessing
import os
from celery import Celery, signals
@@ -54,6 +55,21 @@ def on_task_received(sender, request, **kwargs):
logger.info(f"Task {request.id} has trace {trace}")
@receiver(signals.after_setup_task_logger)
def on_after_setup_task_logger(sender, logger, loglevel, logfile, format, colorize, **kwargs):
# This hack seems to be required to get celery to log internal events from eg billiard/pool.py such as
# "worker killed because it used too much memory"
# You can test that it is working by starting a celery worker with a low value like
# --max-memory-per-child 300000
# and then trigger a task. Result should look like this:
# [2026-09-23 10:42:26,234: WARNING/ForkPoolWorker-16]: [???:???] child process exiting after exceeding memory limit (394540KiB / 300000KiB)
# The ???:??? are likely because by copying the handlers, we are also copying the format, but I was unable to find
# a better compatible way.
multi_logger = multiprocessing.get_logger()
multi_logger.setLevel(logging.WARNING)
multi_logger.handlers = logger.handlers
@receiver(signals.task_prerun)
def on_task_prerun(sender, task_id, task, **kwargs):
from pretix.helpers.logs import local
+1 -1
View File
@@ -946,7 +946,7 @@ class UserEmailConfirmView(FormView):
@transaction.atomic()
def form_valid(self, form):
reason = self.request.GET['reason']
reason = self.request.GET.get('reason')
if reason not in ('email_change', 'email_verify'):
raise PermissionDenied
try:
+10
View File
@@ -27,6 +27,7 @@ from http import cookies
from django.conf import settings
from django.core.exceptions import SuspiciousFileOperation
from django.middleware.csrf import CsrfViewMiddleware as BaseCsrfMiddleware
from PIL import Image
from requests.adapters import HTTPAdapter
from urllib3.connection import HTTPConnection, HTTPSConnection
@@ -42,6 +43,7 @@ from urllib3.util.timeout import _DEFAULT_TIMEOUT
from pretix.helpers.reportlab import ThumbnailingImageReader
from pretix.helpers.ssrf import should_block_access
from pretix.multidomain.middlewares import CsrfViewMiddleware
def monkeypatch_vobject_performance():
@@ -242,6 +244,13 @@ def monkeypatch_reportlab_imagereader():
utils.ImageReader.__init__ = new_init
def monkeypatch_csrf_middleware():
# Some views from django or plugins use the middleware or the derived decorators directly,
# so we have to also patch the stock middleware
BaseCsrfMiddleware._get_secret = CsrfViewMiddleware._get_secret
BaseCsrfMiddleware._set_csrf_cookie = CsrfViewMiddleware._set_csrf_cookie
def monkeypatch_all_at_ready():
monkeypatch_vobject_performance()
monkeypatch_pillow_safer()
@@ -249,3 +258,4 @@ def monkeypatch_all_at_ready():
monkeypatch_urllib3_ssrf_protection()
monkeypatch_cookie_morsel()
monkeypatch_reportlab_imagereader()
monkeypatch_csrf_middleware()
+29 -26
View File
@@ -89,39 +89,42 @@ def normalize_text(text: str) -> str:
return text
def font_supports_text(text, font_name):
if not text:
return True
font = pdfmetrics.getFont(font_name)
if not isinstance(font, TTFont):
return True
return all(
ord(c) in font.face.charToGlyph or not c.isprintable()
for c in text
)
def find_font_supporting_text(fonts, text, preferred_font):
if font_supports_text(text, preferred_font):
return preferred_font
for family, styles in fonts.items():
if font_supports_text(text, family):
if (preferred_font.endswith("It") or preferred_font.endswith(" I")) and "italic" in styles:
return family + " I"
if (preferred_font.endswith("Bd") or preferred_font.endswith(" B")) and "bold" in styles:
return family + " B"
return family
return preferred_font
class FontFallbackParagraph(Paragraph):
def __init__(self, text, style=None, *args, **kwargs):
if style is None:
style = ParagraphStyle(name='paragraphImplicitDefaultStyle')
if not self._font_supports_text(text, style.fontName):
newFont = self._find_font(text, style.fontName)
if newFont:
logger.debug(f"replacing {style.fontName} with {newFont} for {text!r}")
style = style.clone(name=style.name + '_' + newFont, fontName=newFont)
supporting_font = find_font_supporting_text(get_fonts(pdf_support_required=True), text, style.fontName)
if supporting_font != style.fontName:
logger.debug(f"replacing {style.fontName} with {supporting_font} for {text!r}")
style = style.clone(name=style.name + '_' + supporting_font, fontName=supporting_font)
super().__init__(text, style, *args, **kwargs)
def _font_supports_text(self, text, font_name):
if not text:
return True
font = pdfmetrics.getFont(font_name)
if not isinstance(font, TTFont):
return True
return all(
ord(c) in font.face.charToGlyph or not c.isprintable()
for c in text
)
def _find_font(self, text, original_font):
for family, styles in get_fonts(pdf_support_required=True).items():
if self._font_supports_text(text, family):
if (original_font.endswith("It") or original_font.endswith(" I")) and "italic" in styles:
return family + " I"
if (original_font.endswith("Bd") or original_font.endswith(" B")) and "bold" in styles:
return family + " B"
return family
class PlainTextParagraph(FontFallbackParagraph):
def __init__(self, text, style=None, linebreaks=True, *args, **kwargs):
-13
View File
@@ -302,19 +302,6 @@ class CsrfViewMiddleware(BaseCsrfMiddleware):
# Content varies with the CSRF cookie, so set the Vary header.
patch_vary_headers(response, ('Cookie',))
def process_response(self, request, response):
if (
not settings.CSRF_USE_SESSIONS
and request.is_secure()
and settings.CSRF_COOKIE_NAME in response.cookies
and response.cookies[settings.CSRF_COOKIE_NAME].value
):
logger.warning("Usage of djangos CsrfViewMiddleware detected (legacy cookie found in response). "
"This may be caused by using csrf_project or requires_csrf_token from django.views.decorators.csrf. "
"Use the pretix.multidomain.middlewares equivalent instead.")
return super().process_response(request, response)
def handle_duplicated_csrftoken(request, response):
# Due to a Safari bug, in some browser, two csrftoken cookies can exist:
-1
View File
@@ -109,7 +109,6 @@ class PaypalSettingsHolder(BasePaymentProvider):
('client_id',
forms.CharField(
label=_('Client ID'),
max_length=80,
min_length=80,
help_text=format_html(
'<a target="_blank" rel="noopener" href="{docs_url}">{text}</a>',
+13
View File
@@ -459,3 +459,16 @@ def _get_variant_unavailability_reason(variant, now_dt: Optional[datetime]=None,
return 'available_until'
else:
return None
def _item_num_options(item):
if item.current_unavailability_reason:
return 0
if item.has_variations:
return len([v for v in item.available_variations if not v.current_unavailability_reason])
else:
return 1
def get_item_option_count(items):
return sum(_item_num_options(item) for item in items)
@@ -31,7 +31,7 @@
{% endif %}
{% for item in items %}
{% if item.has_variations %}
<article aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-legend"{% if item.description %} aria-describedby="{{ form_prefix }}item-{{ item.pk }}-description"{% endif %} class="item-with-variations{% if event.settings.show_variations_expanded %} details-open{% endif %}" id="{{ form_prefix }}item-{{ item.pk }}">
<article aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-legend"{% if item.description %} aria-describedby="{{ form_prefix }}item-{{ item.pk }}-description"{% endif %} class="item-with-variations{% if event.settings.show_variations_expanded or always_show_open %} details-open{% endif %}" id="{{ form_prefix }}item-{{ item.pk }}">
<div class="row product-row headline">
<div class="col-md-8 col-sm-6 col-xs-12">
{% if item.picture %}
@@ -85,7 +85,7 @@
{% endif %}
</div>
<div class="col-md-2 col-sm-3 col-xs-6 availability-box">
{% if not event.settings.show_variations_expanded %}
{% if not event.settings.show_variations_expanded and not always_show_open %}
{% if item.best_variation_availability <= 10 %}
{% if not item.min_price %}
<strong class="gone">{% trans "FULLY BOOKED" %}</strong>
@@ -116,7 +116,7 @@
</div>
<div class="clearfix"></div>
</div>
<div class="variations {% if not event.settings.show_variations_expanded %}variations-collapsed{% endif %}" id="{{ form_prefix }}item-{{ item.pk }}-variations">
<div class="variations {% if not event.settings.show_variations_expanded and not always_show_open %}variations-collapsed{% endif %}" id="{{ form_prefix }}item-{{ item.pk }}-variations">
{% for var in item.available_variations %}
<article aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-legend"{% if var.description %} aria-describedby="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-description"{% endif %} class="row product-row variation" id="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}"
{% if not item.free_price %}
@@ -222,37 +222,54 @@
</div>
{% if var.cached_availability.0 == 100 and not item.current_unavailability_reason and not var.current_unavailability_reason %}
<div class="col-md-2 col-sm-3 col-xs-6 availability-box available">
{% if var.order_max == 1 %}
<label class="btn btn-default btn-checkbox{% if not ev.presale_is_running %} disabled{% endif %}">
<input type="checkbox" value="1"
{% if item.free_price %}
data-checked-onchange="price-variation-{{ item.pk }}-{{ var.pk }}"
{% endif %}
{% if not ev.presale_is_running %}disabled{% endif %}
id="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}"
name="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}"
aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-legend {{ form_prefix }}item-{{ item.pk }}-legend"
{% if var.description %} aria-describedby="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-description"{% endif %}>
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
{% trans "Select" context "checkbox" %}
{% if max_choices == 1 and not form_prefix %}
{# items_from_post_data which parses _selected_item does not support form_prefix at the moment #}
<label>
<input type="radio" name="{{ form_prefix }}_selected_item"
{% if itemnum == 1 %}checked="checked"{% endif %}
{% if not ev.presale_is_running %}disabled{% endif %}
id="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}"
value="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}"
aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-legend"
{% if var.description %}aria-describedby="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-description"{% endif %}
{% if item.free_price %}data-checked-onchange="{{ form_prefix }}price-variation-{{ item.pk }}-{{ var.pk }}"{% endif %}
>
</label>
{% else %}
<fieldset class="input-item-count-group">
<legend class="sr-only">{% trans "Quantity" %}</legend>
<button type="button" data-step="-1" data-controls="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-dec" aria-label="- {{ item }}, {{ var }}: {% trans "Decrease quantity" %}"
{% if not ev.presale_is_running %}disabled{% endif %}>-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
{% if not ev.presale_is_running %}disabled{% endif %}
{% if var.order_max == 1 %}
<label class="btn btn-default btn-checkbox{% if not ev.presale_is_running %} disabled{% endif %}">
<input type="checkbox" value="1"
{% if item.free_price %}
data-checked-onchange="price-variation-{{ item.pk }}-{{ var.pk }}"
data-checked-onchange="price-variation-{{ item.pk }}-{{ var.pk }}"
{% endif %}
max="{{ var.order_max }}"
id="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}"
name="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}"
aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-legend {{ form_prefix }}item-{{ item.pk }}-legend">
<button type="button" data-step="1" data-controls="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-inc" aria-label="+ {{ item }}, {{ var }}: {% trans "Increase quantity" %}"
{% if not ev.presale_is_running %}disabled{% endif %}>+</button>
</fieldset>
{% if not ev.presale_is_running %}disabled{% endif %}
{% if itemnum == 1 %}checked="checked"{% endif %}
id="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}"
name="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}"
aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-legend {{ form_prefix }}item-{{ item.pk }}-legend"
{% if var.description %}aria-describedby="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-description"{% endif %}>
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
{% trans "Select" context "checkbox" %}
</label>
{% else %}
<fieldset class="input-item-count-group">
<legend class="sr-only">{% trans "Quantity" %}</legend>
<button type="button" data-step="-1" data-controls="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-dec" aria-label="- {{ item }}, {{ var }}: {% trans "Decrease quantity" %}"
{% if not ev.presale_is_running %}disabled{% endif %}>-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
{% if not ev.presale_is_running %}disabled{% endif %}
{% if item.free_price %}
data-checked-onchange="price-variation-{{ item.pk }}-{{ var.pk }}"
{% endif %}
max="{{ var.order_max }}"
{% if itemnum == 1 %}checked="checked"{% endif %}
id="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}"
name="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}"
aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-legend {{ form_prefix }}item-{{ item.pk }}-legend">
<button type="button" data-step="1" data-controls="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-inc" aria-label="+ {{ item }}, {{ var }}: {% trans "Increase quantity" %}"
{% if not ev.presale_is_running %}disabled{% endif %}>+</button>
</fieldset>
{% endif %}
{% endif %}
</div>
{% else %}
@@ -387,37 +404,52 @@
</div>
{% if item.cached_availability.0 == 100 and not item.current_unavailability_reason %}
<div class="col-md-2 col-sm-3 col-xs-6 availability-box available">
{% if item.order_max == 1 %}
<label class="btn btn-default btn-checkbox{% if not ev.presale_is_running %} disabled{% endif %}">
<input type="checkbox" value="1" {% if itemnum == 1 %}checked{% endif %}
{% if item.free_price %}
data-checked-onchange="price-item-{{ item.pk }}"
{% endif %}
{% if not ev.presale_is_running %}disabled{% endif %}
name="{{ form_prefix }}item_{{ item.id }}" id="{{ form_prefix }}item_{{ item.id }}"
aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-legend"
{% if item.description %} aria-describedby="{{ form_prefix }}item-{{ item.id }}-description"{% endif %}>
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
{% trans "Select" context "checkbox" %}
{% if max_choices == 1 and not form_prefix %}
{# items_from_post_data which parses _selected_item does not support form_prefix at the moment #}
<label>
<input type="radio" name="{{ form_prefix }}_selected_item"
{% if itemnum == 1 %}checked="checked"{% endif %}
{% if not ev.presale_is_running %}disabled{% endif %}
id="{{ form_prefix }}item_{{ item.id }}"
value="{{ form_prefix }}item_{{ item.id }}"
aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-legend"
{% if item.description %}aria-describedby="{{ form_prefix }}item-{{ item.id }}-description"{% endif %}
{% if item.free_price %}data-checked-onchange="{{ form_prefix }}price-item-{{ item.pk }}"{% endif %}
>
</label>
{% else %}
<fieldset class="input-item-count-group">
<legend class="sr-only">{% trans "Quantity" %}</legend>
<button type="button" data-step="-1" data-controls="{{ form_prefix }}item_{{ item.id }}" class="btn btn-default input-item-count-dec" aria-label="- {{ item }}: {% trans "Decrease quantity" %}"
{% if not ev.presale_is_running %}disabled{% endif %}>-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
{% if not ev.presale_is_running %}disabled{% endif %}
{% if itemnum == 1 %}value="1"{% endif %}
{% if item.order_max == 1 %}
<label class="btn btn-default btn-checkbox{% if not ev.presale_is_running %} disabled{% endif %}">
<input type="checkbox" value="1" {% if itemnum == 1 %}checked{% endif %}
{% if item.free_price %}
data-checked-onchange="price-item-{{ item.pk }}"
data-checked-onchange="price-item-{{ item.pk }}"
{% endif %}
max="{{ item.order_max }}"
name="{{ form_prefix }}item_{{ item.id }}"
id="{{ form_prefix }}item_{{ item.id }}"
aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-legend">
<button type="button" data-step="1" data-controls="{{ form_prefix }}item_{{ item.id }}" class="btn btn-default input-item-count-inc" aria-label="+ {{ item }}: {% trans "Increase quantity" %}"
{% if not ev.presale_is_running %}disabled{% endif %}>+</button>
</fieldset>
{% if not ev.presale_is_running %}disabled{% endif %}
name="{{ form_prefix }}item_{{ item.id }}" id="{{ form_prefix }}item_{{ item.id }}"
aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-legend"
{% if item.description %}aria-describedby="{{ form_prefix }}item-{{ item.id }}-description"{% endif %}>
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
{% trans "Select" context "checkbox" %}
</label>
{% else %}
<fieldset class="input-item-count-group">
<legend class="sr-only">{% trans "Quantity" %}</legend>
<button type="button" data-step="-1" data-controls="{{ form_prefix }}item_{{ item.id }}" class="btn btn-default input-item-count-dec" aria-label="- {{ item }}: {% trans "Decrease quantity" %}"
{% if not ev.presale_is_running %}disabled{% endif %}>-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
{% if not ev.presale_is_running %}disabled{% endif %}
{% if itemnum == 1 %}value="1"{% endif %}
{% if item.free_price %}
data-checked-onchange="price-item-{{ item.pk }}"
{% endif %}
max="{{ item.order_max }}"
name="{{ form_prefix }}item_{{ item.id }}"
id="{{ form_prefix }}item_{{ item.id }}"
aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-legend">
<button type="button" data-step="1" data-controls="{{ form_prefix }}item_{{ item.id }}" class="btn btn-default input-item-count-inc" aria-label="+ {{ item }}: {% trans "Increase quantity" %}"
{% if not ev.presale_is_running %}disabled{% endif %}>+</button>
</fieldset>
{% endif %}
{% endif %}
</div>
{% else %}
@@ -38,7 +38,7 @@
{% endif %}
<p>
{% if options == 0 and not seating_available %}
{% if itemnum == 0 and not seating_available %}
{% if request.event.has_subevents and not voucher.subevent %}
{% blocktrans trimmed %}
For the selected date, there are currently no products available that can be bought with this voucher. Please try a different date or a different voucher.
@@ -73,353 +73,10 @@
{% endif %}
{% endif %}
{% for tup in items_by_category %}
<section {% if tup.0 %}aria-labelledby="category-{{ tup.0.id }}"{% else %}aria-label="{% trans "Uncategorized items" %}"{% endif %}{% if tup.0.description %} aria-describedby="category-info-{{ tup.0.id }}"{% endif %}>
{% if tup.0 %}
<h3 id="category-{{ tup.0.id }}">{{ tup.0.name }}</h3>
{% if tup.0.description %}
<div id="category-info-{{ tup.0.id }}">{{ tup.0.description|localize|rich_text }}</div>
{% endif %}
{% endif %}
{% for item in tup.1 %}
{% if item.has_variations %}
<article aria-labelledby="item-{{ item.pk }}-legend"{% if item.description %} aria-describedby="item-{{ item.pk }}-description"{% endif %} class="item-with-variations" id="item-{{ item.pk }}">
<div class="row product-row headline">
<div class="col-md-8 col-sm-6 col-xs-12">
{% if item.picture %}
<a href="{{ item.picture.url }}" class="productpicture"
data-title="{{ item.name|force_escape|force_escape }}"
{# Yes, double-escape to prevent XSS in lightbox #}
data-lightbox="{{ item.id }}"
aria-label="{% blocktrans trimmed with item=item.name %}Show full-size image of {{ item }}{% endblocktrans %}">
<img src="{{ item.picture|thumb:'60x60^' }}"
alt="{{ item.name }}"/>
</a>
{% endif %}
<div class="product-description {% if item.picture %}with-picture{% endif %}">
<h4 id="item-{{ item.pk }}-legend">
{{ item.name }}
</h4>
{% if item.description %}
<div id="item-{{ item.pk }}-description" class="product-description">
{{ item.description|localize|rich_text }}
</div>
{% endif %}
</div>
</div>
<div class="col-md-2 col-sm-3 col-xs-6 price">
{% if item.min_price != item.max_price or item.free_price %}
{% blocktrans trimmed with minprice=item.min_price|money:event.currency %}
from {{ minprice }}
{% endblocktrans %}
{% elif not item.min_price and not item.max_price %}
{% if not item.mandatory_priced_addons or voucher.all_addons_included %}
<span class="text-uppercase">{% trans "free" context "price" %}</span>
{% endif %}
{% else %}
{{ item.min_price|money:event.currency }}
{% endif %}
</div>
<div class="col-md-2 col-sm-3 col-xs-6 availability-box">
</div>
<div class="clearfix"></div>
</div>
<div class="variations">
{% for var in item.available_variations %}
<article aria-labelledby="item-{{ item.pk }}-{{ var.pk }}-legend"{% if var.description %} aria-describedby="item-{{ item.pk }}-{{ var.pk }}-description"{% endif %} class="row product-row variation" id="item-{{ item.pk }}-{{ var.pk }}"
{% if not item.free_price %}
data-price="{% if event.settings.display_net_prices %}{{ var.display_price.net|unlocalize }}{% else %}{{ var.display_price.gross|unlocalize }}{% endif %}"
{% endif %}>
<div class="col-md-8 col-sm-6 col-xs-12">
<h5 id="item-{{ item.pk }}-{{ var.pk }}-legend">{{ var }}</h5>
{% if var.description %}
<div id="item-{{ item.pk }}-{{ var.pk }}-description" class="variation-description">
{{ var.description|localize|rich_text }}
</div>
{% endif %}
{% if item.do_show_quota_left %}
{% include "pretixpresale/event/fragment_quota_left.html" with avail=var.cached_availability %}
{% endif %}
</div>
<div class="col-md-2 col-sm-3 col-xs-6 price">
{% if var.original_price %}
<p>
<del><span class="sr-only">{% trans "Original price:" %}</span>
{% if event.settings.display_net_prices %}
{{ var.original_price.net|money:event.currency }}
{% else %}
{{ var.original_price.gross|money:event.currency }}
{% endif %}
</del>
{% if item.free_price %}
</p>
{% else %}
<ins><span class="sr-only">{% trans "New price:" %}</span>
{% endif %}
{% elif not item.free_price %}
<p>
{% endif %}
{% if item.free_price %}
<label class="sr-only" for="price-variation-{{ item.pk }}-{{ var.pk }}">{% blocktrans trimmed with item=var.value %}Modify price for {{ item }}{% endblocktrans %}</label>
<div class="input-group input-group-price">
<span class="input-group-addon">{{ event.currency }}</span>
<input type="number" class="form-control input-item-price"
id="price-variation-{{ item.pk }}-{{ var.pk }}"
placeholder="0"
min="{% if event.settings.display_net_prices %}{{ var.display_price.net|stringformat:"0.2f" }}{% else %}{{ var.display_price.gross|stringformat:"0.2f" }}{% endif %}"
name="price_{{ item.id }}_{{ var.id }}"
{% if var.suggested_price.gross != var.display_price.gross %}
{% if event.settings.display_net_prices %}
title="{% blocktrans trimmed with item=var.value price=var.display_price.net|money:event.currency %}Modify price for {{ item }}, at least {{ price }}{% endblocktrans %}"
{% else %}
title="{% blocktrans trimmed with item=var.value price=var.display_price.gross|money:event.currency %}Modify price for {{ item }}, at least {{ price }}{% endblocktrans %}"
{% endif %}
{% else %}
title="{% blocktrans trimmed with item=var.value %}Modify price for {{ item }}{% endblocktrans %}"
{% endif %}
value="{% if event.settings.display_net_prices %}{{var.suggested_price.net|stringformat:"0.2f" }}{% else %}{{ var.suggested_price.gross|stringformat:"0.2f" }}{% endif %}"
step="any">
</div>
<p>
{% elif not var.display_price.gross %}
{% if not item.mandatory_priced_addons or var.original_price or voucher.all_addons_included %}
<span class="text-uppercase">{% trans "free" context "price" %}</span>
{% endif %}
{% elif event.settings.display_net_prices %}
{{ var.display_price.net|money:event.currency }}
{% else %}
{{ var.display_price.gross|money:event.currency }}
{% endif %}
{% if item.original_price or var.original_price %}
</ins>
{% endif %}
{% if item.includes_mixed_tax_rate %}
{% if event.settings.display_net_prices %}
<small>{% trans "plus taxes" %}</small>
{% else %}
<small>{% trans "incl. taxes" %}</small>
{% endif %}
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=var.display_price.gross|money:event.currency %}{{ value }} incl. taxes{% endblocktrans %}" data-placement="bottom">
{% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}
</small>
{% elif var.display_price.rate and var.display_price.gross %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=var.display_price.net|money:event.currency %}{{ value }} without taxes{% endblocktrans %}" data-placement="bottom">
{% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}
</small>
{% endif %}
</p>
</div>
{% if var.cached_availability.0 == 100 and not item.current_unavailability_reason %}
<div class="col-md-2 col-sm-3 col-xs-6 availability-box available radio-box">
{% if max_times > 1 %}
{% if var.order_max == 1 %}
<label class="btn btn-default btn-checkbox">
<input type="checkbox"
value="1"
id="variation_{{ item.id }}_{{ var.id }}"
name="variation_{{ item.id }}_{{ var.id }}"
{% if options == 1 %}checked{% endif %}
aria-labelledby="item-{{ item.pk }}-{{ var.pk }}-legend item-{{ item.pk }}-legend"
{% if var.description %} aria-describedby="item-{{ item.pk }}-{{ var.pk }}-description"{% endif %}>
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
{% trans "Select" context "checkbox" %}
</label>
{% else %}
<fieldset class="input-item-count-group">
<legend class="sr-only">{% trans "Quantity" %}</legend>
<button type="button" data-step="-1" data-controls="variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-dec" aria-label="- {{ item }}, {{ var }}: {% trans "Decrease quantity" %}">-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
max="{{ var.order_max }}"
id="variation_{{ item.id }}_{{ var.id }}"
name="variation_{{ item.id }}_{{ var.id }}"
{% if options == 1 %}value="1"{% endif %}
aria-labelledby="item-{{ item.pk }}-{{ var.pk }}-legend item-{{ item.pk }}-legend">
<button type="button" data-step="1" data-controls="variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-inc" aria-label="+ {{ item }}, {{ var }}: {% trans "Increase quantity" %}">+</button>
</fieldset>
{% endif %}
{% else %}
<label>
<input type="radio" name="_voucher_item"
{% if options == 1 %}checked="checked"{% endif %}
id="variation_{{ item.id }}_{{ var.id }}"
value="variation_{{ item.id }}_{{ var.id }}"
aria-labelledby="item-{{ item.pk }}-{{ var.pk }}-legend"
{% if var.description %} aria-describedby="item-{{ item.pk }}-{{ var.pk }}-description"{% endif %}>
</label>
{% endif %}
</div>
{% else %}
{% include "pretixpresale/event/fragment_availability.html" with price=var.display_price.gross original_price=var.original_price.gross avail=var.cached_availability.0 item=item %}
{% endif %}
<div class="clearfix"></div>
</article>
{% endfor %}
</div>
</article>
{% else %}
<article aria-labelledby="item-{{ item.pk }}-legend"{% if item.description %} aria-describedby="item-{{ item.pk }}-description"{% endif %} class="row product-row simple" id="item-{{ item.pk }}"
{% if not item.free_price %}
data-price="{% if event.settings.display_net_prices %}{{ item.display_price.net|unlocalize }}{% else %}{{ item.display_price.gross|unlocalize }}{% endif %}"
{% endif %}>
<div class="col-md-8 col-sm-6 col-xs-12">
{% if item.picture %}
<a href="{{ item.picture.url }}" class="productpicture"
data-title="{{ item.name|force_escape|force_escape }}"
{# Yes, double-escape to prevent XSS in lightbox #}
data-lightbox="{{ item.id }}"
aria-label="{% blocktrans trimmed with item=item.name %}Show full-size image of {{ item }}{% endblocktrans %}">
<img src="{{ item.picture|thumb:'60x60^' }}"
alt="{{ item.name }}"/>
</a>
{% endif %}
<div class="product-description {% if item.picture %}with-picture{% endif %}">
<h4 id="item-{{ item.pk }}-legend">{{ item.name }}</h4>
{% if item.description %}
<div id="item-{{ item.pk }}-description" class="product-description">
{{ item.description|localize|rich_text }}
</div>
{% endif %}
{% if item.do_show_quota_left %}
{% include "pretixpresale/event/fragment_quota_left.html" with avail=item.cached_availability %}
{% endif %}
{% if item.min_per_order and item.min_per_order > 1 %}
<p>
<small>
{% blocktrans trimmed with num=item.min_per_order %}
minimum amount to order: {{ num }}
{% endblocktrans %}
</small>
</p>
{% endif %}
</div>
</div>
<div class="col-md-2 col-sm-3 col-xs-6 price">
{% if item.original_price %}
<p>
<del><span class="sr-only">{% trans "Original price:" %}</span>
{% if event.settings.display_net_prices %}
{{ item.original_price.net|money:event.currency }}
{% else %}
{{ item.original_price.gross|money:event.currency }}
{% endif %}
</del>
{% if item.free_price %}
</p>
{% else %}
<ins><span class="sr-only">{% trans "New price:" %}</span>
{% endif %}
{% elif not item.free_price %}
<p>
{% endif %}
{% if item.free_price %}
<label class="sr-only" for="price-item-{{ item.pk }}">{% blocktrans trimmed with item=item.name currency=event.currency %}Set price in {{ currency }} for {{ item }}{% endblocktrans %}</label>
<div class="input-group input-group-price">
<span class="input-group-addon">{{ event.currency }}</span>
<input type="number" class="form-control input-item-price" placeholder="0"
min="{% if event.settings.display_net_prices %}{{ item.display_price.net|stringformat:"0.2f" }}{% else %}{{ item.display_price.gross|stringformat:"0.2f" }}{% endif %}"
name="price_{{ item.id }}"
{% if item.suggested_price.gross != item.display_price.gross %}
{% if event.settings.display_net_prices %}
title="{% blocktrans trimmed with item=item.name price=item.display_price.net|money:event.currency %}Modify price for {{ item }}, at least {{ price }}{% endblocktrans %}"
{% else %}
title="{% blocktrans trimmed with item=item.name price=item.display_price.gross|money:event.currency %}Modify price for {{ item }}, at least {{ price }}{% endblocktrans %}"
{% endif %}
{% else %}
title="{% blocktrans trimmed with item=item.name %}Modify price for {{ item }}{% endblocktrans %}"
{% endif %}
value="{% if event.settings.display_net_prices %}{{ item.suggested_price.net|stringformat:"0.2f" }}{% else %}{{ item.suggested_price.gross|stringformat:"0.2f" }}{% endif %}"
step="any">
</div>
<p>
{% elif not item.display_price.gross %}
{% if not item.mandatory_priced_addons or item.original_price or voucher.all_addons_included %}
<span class="text-uppercase">{% trans "free" context "price" %}</span>
{% endif %}
{% elif event.settings.display_net_prices %}
{{ item.display_price.net|money:event.currency }}
{% else %}
{{ item.display_price.gross|money:event.currency }}
{% endif %}
{% if item.original_price %}
</ins>
{% endif %}
{% if item.includes_mixed_tax_rate %}
{% if event.settings.display_net_prices %}
<small>{% trans "plus taxes" %}</small>
{% else %}
<small>{% trans "incl. taxes" %}</small>
{% endif %}
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=item.display_price.gross|money:event.currency %}{{ value }} incl. taxes{% endblocktrans %}" data-placement="bottom">
{% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}
</small>
{% elif item.display_price.rate and item.display_price.gross %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=item.display_price.net|money:event.currency %}{{ value }} without taxes{% endblocktrans %}" data-placement="bottom">
{% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}
</small>
{% endif %}
</p>
</div>
{% if item.cached_availability.0 == 100 and not item.current_unavailability_reason %}
<div class="col-md-2 col-sm-3 col-xs-6 availability-box available radio-box">
{% if max_times > 1 %}
{% if item.order_max == 1 %}
<label class="btn btn-default btn-checkbox">
<input type="checkbox"
value="1"
id="item_{{ item.id }}"
name="item_{{ item.id }}"
{% if options == 1 %}checked{% endif %}
aria-labelledby="item-{{ item.pk }}-legend"
{% if item.description %} aria-describedby="item-{{ item.id }}-description"{% endif %}>
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
{% trans "Select" context "checkbox" %}
</label>
{% else %}
<fieldset class="input-item-count-group">
<legend class="sr-only">{% trans "Quantity" %}</legend>
<button type="button" data-step="-1" data-controls="item_{{ item.id }}" class="btn btn-default input-item-count-dec" aria-label="- {{ item }}: {% trans "Decrease quantity" %}">-</button>
<input type="number" class="form-control input-item-count"
placeholder="0" min="0"
max="{{ item.order_max }}"
id="item_{{ item.id }}"
name="item_{{ item.id }}"
{% if options == 1 %}value="1"{% endif %}
aria-labelledby="item-{{ item.pk }}-legend">
<button type="button" data-step="1" data-controls="item_{{ item.id }}" class="btn btn-default input-item-count-inc" aria-label="+ {{ item }}: {% trans "Increase quantity" %}">+</button>
</fieldset>
{% endif %}
{% else %}
<label>
<input type="radio" name="_voucher_item"
{% if options == 1 %}checked="checked"{% endif %}
id="item_{{ item.id }}"
value="item_{{ item.id }}"
aria-labelledby="item-{{ item.pk }}-legend"
{% if item.description %} aria-describedby="item-{{ item.id }}-description"{% endif %}>
</label>
{% endif %}
</div>
{% else %}
{% include "pretixpresale/event/fragment_availability.html" with price=item.display_price.gross original_price=item.original_price.gross avail=item.cached_availability.0 item=item %}
{% endif %}
<div class="clearfix"></div>
</article>
{% endif %}
{% endfor %}
</section>
{% endfor %}
{% include "pretixpresale/event/fragment_product_list.html" with ev=event always_show_open=True %}
{% eventsignal event "pretix.presale.signals.voucher_redeem_info" voucher=voucher %}
{% if event.presale_is_running and options > 0 %}
{% if event.presale_is_running and itemnum > 0 %}
<div class="row checkout-button-row">
<div class="col-md-4 col-md-offset-8 col-xs-12 text-center">
{% if voucher.min_usages_remaining > 1 %}
+5 -6
View File
@@ -70,7 +70,7 @@ from pretix.helpers.http import redirect_to_url
from pretix.helpers.safedownload import check_token
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.productlist import (
item_group_by_category, prepare_item_list_for_shop,
get_item_option_count, item_group_by_category, prepare_item_list_for_shop,
)
from pretix.presale.views import (
CartMixin, EventViewMixin, allow_cors_if_namespaced,
@@ -239,9 +239,9 @@ def _items_from_post_data(request, warn_if_empty=True):
# Compatibility patch that makes the frontend code a lot easier
req_items = list(request.POST.lists())
if '_voucher_item' in request.POST and '_voucher_code' in request.POST:
if '_selected_item' in request.POST:
req_items.append((
'%s' % request.POST['_voucher_item'], ('1',)
'%s' % request.POST['_selected_item'], ('1',)
))
pass
@@ -669,7 +669,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
context = super().get_context_data(**kwargs)
context['voucher'] = self.voucher
context['max_times'] = self.voucher.max_usages - self.voucher.redeemed
context['max_choices'] = self.voucher.max_usages - self.voucher.redeemed
# Fetch all items
items, display_add_to_cart = prepare_item_list_for_shop(
@@ -687,8 +687,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
# Calculate how many options the user still has. If there is only one option, we can
# check the box right away ;)
context['options'] = sum([(len(item.available_variations) if item.has_variations else 1)
for item in items])
context['itemnum'] = get_item_option_count(items)
context['allfree'] = all(
item.display_price.gross == Decimal('0.00') and not item.mandatory_priced_addons
+12 -9
View File
@@ -41,7 +41,6 @@ from importlib import import_module
from urllib.parse import urlencode
import isoweek
from dateutil import parser
from django import forms
from django.conf import settings
from django.contrib import messages
@@ -73,7 +72,7 @@ from pretix.helpers.http import redirect_to_url
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.ical import get_public_ical
from pretix.presale.productlist import (
item_group_by_category, prepare_item_list_for_shop,
get_item_option_count, item_group_by_category, prepare_item_list_for_shop,
)
from pretix.presale.signals import seatingframe_html_head
from pretix.presale.views.organizer import (
@@ -228,7 +227,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
break
items = [i for i in items if not i.requires_seat]
context['itemnum'] = len(items)
context['itemnum'] = get_item_option_count(items)
context['allfree'] = all(
item.display_price.gross == Decimal('0.00') and not item.mandatory_priced_addons
for item in items if not item.has_variations
@@ -589,7 +588,9 @@ class EventAuth(View):
class TimemachineForm(forms.Form):
now_dt = forms.SplitDateTimeField(
label=_('Fake date time'),
widget=SplitDateTimePickerWidget(),
widget=SplitDateTimePickerWidget(
min_date=date(2000, 1, 1),
),
initial=lambda: now().astimezone(get_current_timezone()),
)
@@ -603,12 +604,14 @@ class EventTimeMachine(EventViewMixin, TemplateView):
raise PermissionDenied(_('You are not allowed to access time machine mode.'))
if not request.event.testmode:
raise PermissionDenied(_('This feature is only available in test mode.'))
initial = {}
if now_dt := request.session.get(f'timemachine_now_dt:{request.event.pk}', None):
initial['now_dt'] = datetime.fromisoformat(now_dt)
self.timemachine_form = TimemachineForm(
data=request.method == 'POST' and request.POST or None,
initial=(
{'now_dt': parser.parse(request.session.get(f'timemachine_now_dt:{request.event.pk}', None))}
if request.session.get(f'timemachine_now_dt:{request.event.pk}', None) else {}
)
initial=initial
)
def get_context_data(self, **kwargs):
@@ -622,7 +625,7 @@ class EventTimeMachine(EventViewMixin, TemplateView):
messages.success(self.request, _('Time machine disabled!'))
return redirect(self.get_success_url())
elif self.timemachine_form.is_valid():
request.session[f'timemachine_now_dt:{request.event.pk}'] = str(self.timemachine_form.cleaned_data['now_dt'])
request.session[f'timemachine_now_dt:{request.event.pk}'] = self.timemachine_form.cleaned_data['now_dt'].isoformat()
return redirect(eventreverse(request.event, "presale:event.index"))
else:
return self.get(request)
@@ -713,7 +713,7 @@ $(function () {
$('[data-checked-onchange]').each(function () {
let countInput = this
$('#' + this.getAttribute('data-checked-onchange')).on('input', function () {
if (countInput.type === 'checkbox') {
if (countInput.type === 'checkbox' || countInput.type == "radio") {
if (countInput.checked) return
countInput.checked = true
} else if (countInput.type === 'number' && !countInput.valueAsNumber) {
+23 -1
View File
@@ -21,6 +21,8 @@
#
import inspect
import os
import re
from pathlib import Path
import pytest
from django.core.cache import cache
@@ -28,7 +30,6 @@ from django.test import override_settings
from django.utils import translation
from django_scopes import scopes_disabled
from fakeredis import FakeRedisConnection
from hierarkey.proxy import dirty_cache_keys
from xdist.dsession import DSession
from pretix.testutils.mock import get_redis_connection
@@ -85,6 +86,7 @@ def reset_locale():
@pytest.fixture(autouse=True)
def reset_hierarkey_cache_state():
from hierarkey.proxy import dirty_cache_keys
dirty_cache_keys.set(set())
@@ -143,3 +145,23 @@ def set_lock_namespaces(request):
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
def pytest_addoption(parser):
parser.addoption(
'--pdf-dir', action='store', help="output dir for files generated by pdf tests", type=Path
)
parser.addoption(
'--pdf-tmp-keep', action='store_true', help="keep the temporary directory created for pdf tests"
)
@pytest.fixture
def pdf_dir(request, tmp_path_factory):
dirname = re.sub('[^a-zA-Z0-9]+', '_', request.node.name).strip("_")
if dir := request.config.getoption('--pdf-dir'):
pdf_dir: Path = (dir / dirname).absolute()
pdf_dir.mkdir(exist_ok=True, parents=True)
yield pdf_dir
else:
yield tmp_path_factory.mktemp(dirname)
+18
View File
@@ -242,6 +242,24 @@ def test_cookie_domain_on_main_domain(env, client):
assert r.client.cookies['pretix_session']['domain'] == ''
@pytest.fixture
def csrf_client():
from django.test import Client
return Client(enforce_csrf_checks=True)
@pytest.mark.django_db
@pytest.mark.parametrize("url", [
'/mrmcd/2015/',
'/ensure_csrf_cookie/',
])
def test_csrf_secure(env, url, csrf_client):
r = csrf_client.get(url, secure=True, HTTP_HOST='example.com')
assert 'pretix_csrftoken' not in r.client.cookies
assert '__Host-pretix_csrftoken' in r.client.cookies
assert r.client.cookies['__Host-pretix_csrftoken']['secure']
@pytest.mark.django_db
@override_settings(USE_X_FORWARDED_HOST=True)
def test_with_forwarded_host(env, client):
@@ -0,0 +1,99 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/PageMode /UseNone
/Pages 2 0 R
/Type /Catalog
>>
endobj
2 0 obj
<<
/Count 1
/Kids [ 3 0 R ]
/Type /Pages
>>
endobj
3 0 obj
<<
/Contents 4 0 R
/MediaBox [ 0 0 595.2756 841.8898 ]
/Parent 2 0 R
/Resources <<
/Font 5 0 R
/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/CropBox [ 10 10 307.84 220 ]
>>
endobj
4 0 obj
<<
/Length 117
>>
stream
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
endstream
endobj
5 0 obj
<<
/F1 6 0 R
>>
endobj
6 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
7 0 obj
<<
/Author (anonymous)
/CreationDate (D\07220260917173038\05302\04700\047)
/Creator (anonymous)
/Keywords ()
/ModDate (D\07220260917173038\05302\04700\047)
/Producer (ReportLab PDF Library \055 \050opensource\051)
/Subject (unspecified)
/Title (untitled)
/Trapped /False
>>
endobj
xref
0 8
0000000000 65535 f
0000000015 00000 n
0000000083 00000 n
0000000142 00000 n
0000000371 00000 n
0000000539 00000 n
0000000570 00000 n
0000000677 00000 n
trailer
<<
/Size 8
/Root 1 0 R
/Info 7 0 R
/ID [ <a3fd776687419b9853cf896bf5d6dc96> <a3fd776687419b9853cf896bf5d6dc96> ]
>>
startxref
966
%%EOF
@@ -0,0 +1,213 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/Producer (pypdf)
>>
endobj
2 0 obj
<<
/Type /Pages
/Count 2
/Kids [ 4 0 R 11 0 R ]
>>
endobj
3 0 obj
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj
4 0 obj
<<
/Contents 5 0 R
/MediaBox [ 0 0 297.84 210 ]
/Resources <<
/Font 6 0 R
/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
5 0 obj
[ 9 0 R 10 0 R ]
endobj
6 0 obj
<<
/F1 7 0 R
/F1-0 8 0 R
>>
endobj
7 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
8 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
9 0 obj
<<
/Length 170
>>
stream
q
1 0.0 0.0 1 -10 -10 cm
10 10 297.84 210 re
W
n
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1-0 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
Q
endstream
endobj
10 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
11 0 obj
<<
/Contents 12 0 R
/MediaBox [ 0 0 297.84 210 ]
/Resources <<
/Font 13 0 R
/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
12 0 obj
[ 15 0 R 16 0 R ]
endobj
13 0 obj
<<
/F1 14 0 R
/F1-0 8 0 R
>>
endobj
14 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
15 0 obj
<<
/Length 170
>>
stream
q
1 0.0 0.0 1 -10 -10 cm
10 10 297.84 210 re
W
n
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1-0 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
Q
endstream
endobj
16 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
xref
0 17
0000000000 65535 f
0000000015 00000 n
0000000054 00000 n
0000000120 00000 n
0000000169 00000 n
0000000361 00000 n
0000000393 00000 n
0000000436 00000 n
0000000543 00000 n
0000000650 00000 n
0000000871 00000 n
0000001029 00000 n
0000001224 00000 n
0000001258 00000 n
0000001303 00000 n
0000001411 00000 n
0000001633 00000 n
trailer
<<
/Size 17
/Root 3 0 R
/Info 1 0 R
>>
startxref
1791
%%EOF
@@ -0,0 +1,98 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/PageMode /UseNone
/Pages 2 0 R
/Type /Catalog
>>
endobj
2 0 obj
<<
/Count 1
/Kids [ 3 0 R ]
/Type /Pages
>>
endobj
3 0 obj
<<
/Contents 4 0 R
/MediaBox [ 10 10 307.84 220 ]
/Parent 2 0 R
/Resources <<
/Font 5 0 R
/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
>>
endobj
4 0 obj
<<
/Length 117
>>
stream
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
endstream
endobj
5 0 obj
<<
/F1 6 0 R
>>
endobj
6 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
7 0 obj
<<
/Author (anonymous)
/CreationDate (D\07220260917173038\05302\04700\047)
/Creator (anonymous)
/Keywords ()
/ModDate (D\07220260917173038\05302\04700\047)
/Producer (ReportLab PDF Library \055 \050opensource\051)
/Subject (unspecified)
/Title (untitled)
/Trapped /False
>>
endobj
xref
0 8
0000000000 65535 f
0000000015 00000 n
0000000083 00000 n
0000000142 00000 n
0000000336 00000 n
0000000504 00000 n
0000000535 00000 n
0000000642 00000 n
trailer
<<
/Size 8
/Root 1 0 R
/Info 7 0 R
/ID [ <a3fd776687419b9853cf896bf5d6dc96> <a3fd776687419b9853cf896bf5d6dc96> ]
>>
startxref
931
%%EOF
@@ -0,0 +1,213 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/Producer (pypdf)
>>
endobj
2 0 obj
<<
/Type /Pages
/Count 2
/Kids [ 4 0 R 11 0 R ]
>>
endobj
3 0 obj
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj
4 0 obj
<<
/Contents 5 0 R
/MediaBox [ 0 0 297.84 210 ]
/Resources <<
/Font 6 0 R
/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
5 0 obj
[ 9 0 R 10 0 R ]
endobj
6 0 obj
<<
/F1 7 0 R
/F1-0 8 0 R
>>
endobj
7 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
8 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
9 0 obj
<<
/Length 170
>>
stream
q
1 0.0 0.0 1 -10 -10 cm
10 10 297.84 210 re
W
n
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1-0 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
Q
endstream
endobj
10 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
11 0 obj
<<
/Contents 12 0 R
/MediaBox [ 0 0 297.84 210 ]
/Resources <<
/Font 13 0 R
/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
12 0 obj
[ 15 0 R 16 0 R ]
endobj
13 0 obj
<<
/F1 14 0 R
/F1-0 8 0 R
>>
endobj
14 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
15 0 obj
<<
/Length 170
>>
stream
q
1 0.0 0.0 1 -10 -10 cm
10 10 297.84 210 re
W
n
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1-0 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
Q
endstream
endobj
16 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
xref
0 17
0000000000 65535 f
0000000015 00000 n
0000000054 00000 n
0000000120 00000 n
0000000169 00000 n
0000000361 00000 n
0000000393 00000 n
0000000436 00000 n
0000000543 00000 n
0000000650 00000 n
0000000871 00000 n
0000001029 00000 n
0000001224 00000 n
0000001258 00000 n
0000001303 00000 n
0000001411 00000 n
0000001633 00000 n
trailer
<<
/Size 17
/Root 3 0 R
/Info 1 0 R
>>
startxref
1791
%%EOF
Binary file not shown.
@@ -0,0 +1,216 @@
%PDF-1.7
%âãÏÓ
1 0 obj
<<
/Producer (pypdf)
>>
endobj
2 0 obj
<<
/Type /Pages
/Count 2
/Kids [ 4 0 R 11 0 R ]
>>
endobj
3 0 obj
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj
4 0 obj
<<
/Contents 5 0 R
/MediaBox [ 0 0 210 297.84 ]
/Resources <<
/Font 6 0 R
/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
/ExtGState <<
/GS0 8 0 R
>>
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
5 0 obj
[ 9 0 R 10 0 R ]
endobj
6 0 obj
<<
/F1 7 0 R
>>
endobj
7 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
8 0 obj
<<
/Type /ExtGState
/OP false
/op false
/OPM 1
>>
endobj
9 0 obj
<<
/Length 287
>>
stream
q
0.00000000000000006123234 -1 1 0.00000000000000006123234 0.0 297.84 cm
0.0 0.0 297.84 210 re
W
n
/GS0 gs
0.925811 0.881331 0.538643 rg
0 0.23622 297.638 209.764 re
f*
0.476631 0.756542 0.260899 rg
0 0.23622 297.638 209.764 re
292.638 205 m
5 205 l
5 5.23622 l
292.638 5.23622 l
h
f*
Q
endstream
endobj
10 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
11 0 obj
<<
/Contents 12 0 R
/MediaBox [ 0 0 210 297.84 ]
/Resources <<
/Font 13 0 R
/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
/ExtGState <<
/GS0 8 0 R
>>
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
12 0 obj
[ 15 0 R 16 0 R ]
endobj
13 0 obj
<<
/F1 14 0 R
>>
endobj
14 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
15 0 obj
<<
/Length 287
>>
stream
q
0.00000000000000006123234 -1 1 0.00000000000000006123234 0.0 297.84 cm
0.0 0.0 297.84 210 re
W
n
/GS0 gs
0.925811 0.881331 0.538643 rg
0 0.23622 297.638 209.764 re
f*
0.476631 0.756542 0.260899 rg
0 0.23622 297.638 209.764 re
292.638 205 m
5 205 l
5 5.23622 l
292.638 5.23622 l
h
f*
Q
endstream
endobj
16 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
xref
0 17
0000000000 65535 f
0000000015 00000 n
0000000054 00000 n
0000000120 00000 n
0000000169 00000 n
0000000389 00000 n
0000000421 00000 n
0000000452 00000 n
0000000559 00000 n
0000000624 00000 n
0000000962 00000 n
0000001120 00000 n
0000001343 00000 n
0000001377 00000 n
0000001410 00000 n
0000001518 00000 n
0000001857 00000 n
trailer
<<
/Size 17
/Root 3 0 R
/Info 1 0 R
>>
startxref
2015
%%EOF
+55
View File
@@ -32,13 +32,18 @@
# 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.
import logging
from datetime import timedelta
from decimal import Decimal
from io import BytesIO
from pathlib import Path
import pypdfium2
import pytest
from django.core.files import File
from django.utils.timezone import now
from django_scopes import scope
from PIL import ImageChops
from pypdf import PdfReader
from pretix.base.models import (
@@ -118,3 +123,53 @@ def test_generate_pdf_multi(env):
assert ftype == 'application/pdf'
pdf = PdfReader(BytesIO(buf))
assert len(pdf.pages) == 1
def asset_path(name):
return Path(__file__).parent / "assets" / name
def compare_pdfs(pdf_dir: Path, inp_a: Path | bytes, inp_b: Path | bytes):
logging.info(f"Comparing pdfs, writing files to {pdf_dir}")
pdf_a = pypdfium2.PdfDocument(inp_a)
pdf_b = pypdfium2.PdfDocument(inp_b)
pdf_a.save(pdf_dir / "a.pdf")
pdf_b.save(pdf_dir / "b.pdf")
assert len(pdf_a) == len(pdf_b)
for i, (page_a, page_b) in enumerate(zip(pdf_a, pdf_b)):
render_a = page_a.render()
render_b = page_b.render()
assert render_a.height == render_b.height
assert render_a.width == render_b.width
diff = ImageChops.difference(render_a.to_pil(), render_b.to_pil())
if diff.getbbox():
diff.save(pdf_dir / f"{i}.png")
assert not diff.getbbox(), f"Page {i} differs."
@pytest.mark.django_db
@pytest.mark.parametrize("case", [
"bg-rotated",
"bg-mediabox-offset",
"bg-cropbox"
])
def test_generate_pdf_weird_bgs(pdf_dir, env, case):
event, order, shirt = env
asset_folder = asset_path(case)
with open(asset_folder / "bg.pdf", 'rb') as fi:
event.badge_layouts.create(name="Default", default=True, background=File(fi, name="test.pdf"))
e = BadgeExporter(event, organizer=event.organizer)
fname, ftype, buf = e.render({
'items': [shirt.pk],
'rendering': 'one',
'include_pending': True
})
assert ftype == 'application/pdf'
assert buf
compare_pdfs(pdf_dir, buf, asset_folder / "expected.pdf")
+25 -5
View File
@@ -913,7 +913,7 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest):
self.q.size = 0
self.q.save()
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
assert "_voucher_item" not in html.rendered_content
assert "_selected_item" not in html.rendered_content
def test_sold_out_blocking(self):
self.q.size = 0
@@ -921,7 +921,7 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest):
self.v.block_quota = True
self.v.save()
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
assert "_voucher_item" in html.rendered_content
assert "_selected_item" in html.rendered_content
def test_sold_out_ignore(self):
self.q.size = 0
@@ -929,7 +929,7 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest):
self.v.allow_ignore_quota = True
self.v.save()
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
assert "_voucher_item" in html.rendered_content
assert "_selected_item" in html.rendered_content
def test_variations_sold_out(self):
with scopes_disabled():
@@ -940,7 +940,7 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest):
self.q.size = 0
self.q.save()
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
assert "_voucher_item" not in html.rendered_content
assert "_selected_item" not in html.rendered_content
def test_variations_sold_out_blocking(self):
with scopes_disabled():
@@ -953,7 +953,27 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest):
self.v.block_quota = True
self.v.save()
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
assert "_voucher_item" in html.rendered_content
assert "_selected_item" in html.rendered_content
def test_variation_not_available(self):
with scopes_disabled():
var1 = ItemVariation.objects.create(item=self.item, value='Red', position=1)
var2 = ItemVariation.objects.create(item=self.item, value='Black', position=2, available_from=now() + datetime.timedelta(days=2))
var3 = ItemVariation.objects.create(item=self.item, value='Purple', position=3,
available_from=now() + datetime.timedelta(days=2), available_from_mode=Item.UNAVAIL_MODE_INFO)
self.q.variations.add(var1)
self.q.variations.add(var2)
self.q.variations.add(var3)
self.q.size = 0
self.q.save()
self.v.block_quota = True
self.v.save()
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
assert "_selected_item" in html.rendered_content
assert "Black" not in html.rendered_content
assert "Red" in html.rendered_content
assert "Purple" in html.rendered_content
assert "Not available yet" in html.rendered_content
def test_voucher_price(self):
self.v.value = Decimal("10.00")
+2
View File
@@ -21,6 +21,7 @@
#
from django.http import HttpResponse
from django.urls import path
from django.views.decorators.csrf import ensure_csrf_cookie
def view(request):
@@ -33,6 +34,7 @@ urlpatterns = [
view,
name="view",
),
path('ensure_csrf_cookie/', ensure_csrf_cookie(view), name="ensure_csrf_cookie"),
]
organizer_patterns = [