mirror of
https://github.com/pretix/pretix.git
synced 2026-06-11 01:25:13 +00:00
WIP
This commit is contained in:
@@ -38,6 +38,7 @@ from pretix.base.settings import PERSON_NAME_SCHEMES
|
|||||||
from pretix.base.signals import register_ticket_outputs
|
from pretix.base.signals import register_ticket_outputs
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
from pretix.helpers.database import rolledback_transaction
|
from pretix.helpers.database import rolledback_transaction
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -90,36 +91,43 @@ def generate(model: str, pk: int, provider: str):
|
|||||||
class DummyRollbackException(Exception):
|
class DummyRollbackException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
def preview(event: int, provider: str):
|
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'))
|
||||||
|
|
||||||
|
from pretix.base.models import Order
|
||||||
|
order = event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
|
||||||
|
email='sample@pretix.eu',
|
||||||
|
locale=event.settings.locale,
|
||||||
|
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||||
|
expires=now(), code="PREVIEW1234", total=119)
|
||||||
|
|
||||||
|
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
|
||||||
|
sample = {k: str(v) for k, v in scheme['sample'].items()}
|
||||||
|
position = order.positions.create(item=item, attendee_name_parts=sample, price=item.default_price)
|
||||||
|
s = event.subevents.first()
|
||||||
|
order.positions.create(item=item2, attendee_name_parts=sample, price=item.default_price, addon_to=position, subevent=s)
|
||||||
|
order.positions.create(item=item2, attendee_name_parts=sample, price=item.default_price, addon_to=position, subevent=s)
|
||||||
|
|
||||||
|
InvoiceAddress.objects.create(order=order, name_parts=sample, company=_("Sample company"))
|
||||||
|
return position
|
||||||
|
|
||||||
|
def preview(event: int, provider: str, provider_arguments: dict = {}):
|
||||||
event = Event.objects.get(id=event)
|
event = Event.objects.get(id=event)
|
||||||
|
|
||||||
with rolledback_transaction(), language(event.settings.locale, event.settings.region):
|
with rolledback_transaction(), language(event.settings.locale, event.settings.region):
|
||||||
item = event.items.create(name=_("Sample product"), default_price=Decimal('42.23'),
|
p = get_preview_position(event)
|
||||||
description=_("Sample product description"))
|
|
||||||
item2 = event.items.create(name=_("Sample workshop"), default_price=Decimal('23.40'))
|
|
||||||
|
|
||||||
from pretix.base.models import Order
|
|
||||||
order = event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
|
|
||||||
email='sample@pretix.eu',
|
|
||||||
locale=event.settings.locale,
|
|
||||||
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
|
||||||
expires=now(), code="PREVIEW1234", total=119)
|
|
||||||
|
|
||||||
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
|
|
||||||
sample = {k: str(v) for k, v in scheme['sample'].items()}
|
|
||||||
p = order.positions.create(item=item, attendee_name_parts=sample, price=item.default_price)
|
|
||||||
s = event.subevents.first()
|
|
||||||
order.positions.create(item=item2, attendee_name_parts=sample, price=item.default_price, addon_to=p, subevent=s)
|
|
||||||
order.positions.create(item=item2, attendee_name_parts=sample, price=item.default_price, addon_to=p, subevent=s)
|
|
||||||
|
|
||||||
InvoiceAddress.objects.create(order=order, name_parts=sample, company=_("Sample company"))
|
|
||||||
|
|
||||||
responses = register_ticket_outputs.send(event)
|
responses = register_ticket_outputs.send(event)
|
||||||
for receiver, response in responses:
|
for receiver, response in responses:
|
||||||
prov = response(event)
|
prov = response(event)
|
||||||
if prov.identifier == provider:
|
if prov.identifier == provider:
|
||||||
return prov.generate(p)
|
return prov.generate(p, **provider_arguments)
|
||||||
|
|
||||||
|
|
||||||
def get_tickets_for_order(order, base_position=None):
|
def get_tickets_for_order(order, base_position=None):
|
||||||
|
|||||||
@@ -20,10 +20,12 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import copy
|
import copy
|
||||||
|
import typing
|
||||||
|
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
T = typing.TypeVar('T', bound=models.Model)
|
||||||
|
|
||||||
class Thumbnail(models.Model):
|
class Thumbnail(models.Model):
|
||||||
source = models.CharField(max_length=255)
|
source = models.CharField(max_length=255)
|
||||||
@@ -45,6 +47,13 @@ def modelcopy(obj: models.Model, **kwargs):
|
|||||||
setattr(n, f.name, copy.deepcopy(val))
|
setattr(n, f.name, copy.deepcopy(val))
|
||||||
return n
|
return n
|
||||||
|
|
||||||
|
def modelclone(obj: T, **kwargs) -> T:
|
||||||
|
new = copy.copy(obj)
|
||||||
|
new.pk = None
|
||||||
|
new._state.adding = True
|
||||||
|
for k,v in kwargs.items():
|
||||||
|
setattr(new, k, v)
|
||||||
|
return new
|
||||||
|
|
||||||
# django 5 contains this in django.utils.choices.flatten_choices
|
# django 5 contains this in django.utils.choices.flatten_choices
|
||||||
def flatten_choices(choices):
|
def flatten_choices(choices):
|
||||||
|
|||||||
101
src/pretix/plugins/wallet/forms.py
Normal file
101
src/pretix/plugins/wallet/forms.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from pretix.control.forms import ClearableBasenameFileInput
|
||||||
|
from django.core.files import File
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_rsa_privkey(value: File):
|
||||||
|
value = value.read().strip()
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
value = value.decode()
|
||||||
|
if not value:
|
||||||
|
return
|
||||||
|
if not re.match(r"^-----BEGIN( (RSA |ENCRYPTED )?PRIVATE KEY-----).*-----END\1$", value, re.DOTALL):
|
||||||
|
raise ValidationError(
|
||||||
|
_(
|
||||||
|
"This does not look like an RSA private key in PEM format (it misses the correct begin or end signifiers)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CertificateFileField(forms.FileField):
|
||||||
|
widget = ClearableBasenameFileInput
|
||||||
|
|
||||||
|
def clean(self, value, *args, **kwargs):
|
||||||
|
value = super().clean(value, *args, **kwargs)
|
||||||
|
if isinstance(value, UploadedFile):
|
||||||
|
value.open("rb")
|
||||||
|
value.seek(0)
|
||||||
|
content = value.read()
|
||||||
|
if (
|
||||||
|
content.startswith(b"-----BEGIN CERTIFICATE-----")
|
||||||
|
and b"-----BEGIN CERTIFICATE-----" in content
|
||||||
|
):
|
||||||
|
return SimpleUploadedFile("cert.pem", content, "text/plain")
|
||||||
|
|
||||||
|
openssl_cmd = [
|
||||||
|
"openssl",
|
||||||
|
"x509",
|
||||||
|
"-inform",
|
||||||
|
"DER",
|
||||||
|
"-outform",
|
||||||
|
"PEM",
|
||||||
|
]
|
||||||
|
process = subprocess.Popen(
|
||||||
|
openssl_cmd,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
process.stdin.write(content)
|
||||||
|
pem, error = process.communicate()
|
||||||
|
if process.returncode != 0:
|
||||||
|
logger.info("Trying to convert a DER to PEM failed: {}".format(error))
|
||||||
|
raise ValidationError(
|
||||||
|
_(
|
||||||
|
"This does not look like a X509 certificate in either PEM or DER format"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return SimpleUploadedFile("cert.pem", pem, "text/plain")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class PNGImageField(forms.FileField):
|
||||||
|
widget = ClearableBasenameFileInput
|
||||||
|
|
||||||
|
def clean(self, value, *args, **kwargs):
|
||||||
|
value = super().clean(value, *args, **kwargs)
|
||||||
|
if isinstance(value, UploadedFile):
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
except ImportError:
|
||||||
|
return value
|
||||||
|
|
||||||
|
value.open("rb")
|
||||||
|
value.seek(0)
|
||||||
|
try:
|
||||||
|
with (
|
||||||
|
Image.open(value, formats=settings.PILLOW_FORMATS_IMAGE) as im,
|
||||||
|
tempfile.NamedTemporaryFile("rb", suffix=".png") as tmpfile,
|
||||||
|
):
|
||||||
|
im.save(tmpfile.name)
|
||||||
|
tmpfile.seek(0)
|
||||||
|
return SimpleUploadedFile(
|
||||||
|
"picture.png", tmpfile.read(), "image png"
|
||||||
|
)
|
||||||
|
except IOError:
|
||||||
|
logger.exception("Could not convert image to PNG.")
|
||||||
|
raise ValidationError(
|
||||||
|
_("The file you uploaded could not be converted to PNG format.")
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
@@ -10,7 +10,7 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("pretixbase", "0300_alter_customer_locale_alter_user_locale"),
|
("pretixbase", "0297_outgoingmail"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Generated by Django 5.2.13 on 2026-06-09 18:04
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("wallet", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="walletlayoutitem",
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="walletlayout",
|
||||||
|
name="default",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="walletlayoutitem",
|
||||||
|
name="item",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="walletlayout",
|
||||||
|
to="pretixbase.item",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="walletlayout",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
models.F("event"),
|
||||||
|
condition=models.Q(("default", True)),
|
||||||
|
name="one_default_wallet_per_event",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -20,12 +20,16 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import constraints, Q
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import LoggedModel
|
from pretix.base.models import LoggedModel
|
||||||
from django_scopes import ScopedManager
|
from django_scopes import ScopedManager
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
from pretix.plugins.wallet.styles import get_style
|
||||||
|
from pretix.plugins.wallet.styles.base import PassLayout
|
||||||
|
|
||||||
|
|
||||||
class WalletLayout(LoggedModel):
|
class WalletLayout(LoggedModel):
|
||||||
event = models.ForeignKey(
|
event = models.ForeignKey(
|
||||||
@@ -37,9 +41,18 @@ class WalletLayout(LoggedModel):
|
|||||||
max_length=190,
|
max_length=190,
|
||||||
verbose_name=_('Name')
|
verbose_name=_('Name')
|
||||||
)
|
)
|
||||||
|
default = models.BooleanField(
|
||||||
|
verbose_name=_('Default'),
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
objects = ScopedManager(organizer='event__organizer')
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
constraints.UniqueConstraint("event", condition=Q(default=True), name="one_default_wallet_per_event")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class WalletPlatformLayout(LoggedModel):
|
class WalletPlatformLayout(LoggedModel):
|
||||||
parent = models.ForeignKey(WalletLayout, on_delete=models.CASCADE, related_name="platform_layouts")
|
parent = models.ForeignKey(WalletLayout, on_delete=models.CASCADE, related_name="platform_layouts")
|
||||||
@@ -53,15 +66,16 @@ class WalletPlatformLayout(LoggedModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (('parent', 'platform'),)
|
unique_together = (('parent', 'platform'),)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pass_layout(self):
|
||||||
|
style = get_style(self.platform, self.style)
|
||||||
|
return PassLayout(style=style, layout=self.layout)
|
||||||
|
|
||||||
class WalletLayoutItem(models.Model):
|
class WalletLayoutItem(models.Model):
|
||||||
item = models.ForeignKey('pretixbase.Item', null=True, blank=True, related_name='walletlayout_assignments',
|
item = models.OneToOneField('pretixbase.Item', null=True, blank=True, related_name='walletlayout',
|
||||||
on_delete=models.CASCADE)
|
on_delete=models.CASCADE)
|
||||||
layout = models.ForeignKey(WalletLayout, on_delete=models.CASCADE, related_name='item_assignments')
|
layout = models.ForeignKey(WalletLayout, on_delete=models.CASCADE, related_name='item_assignments')
|
||||||
|
|
||||||
class Meta:
|
|
||||||
unique_together = (('item', 'layout'),)
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.item.event != self.layout.event:
|
if self.item.event != self.layout.event:
|
||||||
raise ValidationError("cannot bind layout to item of different event")
|
raise ValidationError("cannot bind layout to item of different event")
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
from pretix.base.signals import register_ticket_outputs
|
from pretix.base.signals import register_ticket_outputs, register_global_settings
|
||||||
from .ticketoutput import OUTPUTS
|
from .ticketoutput import OUTPUTS
|
||||||
|
|
||||||
def connect_signals():
|
def connect_signals():
|
||||||
@@ -30,6 +30,8 @@ def connect_signals():
|
|||||||
def register(sender, **kwargs):
|
def register(sender, **kwargs):
|
||||||
return o
|
return o
|
||||||
return register
|
return register
|
||||||
register_ticket_outputs.connect(get_register_func(output), dispatch_uid=f"output_{output.identifier}")
|
register_ticket_outputs.connect(get_register_func(output), dispatch_uid=f"wallet_output_{output.identifier}")
|
||||||
|
if hasattr(output, "get_global_settings"):
|
||||||
|
register_global_settings.connect(output.get_global_settings, dispatch_uid=f"wallet_global_settings_{output.identifier}")
|
||||||
|
|
||||||
connect_signals()
|
connect_signals()
|
||||||
|
|||||||
@@ -62,6 +62,27 @@ function saveLayout(e: SubmitEvent) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openForm(url: string, data: Record<string, string>) {
|
||||||
|
|
||||||
|
let form = document.createElement("form");
|
||||||
|
form.target = "_blank";
|
||||||
|
form.method = "POST";
|
||||||
|
form.action = url;
|
||||||
|
form.style.display = "none";
|
||||||
|
|
||||||
|
for (var key in data) {
|
||||||
|
var input = document.createElement("input");
|
||||||
|
input.type = "hidden";
|
||||||
|
input.name = key;
|
||||||
|
input.value = data[key];
|
||||||
|
form.appendChild(input);
|
||||||
|
}
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
|
document.body.removeChild(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const currentPlatform = ref(PLATFORMS[0].identifier);
|
const currentPlatform = ref(PLATFORMS[0].identifier);
|
||||||
const currentLayout = computed(() => ({}));
|
const currentLayout = computed(() => ({}));
|
||||||
const platformStyles = computed(() => {
|
const platformStyles = computed(() => {
|
||||||
@@ -85,6 +106,10 @@ const platformChoices = computed(() => {
|
|||||||
return [[null, "Do not generate pass"], ...Object.values(platformStyles.value).map(x => [x.identifier, x.name])]
|
return [[null, "Do not generate pass"], ...Object.values(platformStyles.value).map(x => [x.identifier, x.name])]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function openPreview(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
openForm("../../preview/", {"csrfmiddlewaretoken": CSRF_TOKEN, "platform": currentPlatform.value, "style": platformLayout.value.style, "layout": JSON.stringify(platformLayout.value.layout)})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
@@ -117,5 +142,7 @@ const platformChoices = computed(() => {
|
|||||||
pre
|
pre
|
||||||
code {{ wallet_layout }}
|
code {{ wallet_layout }}
|
||||||
.form-group.submit-group
|
.form-group.submit-group
|
||||||
|
button.btn.btn-lg.btn-default(type="button" @click="openPreview") Preview
|
||||||
button.btn.btn-primary.btn-save(type="submit") Submit
|
button.btn.btn-primary.btn-save(type="submit") Submit
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,4 +15,13 @@ AVAILABLE_STYLES_DICT = {
|
|||||||
plat: {s.identifier: s for s in styls} for plat, styls in AVAILABLE_STYLES.items()
|
plat: {s.identifier: s for s in styls} for plat, styls in AVAILABLE_STYLES.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# TODO? move to models?
|
||||||
|
def get_platform(identifier: str):
|
||||||
|
for platform in AVAILABLE_PLATFORMS:
|
||||||
|
if platform.identifier == identifier:
|
||||||
|
return platform
|
||||||
|
|
||||||
|
def get_style(platform: str, identifier: str):
|
||||||
|
return AVAILABLE_STYLES_DICT.get(platform, {}).get(identifier)
|
||||||
|
|
||||||
__all__ = ["AVAILABLE_PLATFORMS", "AVAILABLE_STYLES", "PassLayout"]
|
__all__ = ["AVAILABLE_PLATFORMS", "AVAILABLE_STYLES", "PassLayout"]
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from .base import (
|
|||||||
WalletPlatform,
|
WalletPlatform,
|
||||||
PassStyle,
|
PassStyle,
|
||||||
PlaceholderFieldEntry,
|
PlaceholderFieldEntry,
|
||||||
|
PassLayout,
|
||||||
)
|
)
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
@@ -14,15 +15,56 @@ import io
|
|||||||
import hashlib
|
import hashlib
|
||||||
import zipfile
|
import zipfile
|
||||||
import cryptography
|
import cryptography
|
||||||
|
import cryptography.x509
|
||||||
import cryptography.hazmat.primitives.serialization.pkcs7
|
import cryptography.hazmat.primitives.serialization.pkcs7
|
||||||
import json
|
import json
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
|
from pretix.base.models import OrderPosition
|
||||||
|
from django.utils.encoding import force_bytes
|
||||||
|
|
||||||
class ApplePlatform(WalletPlatform):
|
class ApplePlatform(WalletPlatform):
|
||||||
identifier = "apple"
|
identifier = "apple"
|
||||||
name = _("Apple")
|
name = _("Apple")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate(cls, layout: PassLayout, op: OrderPosition):
|
||||||
|
from ..views import get_layout_variables
|
||||||
|
|
||||||
|
order = op.order
|
||||||
|
event = order.event
|
||||||
|
filename = "{}-{}.pkpass".format(order.event.slug, order.code)
|
||||||
|
|
||||||
|
ticket = str(op.item.name)
|
||||||
|
if op.variation:
|
||||||
|
ticket += " - " + str(op.variation)
|
||||||
|
|
||||||
|
serialNumber = "%s-%s-%s-%d" % (
|
||||||
|
order.event.organizer.slug,
|
||||||
|
order.event.slug,
|
||||||
|
order.code,
|
||||||
|
op.pk,
|
||||||
|
)
|
||||||
|
breakpoint()
|
||||||
|
context = {
|
||||||
|
"placeholders": get_layout_variables(op.order.event),
|
||||||
|
"evaluation_context": [op, order, order.event],
|
||||||
|
"ca_certificate": order.event.settings.wallet_apple_ca_certificate.read(),
|
||||||
|
"certificate": order.event.settings.wallet_apple_certificate.read(),
|
||||||
|
"key": order.event.settings.wallet_apple_key.read(),
|
||||||
|
"password": order.event.settings.wallet_apple_key_password,
|
||||||
|
"description": _("Ticket for {event} ({product})").format( # TODO: i18n
|
||||||
|
event=event.name, product=ticket
|
||||||
|
),
|
||||||
|
"organizationName": event.organizer.name,
|
||||||
|
"passTypeIdentifier": order.event.settings.wallet_apple_pass_type_id,
|
||||||
|
"teamIdentifier": order.event.settings.wallet_apple_team_id,
|
||||||
|
"serialNumber": serialNumber,
|
||||||
|
"locales": event.settings.locales,
|
||||||
|
}
|
||||||
|
|
||||||
|
data = layout.generate(context)
|
||||||
|
return filename, "application/vnd.apple.pkpass", data
|
||||||
|
|
||||||
|
|
||||||
class StringResource:
|
class StringResource:
|
||||||
# mapping string in default event locale -> LazyI18nString
|
# mapping string in default event locale -> LazyI18nString
|
||||||
@@ -58,13 +100,13 @@ class StringResource:
|
|||||||
class SignedZipFile:
|
class SignedZipFile:
|
||||||
"""Generates a zip-file with manifest and signature as apple expects a pkpass file to be"""
|
"""Generates a zip-file with manifest and signature as apple expects a pkpass file to be"""
|
||||||
|
|
||||||
def __init__(self, ca_certificate, certificate, key, password):
|
def __init__(self, ca_certificate: str | bytes, certificate: str | bytes, key: str | bytes, password):
|
||||||
self.ca_certificate = cryptography.x509.load_pem_x509_certificate(
|
self.ca_certificate = cryptography.x509.load_pem_x509_certificate(
|
||||||
ca_certificate
|
force_bytes(ca_certificate)
|
||||||
)
|
)
|
||||||
self.certificate = cryptography.x509.load_pem_x509_certificate(certificate)
|
self.certificate = cryptography.x509.load_pem_x509_certificate(force_bytes(certificate))
|
||||||
self.key = cryptography.hazmat.primitives.serialization.load_pem_private_key(
|
self.key = cryptography.hazmat.primitives.serialization.load_pem_private_key(
|
||||||
key, password
|
force_bytes(key), force_bytes(password) if password else None
|
||||||
)
|
)
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
@@ -109,8 +151,6 @@ class SignedZipFile:
|
|||||||
|
|
||||||
|
|
||||||
class AppleWalletStyle(PassStyle):
|
class AppleWalletStyle(PassStyle):
|
||||||
platform = ApplePlatform
|
|
||||||
|
|
||||||
def pass_content(self, fields, strings):
|
def pass_content(self, fields, strings):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@@ -145,17 +185,17 @@ class AppleWalletStyle(PassStyle):
|
|||||||
context["key"],
|
context["key"],
|
||||||
context["password"],
|
context["password"],
|
||||||
)
|
)
|
||||||
strings = StringResource(locales=context['locales'])
|
strings = StringResource(locales=context["locales"])
|
||||||
|
|
||||||
pass_json = self.generate_pass_json(fields, context, strings)
|
pass_json = self.generate_pass_json(fields, context, strings)
|
||||||
print(pass_json)
|
print(pass_json)
|
||||||
if fields['logo']:
|
if fields["logo"]:
|
||||||
logo = fields['logo'][0]['value']
|
logo = fields["logo"][0]["value"]
|
||||||
else:
|
else:
|
||||||
logo = open(finders.find("pretix_passbook/logo.png"), "rb")
|
logo = open(finders.find("pretix_passbook/logo.png"), "rb")
|
||||||
|
|
||||||
if fields['icon']:
|
if fields["icon"]:
|
||||||
icon = fields['icon'][0]['value']
|
icon = fields["icon"][0]["value"]
|
||||||
else:
|
else:
|
||||||
icon = open(finders.find("pretix_passbook/icon.png"), "rb")
|
icon = open(finders.find("pretix_passbook/icon.png"), "rb")
|
||||||
|
|
||||||
@@ -214,7 +254,7 @@ class AppleWalletEventTicket(AppleWalletStyle):
|
|||||||
), # TODO: validation of max field count if combined "Coupons, store cards, and generic passes with a square barcode can have a total of up to four secondary and auxiliary fields, combined."
|
), # TODO: validation of max field count if combined "Coupons, store cards, and generic passes with a square barcode can have a total of up to four secondary and auxiliary fields, combined."
|
||||||
TextFieldGroup(
|
TextFieldGroup(
|
||||||
identifier="headers", name=_("Header"), max_entries=3
|
identifier="headers", name=_("Header"), max_entries=3
|
||||||
), # TODO: header image
|
),
|
||||||
TextFieldGroup(identifier="auxillary", name=_("Auxillary"), max_entries=4),
|
TextFieldGroup(identifier="auxillary", name=_("Auxillary"), max_entries=4),
|
||||||
TextFieldGroup(identifier="back", name=_("Back")),
|
TextFieldGroup(identifier="back", name=_("Back")),
|
||||||
]
|
]
|
||||||
@@ -222,24 +262,32 @@ class AppleWalletEventTicket(AppleWalletStyle):
|
|||||||
|
|
||||||
def convert_fields(self, strings, fields, prefix):
|
def convert_fields(self, strings, fields, prefix):
|
||||||
converted = []
|
converted = []
|
||||||
for i,f in enumerate(fields):
|
for i, f in enumerate(fields):
|
||||||
converted_field = {**f, "key": f"{prefix}-{i}"}
|
converted_field = {**f, "key": f"{prefix}-{i}"}
|
||||||
if "label" in converted_field and isinstance(converted_field['label'], LazyI18nString):
|
if "label" in converted_field and isinstance(
|
||||||
strings.add_entry(f"{prefix}-{i}-label", converted_field['label'])
|
converted_field["label"], LazyI18nString
|
||||||
converted_field['label'] = f"{prefix}-{i}-label"
|
):
|
||||||
|
strings.add_entry(f"{prefix}-{i}-label", converted_field["label"])
|
||||||
|
converted_field["label"] = f"{prefix}-{i}-label"
|
||||||
|
|
||||||
if isinstance(converted_field['value'], LazyI18nString):
|
if isinstance(converted_field["value"], LazyI18nString):
|
||||||
strings.add_entry(f"{prefix}-{i}-value", converted_field['value'])
|
strings.add_entry(f"{prefix}-{i}-value", converted_field["value"])
|
||||||
converted_field['value'] = f"{prefix}-{i}-value"
|
converted_field["value"] = f"{prefix}-{i}-value"
|
||||||
converted.append(converted_field)
|
converted.append(converted_field)
|
||||||
return converted
|
return converted
|
||||||
|
|
||||||
def pass_content(self, fields, strings):
|
def pass_content(self, fields, strings):
|
||||||
return {
|
return {
|
||||||
"eventTicket": {
|
"eventTicket": {
|
||||||
"primaryFields": self.convert_fields(strings, fields['primary'], 'primary'),
|
"primaryFields": self.convert_fields(
|
||||||
"secondaryFields": self.convert_fields(strings, fields['secondary'], 'secondary'),
|
strings, fields["primary"], "primary"
|
||||||
"auxillaryFields": self.convert_fields(strings, fields['auxillary'], 'auxillary'),
|
),
|
||||||
"backFields": self.convert_fields(strings, fields['back'], 'back'),
|
"secondaryFields": self.convert_fields(
|
||||||
|
strings, fields["secondary"], "secondary"
|
||||||
|
),
|
||||||
|
"auxillaryFields": self.convert_fields(
|
||||||
|
strings, fields["auxillary"], "auxillary"
|
||||||
|
),
|
||||||
|
"backFields": self.convert_fields(strings, fields["back"], "back"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,7 +222,6 @@ class ImageFieldGroup(PlaceholderFieldGroup):
|
|||||||
|
|
||||||
|
|
||||||
class PassStyle:
|
class PassStyle:
|
||||||
platform: type[WalletPlatform]
|
|
||||||
identifier: str # unique within platform
|
identifier: str # unique within platform
|
||||||
name: str
|
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
|
# 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
|
||||||
@@ -232,7 +231,6 @@ class PassStyle:
|
|||||||
|
|
||||||
def asdict(self):
|
def asdict(self):
|
||||||
return {
|
return {
|
||||||
"platform": self.platform.identifier,
|
|
||||||
"identifier": self.identifier,
|
"identifier": self.identifier,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"fieldgroups": [x.asdict() for x in self.fieldgroups],
|
"fieldgroups": [x.asdict() for x in self.fieldgroups],
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{% extends "pretixcontrol/event/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% block title %}{% trans "Wallet layout" %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% trans "Wallet layout" %}</h1>
|
||||||
|
<form action="" method="post" class="form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>{% blocktrans with name=object.name %}Are you sure you want to delete <strong>"{{ name }}"</strong>?{% endblocktrans %}</p>
|
||||||
|
<div class="form-group submit-group">
|
||||||
|
<a href="{% url "plugins:wallet:index" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
|
||||||
|
{% trans "Cancel" %}
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-danger btn-save">
|
||||||
|
{% trans "Delete" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
{% block title %}{% trans "Wallet layouts" %}{% endblock %}
|
{% block title %}{% trans "Wallet layouts" %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans "Wallet layouts" %}</h1>
|
<h1>{% trans "Wallet layouts" %}</h1>
|
||||||
{% if layouts|length == 0 %}
|
{% if object_list|length == 0 %}
|
||||||
<div class="empty-collection">
|
<div class="empty-collection">
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for l in layouts %}
|
{% for l in object_list %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if "can_change_event_settings" in request.eventpermset %}
|
{% if "can_change_event_settings" in request.eventpermset %}
|
||||||
|
|||||||
@@ -25,14 +25,17 @@ from pretix.base.ticketoutput import BaseTicketOutput
|
|||||||
from pretix.base.models import Event
|
from pretix.base.models import Event
|
||||||
from pretix.base.settings import SettingsSandbox
|
from pretix.base.settings import SettingsSandbox
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from .styles import AVAILABLE_STYLES_DICT
|
from .styles import AVAILABLE_STYLES_DICT
|
||||||
|
from .styles.base import PassLayout, WalletPlatform
|
||||||
from .styles.apple import ApplePlatform
|
from .styles.apple import ApplePlatform
|
||||||
from .styles.google import GooglePlatform
|
from .styles.google import GooglePlatform
|
||||||
|
from collections import OrderedDict
|
||||||
from .models import WalletLayout
|
from .models import WalletLayout
|
||||||
from .views import get_layout_variables
|
from .views import get_layout_variables
|
||||||
|
from django import forms
|
||||||
|
from .forms import CertificateFileField, validate_rsa_privkey
|
||||||
|
from pretix.control.forms import ClearableBasenameFileInput
|
||||||
|
|
||||||
logger = logging.getLogger("pretix.plugins.wallet")
|
logger = logging.getLogger("pretix.plugins.wallet")
|
||||||
|
|
||||||
@@ -55,6 +58,7 @@ class WalletSettingsHolder(BaseTicketOutput):
|
|||||||
|
|
||||||
class WalletOutput(BaseTicketOutput):
|
class WalletOutput(BaseTicketOutput):
|
||||||
settings_form_fields = []
|
settings_form_fields = []
|
||||||
|
platform: WalletPlatform
|
||||||
|
|
||||||
def __init__(self, event: Event):
|
def __init__(self, event: Event):
|
||||||
super().__init__(event)
|
super().__init__(event)
|
||||||
@@ -62,6 +66,14 @@ class WalletOutput(BaseTicketOutput):
|
|||||||
"ticketoutput", WalletSettingsHolder.identifier, event
|
"ticketoutput", WalletSettingsHolder.identifier, event
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def generate(self, op):
|
||||||
|
if hasattr(op.item, "walletlayout"):
|
||||||
|
wallet_layout = op.item.walletlayout
|
||||||
|
else:
|
||||||
|
wallet_layout = op.event.wallet_layouts.get(default=True)
|
||||||
|
platform_layout = get_object_or_404(wallet_layout.platform_layouts, platform=self.platform.identifier)
|
||||||
|
return self.platform.generate(platform_layout.pass_layout, op)
|
||||||
|
|
||||||
|
|
||||||
class GoogleWalletTicketOutput(WalletOutput):
|
class GoogleWalletTicketOutput(WalletOutput):
|
||||||
identifier = "wallet_google"
|
identifier = "wallet_google"
|
||||||
@@ -76,61 +88,75 @@ class AppleWalletTicketOutput(WalletOutput):
|
|||||||
download_button_text = "Add to Apple Wallet"
|
download_button_text = "Add to Apple Wallet"
|
||||||
platform = ApplePlatform
|
platform = ApplePlatform
|
||||||
|
|
||||||
def generate(self, op):
|
def get_global_settings(sender, **kwargs):
|
||||||
order = op.order
|
return OrderedDict(
|
||||||
event = order.event
|
[
|
||||||
filename = "{}-{}.pkpass".format(order.event.slug, order.code)
|
(
|
||||||
|
"wallet_apple_team_id",
|
||||||
# layout = self.override_layout_signal.send_chained(
|
forms.CharField(
|
||||||
# order.event, 'layout', orderposition=op, layout=self.layout_map.get(
|
label=_("Apple Wallet Pass team ID"),
|
||||||
# (op.item_id, self.override_channel or order.sales_channel.identifier),
|
required=False,
|
||||||
# self.layout_map.get(
|
),
|
||||||
# (op.item_id, 'web'),
|
),
|
||||||
# self.default_layout
|
(
|
||||||
# )
|
"wallet_apple_pass_type_id",
|
||||||
# )
|
forms.CharField(
|
||||||
# )
|
label=_("Apple Wallet Pass type"),
|
||||||
layout = WalletLayout.objects.get(pk=1)
|
required=False,
|
||||||
platform_layout = layout.platform_layouts.get(platform=self.platform.identifier)
|
),
|
||||||
|
),
|
||||||
ticket = str(op.item.name)
|
(
|
||||||
if op.variation:
|
"wallet_apple_certificate",
|
||||||
ticket += " - " + str(op.variation)
|
CertificateFileField(
|
||||||
|
label=_("Apple Wallet Pass certificate file"),
|
||||||
serialNumber = "%s-%s-%s-%d" % (
|
required=False,
|
||||||
order.event.organizer.slug,
|
),
|
||||||
order.event.slug,
|
),
|
||||||
order.code,
|
(
|
||||||
op.pk,
|
"wallet_apple_ca_certificate",
|
||||||
|
CertificateFileField(
|
||||||
|
label=_("Apple Wallet Pass CA Certificate"),
|
||||||
|
help_text=_(
|
||||||
|
"You can download the current CA certificate from apple at "
|
||||||
|
"https://www.apple.com/certificateauthority/AppleWWDRCAG4.cer"
|
||||||
|
),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"wallet_apple_key",
|
||||||
|
forms.FileField(
|
||||||
|
label=_("Apple Wallet Pass secret key"),
|
||||||
|
required=False,
|
||||||
|
validators=[validate_rsa_privkey],
|
||||||
|
widget=ClearableBasenameFileInput
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"wallet_apple_key_password",
|
||||||
|
forms.CharField(
|
||||||
|
label=_("Apple Wallet Pass key password"),
|
||||||
|
widget=forms.PasswordInput(render_value=True),
|
||||||
|
required=False,
|
||||||
|
help_text=_(
|
||||||
|
"Optional, only necessary if the key entered above requires a password to use."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
context = {
|
|
||||||
"placeholders": get_layout_variables(op.order.event),
|
|
||||||
"evaluation_context": [op, order, order.event],
|
|
||||||
"ca_certificate": open(
|
|
||||||
"/Users/engelhardt/code/tmp/wallet/apple/ca_cert.pem", "rb"
|
|
||||||
).read(),
|
|
||||||
"certificate": open(
|
|
||||||
"/Users/engelhardt/code/tmp/wallet/apple/cert.pem", "rb"
|
|
||||||
).read(),
|
|
||||||
"key": open(
|
|
||||||
"/Users/engelhardt/code/tmp/wallet/apple/secret_key.pem", "rb"
|
|
||||||
).read(),
|
|
||||||
"password": None,
|
|
||||||
"description": _("Ticket for {event} ({product})").format( # TODO: i18n
|
|
||||||
event=event.name, product=ticket
|
|
||||||
),
|
|
||||||
"organizationName": event.organizer.name,
|
|
||||||
"passTypeIdentifier": "pass.test.test",
|
|
||||||
"teamIdentifier": "TEST123456",
|
|
||||||
"serialNumber": serialNumber,
|
|
||||||
"locales": event.settings.locales
|
|
||||||
}
|
|
||||||
|
|
||||||
data = AVAILABLE_STYLES_DICT[self.platform.identifier][platform_layout.style].generate(
|
|
||||||
platform_layout.layout, context
|
|
||||||
)
|
|
||||||
return filename, "application/vnd.apple.pkpass", data
|
|
||||||
|
|
||||||
|
# settings_hierarkey.add_default("wallet_apple_certificate_file", None, File)
|
||||||
|
# settings_hierarkey.add_default("wallet_apple_wwdr_certificate_file", None, File)
|
||||||
|
# settings_hierarkey.add_default("ticketoutput_wallet_apple_background", None, File)
|
||||||
|
# settings_hierarkey.add_default("ticketoutput_wallet_apple_background2x", None, File)
|
||||||
|
# settings_hierarkey.add_default("ticketoutput_wallet_apple_background3x", None, File)
|
||||||
|
# settings_hierarkey.add_default("ticketoutput_wallet_apple_icon", None, File)
|
||||||
|
# settings_hierarkey.add_default("ticketoutput_wallet_apple_icon2x", None, File)
|
||||||
|
# settings_hierarkey.add_default("ticketoutput_wallet_apple_icon3x", None, File)
|
||||||
|
# settings_hierarkey.add_default("ticketoutput_wallet_apple_logo", None, File)
|
||||||
|
# settings_hierarkey.add_default("ticketoutput_wallet_apple_logo2x", None, File)
|
||||||
|
# settings_hierarkey.add_default("ticketoutput_wallet_apple_logo3x", None, File)
|
||||||
|
|
||||||
OUTPUTS = [WalletSettingsHolder, GoogleWalletTicketOutput, AppleWalletTicketOutput]
|
OUTPUTS = [WalletSettingsHolder, GoogleWalletTicketOutput, AppleWalletTicketOutput]
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ from pretix.api.urls import event_router
|
|||||||
from .views import (
|
from .views import (
|
||||||
LayoutEditorView,
|
LayoutEditorView,
|
||||||
LayoutCreateView,
|
LayoutCreateView,
|
||||||
LayoutListView
|
LayoutListView,
|
||||||
|
LayoutPreviewView,
|
||||||
|
LayoutSetDefault,
|
||||||
|
LayoutDelete
|
||||||
)
|
)
|
||||||
from .api import WalletLayoutViewSet
|
from .api import WalletLayoutViewSet
|
||||||
|
|
||||||
@@ -36,10 +39,12 @@ urlpatterns = [
|
|||||||
LayoutCreateView.as_view(), name='add'),
|
LayoutCreateView.as_view(), name='add'),
|
||||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/wallet/edit/(?P<layout>[^/]+)/$',
|
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/wallet/edit/(?P<layout>[^/]+)/$',
|
||||||
LayoutEditorView.as_view(), name='edit'),
|
LayoutEditorView.as_view(), name='edit'),
|
||||||
|
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/wallet/preview/$',
|
||||||
|
LayoutPreviewView.as_view(), name='preview'),
|
||||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/wallet/default/(?P<layout>[^/]+)/$', # TODO
|
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/wallet/default/(?P<layout>[^/]+)/$', # TODO
|
||||||
LayoutEditorView.as_view(), name='default'),
|
LayoutSetDefault.as_view(), name='default'),
|
||||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/wallet/delete/(?P<layout>[^/]+)/$', # TODO
|
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/wallet/delete/(?P<layout>[^/]+)/$', # TODO
|
||||||
LayoutEditorView.as_view(), name='delete'),
|
LayoutDelete.as_view(), name='delete'),
|
||||||
]
|
]
|
||||||
|
|
||||||
event_router.register('walletlayouts', WalletLayoutViewSet)
|
event_router.register('walletlayouts', WalletLayoutViewSet)
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.http import Http404
|
from django.core.exceptions import BadRequest
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import CreateView, DetailView, ListView
|
from django.views.generic import CreateView, DetailView, ListView, DeleteView, View
|
||||||
|
from pretix.base.i18n import language
|
||||||
from pretix.base.pdf import get_images, get_variables
|
from pretix.base.pdf import get_images, get_variables
|
||||||
|
from pretix.base.services.tickets import get_preview_position
|
||||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from pretix.helpers.database import rolledback_transaction
|
||||||
|
from pretix.helpers.models import modelclone
|
||||||
from .models import WalletLayout
|
from .models import WalletLayout
|
||||||
from .styles import AVAILABLE_STYLES, AVAILABLE_PLATFORMS
|
from .styles import AVAILABLE_STYLES, AVAILABLE_PLATFORMS, AVAILABLE_STYLES_DICT, PassLayout
|
||||||
|
from django.contrib import messages
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
def get_layout_variables(event):
|
def get_layout_variables(event):
|
||||||
return {
|
return {
|
||||||
@@ -32,22 +42,25 @@ def get_editor_variables(event):
|
|||||||
for t, vs in get_layout_variables(event).items()
|
for t, vs in get_layout_variables(event).items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class WalletLayoutMixin:
|
||||||
class LayoutListView(EventPermissionRequiredMixin, ListView):
|
|
||||||
model = WalletLayout
|
model = WalletLayout
|
||||||
permission = "can_change_event_settings"
|
permission = "event.settings.general:write"
|
||||||
template_name = "pretixplugins/wallet/layout_list.html"
|
pk_url_kwarg = "layout"
|
||||||
context_object_name = "layouts"
|
context_object_name = "layouts"
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.wallet_layouts.all()
|
return self.request.event.wallet_layouts.all()
|
||||||
|
|
||||||
|
class LayoutListView(WalletLayoutMixin, EventPermissionRequiredMixin, ListView):
|
||||||
|
template_name = "pretixplugins/wallet/layout_list.html"
|
||||||
|
|
||||||
class LayoutEditorView(DetailView):
|
|
||||||
|
class LayoutDetailView(WalletLayoutMixin, EventPermissionRequiredMixin, DetailView):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutEditorView(LayoutDetailView):
|
||||||
template_name = "pretixplugins/wallet/edit.html"
|
template_name = "pretixplugins/wallet/edit.html"
|
||||||
model = WalletLayout
|
|
||||||
permission = "event.settings.general:write"
|
|
||||||
pk_url_kwarg = "layout"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs) -> dict[str, Any]:
|
def get_context_data(self, **kwargs) -> dict[str, Any]:
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
@@ -85,14 +98,26 @@ class WalletLayoutCreateForm(forms.ModelForm):
|
|||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class LayoutCreateView(CreateView):
|
class LayoutCreateView(WalletLayoutMixin, EventPermissionRequiredMixin, CreateView):
|
||||||
template_name = "pretixplugins/wallet/create.html"
|
template_name = "pretixplugins/wallet/create.html"
|
||||||
form_class = WalletLayoutCreateForm
|
form_class = WalletLayoutCreateForm
|
||||||
permission = "event.settings.general:write"
|
permission = "event.settings.general:write"
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
self.object = form.save()
|
||||||
|
if self.copy_from:
|
||||||
|
for pl in self.copy_from.platform_layouts.all():
|
||||||
|
modelclone(pl, parent=self.object).save()
|
||||||
|
return HttpResponseRedirect(self.get_success_url())
|
||||||
|
|
||||||
def get_form_kwargs(self) -> dict[str, Any]:
|
def get_form_kwargs(self) -> dict[str, Any]:
|
||||||
kwargs = super().get_form_kwargs()
|
kwargs = super().get_form_kwargs()
|
||||||
kwargs["event"] = self.request.event
|
kwargs["event"] = self.request.event
|
||||||
|
|
||||||
|
if self.copy_from:
|
||||||
|
kwargs['instance'] = modelclone(self.copy_from, default=False)
|
||||||
|
kwargs.setdefault('initial', {})
|
||||||
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def get_success_url(self) -> str:
|
def get_success_url(self) -> str:
|
||||||
@@ -104,3 +129,87 @@ class LayoutCreateView(CreateView):
|
|||||||
"layout": self.object.pk,
|
"layout": self.object.pk,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def copy_from(self) -> WalletLayout | None:
|
||||||
|
if self.request.GET.get("copy_from"):
|
||||||
|
try:
|
||||||
|
return self.get_queryset().get(pk=self.request.GET.get("copy_from"))
|
||||||
|
except WalletLayout.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class LayoutPreviewView(EventPermissionRequiredMixin, View):
|
||||||
|
permission = "event.settings.general:write"
|
||||||
|
|
||||||
|
def post(self, request, **kwargs):
|
||||||
|
event = request.event
|
||||||
|
platform_id = request.POST.get("platform")
|
||||||
|
style_id = request.POST.get("style")
|
||||||
|
layout = request.POST.get("layout")
|
||||||
|
|
||||||
|
platform = None
|
||||||
|
for p in AVAILABLE_PLATFORMS:
|
||||||
|
if p.identifier == platform_id:
|
||||||
|
platform = p
|
||||||
|
if not platform:
|
||||||
|
raise BadRequest("Unknown platform")
|
||||||
|
if style_id not in AVAILABLE_STYLES_DICT[platform_id]:
|
||||||
|
raise BadRequest("Unknown style")
|
||||||
|
style = AVAILABLE_STYLES_DICT[platform_id][style_id]
|
||||||
|
|
||||||
|
layout = json.loads(layout)
|
||||||
|
with rolledback_transaction(), language(request.event.settings.locale, request.event.settings.region):
|
||||||
|
p = get_preview_position(request.event)
|
||||||
|
layout = PassLayout(style=style, layout=layout)
|
||||||
|
context = {"placeholders": get_layout_variables(event)}
|
||||||
|
layout.validate(context=context)
|
||||||
|
|
||||||
|
fname, mimet, data = platform.generate(layout, p)
|
||||||
|
resp = HttpResponse(data, content_type=mimet)
|
||||||
|
ftype = fname.split(".")[-1]
|
||||||
|
resp['Content-Disposition'] = 'attachment; filename="ticket-preview.{}"'.format(ftype)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutSetDefault(LayoutDetailView):
|
||||||
|
@transaction.atomic
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
obj = self.get_object()
|
||||||
|
request.event.wallet_layouts.exclude(pk=obj.pk).update(default=False)
|
||||||
|
obj.default = True
|
||||||
|
obj.save(update_fields=['default'])
|
||||||
|
messages.success(self.request, _('Your changes have been saved.'))
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
def get_success_url(self) -> str:
|
||||||
|
return reverse('plugins:wallet:index', kwargs={
|
||||||
|
'organizer': self.request.event.organizer.slug,
|
||||||
|
'event': self.request.event.slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutDelete(WalletLayoutMixin, DeleteView):
|
||||||
|
template_name = 'pretixplugins/wallet/delete.html'
|
||||||
|
|
||||||
|
def get_success_url(self) -> str:
|
||||||
|
return reverse('plugins:wallet:index', kwargs={
|
||||||
|
'organizer': self.request.event.organizer.slug,
|
||||||
|
'event': self.request.event.slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def form_valid(self, form):
|
||||||
|
self.object = self.get_object()
|
||||||
|
self.object.log_action(action='pretix.plugins.wallet.layout.deleted', user=self.request.user)
|
||||||
|
self.object.delete()
|
||||||
|
|
||||||
|
if not self.request.event.wallet_layouts.filter(default=True).exists():
|
||||||
|
f = self.request.event.wallet_layouts.first()
|
||||||
|
if f:
|
||||||
|
f.default = True
|
||||||
|
f.save(update_fields=['default'])
|
||||||
|
|
||||||
|
messages.success(self.request, _('The selected layout been deleted.'))
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|||||||
Reference in New Issue
Block a user