mirror of
https://github.com/pretix/pretix.git
synced 2026-10-02 19:24:42 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77adaa69fe | ||
|
|
c875d758f9 | ||
|
|
2ebdd048c2 | ||
|
|
ca40b09080 | ||
|
|
558bf910fd | ||
|
|
7e1e472691 | ||
|
|
b06c9ef463 | ||
|
|
2af8d29ca9 | ||
|
|
2791d5e49e |
@@ -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 *
|
||||
|
||||
@@ -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**:
|
||||
|
||||
|
||||
@@ -129,6 +129,7 @@ dev = [
|
||||
"pytest==9.1.*",
|
||||
"playwright",
|
||||
"responses",
|
||||
"pypdfium2"
|
||||
]
|
||||
|
||||
[project.entry-points."distutils.commands"]
|
||||
|
||||
+33
-18
@@ -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   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("-", "- ")
|
||||
|
||||
# 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   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("-", "- ")
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>',
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user