mirror of
https://github.com/pretix/pretix.git
synced 2026-10-01 19:14:43 +00:00
ticketoutput confirmation_text; is_cacheable
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
import logging
|
||||
import os
|
||||
from decimal import Decimal
|
||||
|
||||
from datetime import timedelta
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
@@ -31,7 +31,7 @@ from django_scopes import scopes_disabled
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, Order,
|
||||
OrderPosition,
|
||||
OrderPosition, CachedFile
|
||||
)
|
||||
from pretix.base.services.tasks import EventTask, ProfiledTask
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
@@ -43,7 +43,7 @@ from django.db import transaction
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def generate_orderposition(order_position: int, provider: str):
|
||||
def generate_orderposition(order_position: int, provider: str, cf: CachedFile | None = None):
|
||||
order_position = OrderPosition.objects.select_related('order', 'order__event').get(id=order_position)
|
||||
|
||||
with language(order_position.order.locale, order_position.order.event.settings.region):
|
||||
@@ -53,15 +53,22 @@ def generate_orderposition(order_position: int, provider: str):
|
||||
if prov.identifier == provider:
|
||||
filename, ttype, data = prov.generate(order_position)
|
||||
path, ext = os.path.splitext(filename)
|
||||
for ct in CachedTicket.objects.filter(order_position=order_position, provider=provider):
|
||||
ct.delete()
|
||||
ct = CachedTicket.objects.create(order_position=order_position, provider=provider,
|
||||
extension=ext, type=ttype, file=None)
|
||||
ct.file.save(filename, ContentFile(data))
|
||||
return ct.pk
|
||||
|
||||
if cf:
|
||||
cf.type = ttype
|
||||
cf.filename = filename
|
||||
cf.file.save(name=filename, content=ContentFile(data))
|
||||
return cf.pk
|
||||
|
||||
def generate_order(order: int, provider: str):
|
||||
if prov.is_cacheable:
|
||||
for ct in CachedTicket.objects.filter(order_position=order_position, provider=provider):
|
||||
ct.delete()
|
||||
ct = CachedTicket.objects.create(order_position=order_position, provider=provider,
|
||||
extension=ext, type=ttype, file=None)
|
||||
ct.file.save(filename, ContentFile(data))
|
||||
return ct.pk
|
||||
|
||||
def generate_order(order: int, provider: str, cf: CachedFile | None = None):
|
||||
order = Order.objects.select_related('event').get(id=order)
|
||||
|
||||
with language(order.locale, order.event.settings.region):
|
||||
@@ -71,21 +78,32 @@ def generate_order(order: int, provider: str):
|
||||
if prov.identifier == provider:
|
||||
filename, ttype, data = prov.generate_order(order)
|
||||
path, ext = os.path.splitext(filename)
|
||||
for ct in CachedCombinedTicket.objects.filter(order=order, provider=provider):
|
||||
ct.delete()
|
||||
ct = CachedCombinedTicket.objects.create(order=order, provider=provider, extension=ext,
|
||||
type=ttype, file=None)
|
||||
ct.file.save(filename, ContentFile(data))
|
||||
return ct.pk
|
||||
|
||||
if cf:
|
||||
cf.type = ttype
|
||||
cf.filename = filename
|
||||
cf.file.save(name=filename, content=ContentFile(data))
|
||||
return cf.pk
|
||||
|
||||
if prov.is_cacheable:
|
||||
for ct in CachedCombinedTicket.objects.filter(order=order, provider=provider):
|
||||
ct.delete()
|
||||
ct = CachedCombinedTicket.objects.create(order=order, provider=provider, extension=ext,
|
||||
type=ttype, file=None)
|
||||
ct.file.save(filename, ContentFile(data))
|
||||
return ct.pk
|
||||
|
||||
@app.task(base=ProfiledTask)
|
||||
def generate(model: str, pk: int, provider: str):
|
||||
def generate(model: str, pk: int, provider: str, cachedfile_pk: int | None = None):
|
||||
with scopes_disabled():
|
||||
if cachedfile_pk:
|
||||
cf = CachedFile.objects.get(pk=cachedfile_pk)
|
||||
else:
|
||||
cf = None
|
||||
if model == 'order':
|
||||
return generate_order(pk, provider)
|
||||
return generate_order(pk, provider, cf)
|
||||
elif model == 'orderposition':
|
||||
return generate_orderposition(pk, provider)
|
||||
return generate_orderposition(pk, provider, cf)
|
||||
|
||||
|
||||
class DummyRollbackException(Exception):
|
||||
@@ -95,7 +113,7 @@ def get_preview_position(event):
|
||||
connection = transaction.get_connection()
|
||||
if not connection.in_atomic_block:
|
||||
raise RuntimeError("get_preview_position needs to be called in a rolledback_transaction")
|
||||
|
||||
|
||||
item = event.items.create(name=_("Sample product"), default_price=Decimal('42.23'),
|
||||
description=_("Sample product description"))
|
||||
item2 = event.items.create(name=_("Sample workshop"), default_price=Decimal('23.40'))
|
||||
@@ -151,7 +169,7 @@ def get_tickets_for_order(order, base_position=None):
|
||||
]
|
||||
|
||||
for p in providers:
|
||||
if not p.is_enabled:
|
||||
if not p.is_enabled or not p.is_cacheable:
|
||||
continue
|
||||
|
||||
if p.multi_download_enabled and not base_position:
|
||||
|
||||
@@ -231,6 +231,19 @@ class BaseTicketOutput:
|
||||
"""
|
||||
return 'fa-download'
|
||||
|
||||
@property
|
||||
def confirmation_text(self) -> str | None:
|
||||
"""
|
||||
A text the user has to confirm before a pass is generated.
|
||||
|
||||
Returning None means no confirmation screen is shown.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_cacheable(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def preview_allowed(self) -> bool:
|
||||
"""
|
||||
|
||||
@@ -1,460 +0,0 @@
|
||||
import enum
|
||||
from typing import Literal, OrderedDict, TypedDict
|
||||
from i18nfield.strings import LazyI18nString
|
||||
import jsonschema
|
||||
from django.core.exceptions import ValidationError
|
||||
from pretix.base.models import OrderPosition
|
||||
from ...plugins.wallet.placeholders import WalletPlaceholderRenderer, get_available_context, get_wallet_placeholder_renderer, get_wallet_placeholders
|
||||
from django import forms
|
||||
from pretix.api.helpers import handle_file_upload
|
||||
from django.core.files import File
|
||||
|
||||
class WalletPlatform:
|
||||
identifier: str
|
||||
name: str
|
||||
|
||||
|
||||
class LayoutContext(TypedDict):
|
||||
placeholders: dict[str, dict]
|
||||
placeholder_renderer: WalletPlaceholderRenderer
|
||||
|
||||
|
||||
class FieldGroupType(enum.Enum):
|
||||
PLACEHOLDER = "placeholder"
|
||||
PREDEFINED = "predefined"
|
||||
|
||||
|
||||
class FieldGroupDisplay(enum.Enum):
|
||||
PLAIN = "plain"
|
||||
WITH_LABEL = "with_label"
|
||||
CODE = "code"
|
||||
|
||||
|
||||
class FieldGroup:
|
||||
type: FieldGroupType
|
||||
identifier: str
|
||||
name: str
|
||||
description: str
|
||||
required: bool = False
|
||||
|
||||
def __init__(self, identifier: str, name: str, description=None, required=False):
|
||||
self.identifier = identifier
|
||||
self.name = name
|
||||
self.required = required
|
||||
self.description = description or ""
|
||||
|
||||
def layout_schema(
|
||||
self,
|
||||
remaining_fields: list["FieldGroup"],
|
||||
context: LayoutContext,
|
||||
) -> dict:
|
||||
raise NotImplementedError()
|
||||
|
||||
def asdict(self, context: LayoutContext):
|
||||
return {
|
||||
"type": self.type.value,
|
||||
"identifier": self.identifier,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"required": self.required,
|
||||
}
|
||||
|
||||
|
||||
class FieldContentType(enum.Enum):
|
||||
IMAGE = "image"
|
||||
TEXT = "text"
|
||||
|
||||
|
||||
class FieldEntryType(enum.Enum):
|
||||
CUSTOM = "custom"
|
||||
PLACEHOLDER = "placeholder"
|
||||
|
||||
|
||||
class FieldEntry[T]:
|
||||
type: FieldEntryType
|
||||
label: LazyI18nString | None
|
||||
content: T
|
||||
|
||||
def __init__(
|
||||
self, type: FieldEntryType, content: T, label: LazyI18nString | None = None
|
||||
):
|
||||
self.type = type
|
||||
self.label = label
|
||||
self.content = content
|
||||
|
||||
def asdict(self) -> dict:
|
||||
return {
|
||||
"type": self.type.value,
|
||||
"content": self.content,
|
||||
"label": self.label.data if self.label else None,
|
||||
}
|
||||
|
||||
|
||||
class PlaceholderFieldEntry(FieldEntry[str]):
|
||||
type = FieldEntryType.PLACEHOLDER
|
||||
label: LazyI18nString | None
|
||||
content: str
|
||||
|
||||
def __init__(self, content: str, label: LazyI18nString | None = None):
|
||||
self.label = label
|
||||
self.content = content
|
||||
|
||||
|
||||
class CustomFieldEntry(FieldEntry[LazyI18nString]):
|
||||
type: FieldEntryType
|
||||
label: LazyI18nString | None
|
||||
content: LazyI18nString
|
||||
|
||||
def asdict(self) -> dict:
|
||||
return {
|
||||
"type": self.type.value,
|
||||
"content": self.content.data,
|
||||
"label": self.label.data if self.label else None,
|
||||
}
|
||||
|
||||
|
||||
class PredefinedFieldGroup(FieldGroup):
|
||||
type = FieldGroupType.PREDEFINED
|
||||
|
||||
def layout_schema(
|
||||
self,
|
||||
remaining_fields: list["FieldGroup"],
|
||||
context: LayoutContext,
|
||||
):
|
||||
return {"type": "object", "properties": {"active": {"type": "boolean"}}}
|
||||
|
||||
|
||||
class PlaceholderFieldGroup(FieldGroup):
|
||||
type = FieldGroupType.PLACEHOLDER
|
||||
content_type: FieldContentType
|
||||
default_entries: list[FieldEntry]
|
||||
display: FieldGroupDisplay
|
||||
min_entries: int | None
|
||||
max_entries: int | None
|
||||
context_args: set[
|
||||
str
|
||||
] # what context arguments are available when rendering this fieldgroup
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
identifier: str,
|
||||
name: str,
|
||||
content_type: FieldContentType,
|
||||
description: str = "",
|
||||
required=False,
|
||||
default_entries=None,
|
||||
min_entries=None,
|
||||
max_entries=None,
|
||||
display=FieldGroupDisplay.WITH_LABEL,
|
||||
context_args: set[str] | None = None,
|
||||
):
|
||||
super().__init__(identifier, name, description, required)
|
||||
self.content_type = content_type
|
||||
self.default_entries = default_entries or []
|
||||
self.min_entries = min_entries
|
||||
self.max_entries = max_entries
|
||||
self.display = display
|
||||
self.context_args = context_args or set()
|
||||
|
||||
if self.required and (self.min_entries is None or self.min_entries < 1):
|
||||
self.min_entries = 1
|
||||
|
||||
def asdict(self, context: LayoutContext):
|
||||
return {
|
||||
**super().asdict(context),
|
||||
"content_type": self.content_type.value,
|
||||
"default_entries": [x.asdict() for x in self.default_entries],
|
||||
"display": self.display.value,
|
||||
"min_entries": self.min_entries,
|
||||
"max_entries": self.max_entries,
|
||||
"context_args": list(get_available_context(self.context_args, context['placeholder_renderer'].transformations)),
|
||||
}
|
||||
|
||||
def layout_schema(
|
||||
self,
|
||||
remaining_fields: list["FieldGroup"],
|
||||
context: LayoutContext,
|
||||
):
|
||||
content_type_placeholders = (
|
||||
context["placeholders"].get(self.content_type.value, {}).values()
|
||||
)
|
||||
renderer = context['placeholder_renderer']
|
||||
available_placeholders = [
|
||||
x.identifier
|
||||
for x in content_type_placeholders
|
||||
if renderer.is_available(x, self.context_args)
|
||||
]
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entries": self.entries_schema(placeholders=available_placeholders),
|
||||
"overflow": {
|
||||
"anyOf": [
|
||||
{"type": "null"},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
f.identifier
|
||||
for f in remaining_fields
|
||||
if isinstance(f, PlaceholderFieldGroup)
|
||||
and f.content_type == self.content_type
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
"required": ["entries"],
|
||||
}
|
||||
|
||||
def entries_schema(self, placeholders: list[str]):
|
||||
baseprops = {}
|
||||
if self.display == FieldGroupDisplay.WITH_LABEL:
|
||||
baseprops["label"] = {"$ref": "#/$defs/I18nString"}
|
||||
|
||||
schema = {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"anyOf": [
|
||||
{
|
||||
"properties": {
|
||||
**baseprops,
|
||||
"type": {"const": "placeholder"},
|
||||
"content": {"enum": placeholders},
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
**baseprops,
|
||||
"type": {"const": "custom"},
|
||||
"content": {"$ref": "#/$defs/I18nString"},
|
||||
}
|
||||
},
|
||||
],
|
||||
"required": ["type", "content"],
|
||||
},
|
||||
}
|
||||
if self.display == FieldGroupDisplay.WITH_LABEL:
|
||||
schema["items"]["required"].append("label")
|
||||
if self.min_entries is not None:
|
||||
schema["minItems"] = self.min_entries
|
||||
# max_entries is not enforced here, as the layout can have more fields than that (null-fields are removed, rest is overspilled)
|
||||
return schema
|
||||
|
||||
|
||||
class TextFieldGroup(PlaceholderFieldGroup):
|
||||
content_type = FieldContentType.TEXT
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(content_type=self.content_type, **kwargs)
|
||||
|
||||
|
||||
class ImageFieldGroup(PlaceholderFieldGroup):
|
||||
content_type = FieldContentType.IMAGE
|
||||
display = FieldGroupDisplay.PLAIN
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(content_type=self.content_type, display=self.display, **kwargs)
|
||||
|
||||
|
||||
class SettingsField:
|
||||
identifier: str
|
||||
label: str
|
||||
type: Literal["image", "text"]
|
||||
help_text: str|None
|
||||
required: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
identifier: str,
|
||||
label: str,
|
||||
type: Literal["image", "text"] = "text",
|
||||
help_text = None,
|
||||
required: bool = False,
|
||||
):
|
||||
self.identifier = identifier
|
||||
self.label = label
|
||||
self.type = type
|
||||
self.help_text = help_text
|
||||
self.required = required
|
||||
|
||||
def asdict(self):
|
||||
return {
|
||||
"identifier": self.identifier,
|
||||
"label": self.label,
|
||||
"type": self.type,
|
||||
"help_text": self.help_text,
|
||||
"required": self.required,
|
||||
}
|
||||
|
||||
|
||||
class PassStyle:
|
||||
identifier: str # unique within platform
|
||||
name: str
|
||||
# order here limits in what order users can configure field "overspilling" (if too many fields are defined, where should the rest go)
|
||||
# -> can only go down in the list
|
||||
# we evaluate the fields in this order, so they overspill in this order as well
|
||||
fieldgroups: list[FieldGroup]
|
||||
|
||||
@property
|
||||
def settings(self) -> list[SettingsField]:
|
||||
return []
|
||||
|
||||
@property
|
||||
def preview_layout(self) -> list | None:
|
||||
return None
|
||||
|
||||
def asdict(self):
|
||||
context = LayoutContext(placeholders=self.placeholders, placeholder_renderer=get_wallet_placeholder_renderer())
|
||||
return {
|
||||
"identifier": self.identifier,
|
||||
"name": self.name,
|
||||
"fieldgroups": [x.asdict(context) for x in self.fieldgroups],
|
||||
"preview_layout": self.preview_layout,
|
||||
"settings": [x.asdict() for x in self.settings],
|
||||
}
|
||||
|
||||
def layout_schema(self):
|
||||
context = LayoutContext(placeholders=self.placeholders, placeholder_renderer=get_wallet_placeholder_renderer())
|
||||
print(f"schema {self.settings=}")
|
||||
schema = {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
# TODO: $id
|
||||
"title": self.name,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fieldgroups": {
|
||||
"description": "Layout Field Groups",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
group.identifier: group.layout_schema(
|
||||
context=context, remaining_fields=self.fieldgroups[i:]
|
||||
)
|
||||
for (i, group) in enumerate(self.fieldgroups)
|
||||
},
|
||||
"required": [
|
||||
group.identifier for group in self.fieldgroups if group.required
|
||||
],
|
||||
},
|
||||
"settings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
setting.identifier: {"type": ["string", "null"]}
|
||||
for setting in self.settings
|
||||
},
|
||||
"required": [
|
||||
setting.identifier for setting in self.settings if setting.required
|
||||
],
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"I18nString": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "object", "additionalProperties": {"type": "string"}},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
if any(group.required for group in self.fieldgroups):
|
||||
schema.setdefault("required", [])
|
||||
schema["required"].append("fieldgroups")
|
||||
# if any(setting.required for setting in self.settings):
|
||||
# schema.setdefault("required", [])
|
||||
# schema["required"].append("settings")
|
||||
|
||||
return schema
|
||||
|
||||
def render_placeholder(self, context, content_type, content):
|
||||
placeholder = self.placeholders.get(content_type, {}).get(content)
|
||||
if placeholder:
|
||||
placeholder_value = context.render_placeholder(placeholder)
|
||||
if placeholder_value:
|
||||
return placeholder.label, placeholder_value
|
||||
|
||||
return None, None
|
||||
|
||||
def __init__(self, event, layout = None, file_settings: dict[str, File] | None = None):
|
||||
self.event = event
|
||||
self.layout = layout
|
||||
self.file_settings = file_settings or {}
|
||||
self.placeholders = get_wallet_placeholders(self.event)
|
||||
|
||||
def validate(self):
|
||||
schema = self.layout_schema()
|
||||
try:
|
||||
jsonschema.validate(self.layout, schema)
|
||||
except jsonschema.ValidationError as e:
|
||||
raise ValidationError("Invalid layout: {}".format(str(e)))
|
||||
|
||||
def extract_file_settings(self, request, file_settings):
|
||||
res = {}
|
||||
for setting in self.settings:
|
||||
if setting.type == "image":
|
||||
if file_settings.get(setting.identifier) == "file:keep":
|
||||
res[setting.identifier] = "keep"
|
||||
elif data := file_settings.get(setting.identifier):
|
||||
res[setting.identifier] = handle_file_upload(data, request.user, getattr(request, "auth", None), {"image/png", "image/jpeg"})
|
||||
elif setting.identifier in file_settings:
|
||||
res[setting.identifier] = None
|
||||
|
||||
return res
|
||||
|
||||
def get_pass_fields(self, op: OrderPosition):
|
||||
if not self.layout:
|
||||
raise ValueError("`value` needs to be set")
|
||||
|
||||
context = get_wallet_placeholder_renderer(order_position=op)
|
||||
|
||||
fields = {}
|
||||
for group in self.fieldgroups:
|
||||
if isinstance(group, PredefinedFieldGroup):
|
||||
pass
|
||||
|
||||
elif isinstance(group, PlaceholderFieldGroup):
|
||||
group_fields = fields.get(group.identifier, [])
|
||||
if group.identifier in self.layout["fieldgroups"]:
|
||||
for field in self.layout["fieldgroups"][group.identifier][
|
||||
"entries"
|
||||
]:
|
||||
field_entry = {}
|
||||
if group.display == FieldGroupDisplay.WITH_LABEL:
|
||||
field_entry["label"] = LazyI18nString(field["label"])
|
||||
if field["type"] == FieldEntryType.PLACEHOLDER.value:
|
||||
label, field_entry["value"] = self.render_placeholder(
|
||||
context, group.content_type.value, field["content"]
|
||||
)
|
||||
if (
|
||||
group.display == FieldGroupDisplay.WITH_LABEL
|
||||
and not str(field_entry["label"])
|
||||
and label
|
||||
):
|
||||
field_entry["label"] = LazyI18nString(label)
|
||||
|
||||
elif field["type"] == FieldEntryType.CUSTOM.value:
|
||||
field_entry["value"] = LazyI18nString(field["content"])
|
||||
if "value" in field_entry and field_entry["value"]:
|
||||
group_fields.append(field_entry)
|
||||
if group.min_entries and len(group_fields) < group.min_entries:
|
||||
raise ValueError(
|
||||
f"Group {group.identifier} needs at least {group.min_entries} entries, but only {len(group_fields)} were provided"
|
||||
)
|
||||
fields[group.identifier] = group_fields[: group.max_entries]
|
||||
if overflow_group := self.layout["fieldgroups"][group.identifier][
|
||||
"overflow"
|
||||
]:
|
||||
fields.setdefault(overflow_group, [])
|
||||
fields[overflow_group] += group_fields[group.max_entries :]
|
||||
|
||||
else:
|
||||
raise ValueError("Unknown field group")
|
||||
return fields
|
||||
|
||||
def group_is_active(self, identifier: str):
|
||||
if not self.layout:
|
||||
raise ValueError("`value` needs to be set")
|
||||
|
||||
return self.layout["fieldgroups"].get(identifier, {}).get("active", False)
|
||||
|
||||
def generate(self, op: OrderPosition):
|
||||
raise NotImplementedError()
|
||||
@@ -10,6 +10,7 @@
|
||||
{% load l10n %}
|
||||
{% load phone_format %}
|
||||
{% load getitem %}
|
||||
{% load dialog %}
|
||||
{% block title %}
|
||||
{% blocktrans trimmed with code=order.code %}
|
||||
Order details: {{ code }}
|
||||
@@ -563,15 +564,41 @@
|
||||
<div class="position-buttons">
|
||||
{% if line.generate_ticket %}
|
||||
{% for b in download_buttons %}
|
||||
<form action="{% url "control:event.order.download.ticket" code=order.code event=request.event.slug organizer=request.event.organizer.slug position=line.pk output=b.identifier %}"
|
||||
method="post" data-asynctask data-asynctask-download
|
||||
class="form-inline helper-display-inline{% if b.javascript_required %} requirejs{% endif %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit"
|
||||
class="btn btn-xs btn-default">
|
||||
{% if b.confirmation_text %}
|
||||
<button type="button" class="btn btn-xs btn-default" data-toggle="modal" data-target="#download-confirm-{{b.identifier}}">
|
||||
<span class="fa {{ b.icon }}"></span> {{ b.text }}
|
||||
</button>
|
||||
</form>
|
||||
<dialog class="modal modal-card" id="download-confirm-{{b.identifier}}" tabindex="-1" role="dialog" aria-labelledby="download-confirm-{{b.identifier}}-label">
|
||||
<div class="modal-card-inner" role="document">
|
||||
<div class="modal-card-icon"><span class="fa fa-shield" aria-hidden="true"></span></div>
|
||||
<div class="modal-card-content">
|
||||
<h4 class="modal-card-title h3" id="download-confirm-{{b.identifier}}-label">{% trans "Confirmation required" %}</h4>
|
||||
<p>{{ b.confirmation_text }}</p>
|
||||
<div class="modal-card-confirm modal-card-confirm-spread">
|
||||
<button type="button" class="btn btn-default btn-lg" data-dismiss="modal">{% trans "Cancel" %}</button>
|
||||
<form action="{% url "control:event.order.download.ticket" code=order.code event=request.event.slug organizer=request.event.organizer.slug position=line.pk output=b.identifier %}"
|
||||
method="post" data-asynctask data-asynctask-download
|
||||
class="form-inline helper-display-inline{% if b.javascript_required %} requirejs{% endif %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit"
|
||||
class="btn btn-primary btn-lg">
|
||||
{% trans "Confirm" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
{% else %}
|
||||
<form action="{% url "control:event.order.download.ticket" code=order.code event=request.event.slug organizer=request.event.organizer.slug position=line.pk output=b.identifier %}"
|
||||
method="post" data-asynctask data-asynctask-download
|
||||
class="form-inline helper-display-inline{% if b.javascript_required %} requirejs{% endif %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit"
|
||||
class="btn btn-xs btn-default">
|
||||
<span class="fa {{ b.icon }}"></span> {{ b.text }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<button type="submit" data-toggle="qrcode" data-qrcode="{{ line.secret }}"
|
||||
|
||||
@@ -589,7 +589,8 @@ class OrderDetail(OrderView):
|
||||
'icon': provider.download_button_icon or 'fa-download',
|
||||
'identifier': provider.identifier,
|
||||
'multi': provider.multi_download_enabled,
|
||||
'javascript_required': provider.javascript_required
|
||||
'javascript_required': provider.javascript_required,
|
||||
'confirmation_text': provider.confirmation_text
|
||||
})
|
||||
return buttons
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from .apple import ApplePlatform, AppleWalletEventTicket
|
||||
from .google import GooglePlatform, GoogleWalletEventTicket
|
||||
from .base import PassStyle
|
||||
from pretix.plugins.wallet.styles.base import PassStyle
|
||||
|
||||
AVAILABLE_PLATFORMS = [ApplePlatform, GooglePlatform]
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from collections import OrderedDict
|
||||
from typing import Any
|
||||
|
||||
from .base import (
|
||||
from pretix.plugins.wallet.styles.base import (
|
||||
FieldEntryType,
|
||||
FieldGroupDisplay,
|
||||
ImageFieldGroup,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from pretix.base.models import Event, OrderPosition
|
||||
|
||||
from .base import (
|
||||
from pretix.plugins.wallet.styles.base import (
|
||||
FieldGroupDisplay,
|
||||
ImageFieldGroup,
|
||||
PassStyle,
|
||||
|
||||
@@ -20,13 +20,13 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import gettext as _
|
||||
from pretix.base.ticketoutput import BaseTicketOutput
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from django.template.loader import render_to_string
|
||||
from django.shortcuts import get_object_or_404
|
||||
from .styles.base import WalletPlatform
|
||||
from pretix.plugins.wallet.styles.base import WalletPlatform
|
||||
from .styles.apple import ApplePlatform
|
||||
from .styles.google import GooglePlatform
|
||||
from collections import OrderedDict
|
||||
@@ -79,6 +79,18 @@ class GoogleWalletTicketOutput(WalletOutput):
|
||||
verbose_name = _("Google")
|
||||
download_button_text = "Add to Google Wallet"
|
||||
platform = GooglePlatform
|
||||
is_cacheable = False
|
||||
|
||||
@property
|
||||
def confirmation_text(self) -> str | None:
|
||||
return _(
|
||||
"Please be aware, that contrary to other virtual wallets/passes (like Apple Wallet), Google Wallet Passes are not "
|
||||
"handled offline. Every pass that is created, has to be transmitted to Google Inc.\n"
|
||||
"\n"
|
||||
"By clicking \"Confirm\" below, we will transfer some of your personal information, which is "
|
||||
"necessary to provide you with your Google Wallet Pass, to Google Inc.\n"
|
||||
"\n"
|
||||
"Please be aware, that there is no way to delete the data, once it has been transmitted.\n")
|
||||
|
||||
def get_global_settings(sender, **kwargs):
|
||||
return OrderedDict(
|
||||
|
||||
@@ -222,10 +222,11 @@
|
||||
{% if line in tickets_with_download %}
|
||||
{% for b in download_buttons %}
|
||||
<form action="{% if position_page and line.addon_to %}{% eventurl event "presale:event.order.position.download" secret=line.addon_to.web_secret order=order.code output=b.identifier pid=line.pk position=line.addon_to.positionid %}{% elif position_page %}{% eventurl event "presale:event.order.position.download" secret=line.web_secret order=order.code output=b.identifier pid=line.pk position=line.positionid %}{% else %}{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=line.pk %}{% endif %}"
|
||||
method="post" data-asynctask data-asynctask-download class="download-btn-form{% if b.javascript_required %} requirejs{% endif %}">
|
||||
method="post" data-asynctask data-asynctask-download class="download-btn-form{% if b.javascript_required or b.confirmation_text %} requirejs{% endif %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit"
|
||||
class="btn btn-sm {% if b.identifier == "pdf" %}btn-primary{% else %}btn-default{% endif %}">
|
||||
class="btn btn-sm {% if b.identifier == "pdf" %}btn-primary{% else %}btn-default{% endif %}"
|
||||
{% if b.confirmation_text %}data-confirmation-text="{{ b.confirmation_text|rich_text|force_escape }}"{% endif %}>
|
||||
<span class="fa {{ b.icon }}" aria-hidden="true"></span> {{ b.text }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load rich_text %}
|
||||
{% if download_email_required %}
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
@@ -43,10 +44,11 @@
|
||||
{% for b in download_buttons %}
|
||||
{% if b.multi %}
|
||||
<form action="{% eventurl event "presale:event.order.download.combined" secret=order.secret order=order.code output=b.identifier %}"
|
||||
method="post" data-asynctask data-asynctask-download class="download-btn-form">
|
||||
method="post" data-asynctask data-asynctask-download class="download-btn-form {% if b.javascript_required or b.confirmation_text %}requirejs{% endif %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit"
|
||||
class="btn btn-lg {% if b.identifier == "pdf" %}btn-primary{% else %}btn-default{% endif %}">
|
||||
class="btn btn-lg {% if b.identifier == "pdf" %}btn-primary{% else %}btn-default{% endif %}"
|
||||
{% if b.confirmation_text %}data-confirmation-text="{{ b.confirmation_text|rich_text|force_escape }}"{% endif %}>
|
||||
<span class="fa {{ b.icon }}" aria-hidden="true"></span> {{ b.multi_text }}
|
||||
</button>
|
||||
</form>
|
||||
@@ -62,11 +64,12 @@
|
||||
<div>
|
||||
{% for b in download_buttons %}
|
||||
<form action="{% if position_page and tickets_with_download.0.addon_to %}{% eventurl event "presale:event.order.position.download" secret=tickets_with_download.0.addon_to.web_secret order=order.code output=b.identifier pid=tickets_with_download.0.pk position=tickets_with_download.0.addon_to.positionid %}{% elif position_page %}{% eventurl event "presale:event.order.position.download" secret=tickets_with_download.0.web_secret order=order.code output=b.identifier pid=tickets_with_download.0.pk position=tickets_with_download.0.positionid %}{% else %}{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=tickets_with_download.0.pk %}{% endif %}"
|
||||
method="post" data-asynctask data-asynctask-download
|
||||
class="download-btn-form{% if b.javascript_required %} requirejs{% endif %}">
|
||||
method="post" data-asynctask data-asynctask-download
|
||||
class="download-btn-form {% if b.javascript_required or b.confirmation_text %}requirejs{% endif %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit"
|
||||
class="btn btn-lg {% if b.identifier == "pdf" %}btn-primary{% else %}btn-default{% endif %}">
|
||||
class="btn btn-lg {% if b.identifier == "pdf" %}btn-primary{% else %}btn-default{% endif %}"
|
||||
{% if b.confirmation_text %}data-confirmation-text="{{ b.confirmation_text|rich_text|force_escape }}"{% endif %}>
|
||||
<span class="fa {{ b.icon }}" aria-hidden="true"></span> {{ b.long_text }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -147,3 +147,14 @@
|
||||
{% dialog "dialog-nothing-to-add" label_nothing_to_add description_nothing_to_add icon="exclamation-circle" %}
|
||||
<p class="modal-card-confirm"><button class="btn btn-primary">{% trans "OK" %}</button></p>
|
||||
{% enddialog %}
|
||||
|
||||
{% trans "Confirmation required" as confirmation_title %}
|
||||
{% dialog "confirmationmodal" confirmation_title "" icon="shield" %}
|
||||
<div class="modal-card-confirm modal-card-confirm-spread">
|
||||
<button type="button" class="btn btn-default btn-lg" commandfor="confirmationmodal" command="close">{% trans "Cancel" %}</button>
|
||||
<button type="button" id="confirmationmodal-confirm"
|
||||
class="btn btn-primary btn-lg">
|
||||
{% trans "Confirm" %}
|
||||
</button>
|
||||
</div>
|
||||
{% enddialog %}
|
||||
@@ -42,7 +42,10 @@ import os
|
||||
import re
|
||||
from collections import Counter, OrderedDict, defaultdict
|
||||
from decimal import Decimal
|
||||
from typing import Iterable
|
||||
from urllib.parse import quote
|
||||
from datetime import timedelta
|
||||
from uuid import UUID
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
@@ -61,12 +64,14 @@ from django.utils.timezone import now
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.views.generic import ListView, TemplateView, View
|
||||
from pretixeu.views import reverse
|
||||
|
||||
from pretix.base.auth import has_event_access_permission
|
||||
from pretix.base.models import (
|
||||
CachedTicket, Checkin, GiftCard, Invoice, Order, OrderPosition, Quota,
|
||||
TaxRule,
|
||||
)
|
||||
from pretix.base.models.base import CachedFile
|
||||
from pretix.base.models.orders import (
|
||||
CachedCombinedTicket, InvoiceAddress, OrderFee, OrderPayment, OrderRefund,
|
||||
QuestionAnswer,
|
||||
@@ -277,7 +282,8 @@ class TicketPageMixin:
|
||||
'icon': provider.download_button_icon or 'fa-download',
|
||||
'identifier': provider.identifier,
|
||||
'multi': provider.multi_download_enabled,
|
||||
'javascript_required': provider.javascript_required
|
||||
'javascript_required': provider.javascript_required,
|
||||
'confirmation_text': provider.confirmation_text,
|
||||
})
|
||||
return buttons
|
||||
|
||||
@@ -1176,9 +1182,6 @@ class AnswerDownload(EventViewMixin, OrderDetailMixin, View):
|
||||
|
||||
|
||||
class OrderDownloadMixin:
|
||||
def get_success_url(self, value):
|
||||
return self.get_self_url()
|
||||
|
||||
@cached_property
|
||||
def output(self):
|
||||
responses = register_ticket_outputs.send(self.request.event)
|
||||
@@ -1212,12 +1215,33 @@ class OrderDownloadMixin:
|
||||
):
|
||||
return self.error(OrderError(_('Please click the link we sent you via email to download your tickets.')))
|
||||
|
||||
ct = self.get_last_ct()
|
||||
if ct:
|
||||
if self.output.is_cacheable and (ct := self.get_last_ct()):
|
||||
return self.success(ct)
|
||||
|
||||
if not self.output.is_cacheable:
|
||||
cf = CachedFile(web_download=True, date=now(), expires=now() + timedelta(hours=2))
|
||||
cf.bind_to_session(request)
|
||||
cf.save()
|
||||
else:
|
||||
cf = None
|
||||
|
||||
return self.do('orderposition' if 'position' in kwargs else 'order',
|
||||
self.order_position.pk if 'position' in kwargs else self.order.pk,
|
||||
self.output.identifier)
|
||||
self.output.identifier,
|
||||
cf.pk if cf else None)
|
||||
|
||||
def get_success_url(self, value):
|
||||
if isinstance(value, UUID):
|
||||
cf = CachedFile.objects.filter(pk=value).first()
|
||||
else:
|
||||
cf = None
|
||||
if not cf or not cf.allowed_for_session(self.request):
|
||||
return self.get_self_url()
|
||||
|
||||
if cf.type == 'text/uri-list':
|
||||
return cf.file.file.read().decode()
|
||||
else:
|
||||
return reverse('cachedfile.download', kwargs={"id":str(value)})
|
||||
|
||||
def get_success_message(self, value):
|
||||
return ""
|
||||
@@ -1259,14 +1283,15 @@ class OrderDownloadMixin:
|
||||
return redirect(self.get_self_url())
|
||||
|
||||
def get_last_ct(self):
|
||||
if 'position' in self.kwargs:
|
||||
ct = CachedTicket.objects.filter(
|
||||
order_position=self.order_position, provider=self.output.identifier, file__isnull=False
|
||||
).last()
|
||||
else:
|
||||
ct = CachedCombinedTicket.objects.filter(
|
||||
order=self.order, provider=self.output.identifier, file__isnull=False
|
||||
).last()
|
||||
if self.output.is_cacheable:
|
||||
if 'position' in self.kwargs:
|
||||
ct = CachedTicket.objects.filter(
|
||||
order_position=self.order_position, provider=self.output.identifier, file__isnull=False
|
||||
).last()
|
||||
else:
|
||||
ct = CachedCombinedTicket.objects.filter(
|
||||
order=self.order, provider=self.output.identifier, file__isnull=False
|
||||
).last()
|
||||
if not ct or not ct.file:
|
||||
return None
|
||||
return ct
|
||||
|
||||
@@ -730,6 +730,17 @@ $(function () {
|
||||
// Prevent double-submit, see also https://github.com/pretix/pretix/issues/5836
|
||||
$(this).addClass('disabled')
|
||||
})
|
||||
document.querySelectorAll('button[data-confirmation-text]').forEach((element) => {
|
||||
element.addEventListener('click', (evt) => {
|
||||
evt.preventDefault();
|
||||
const onConfirm = () => evt.target.form.submit();
|
||||
const confirmationModal = document.getElementById("confirmationmodal");
|
||||
confirmationModal.querySelector("#confirmationmodal-description").innerHTML = element.dataset.confirmationText
|
||||
confirmationModal.querySelector("#confirmationmodal-confirm").addEventListener('click', onConfirm, {'once': true})
|
||||
confirmationModal.addEventListener('close', () => confirmationModal.removeEventListener('click', onConfirm))
|
||||
confirmationModal.showModal()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function copy_answers (elements, answers) {
|
||||
|
||||
Reference in New Issue
Block a user