mirror of
https://github.com/pretix/pretix.git
synced 2026-05-19 17:34:03 +00:00
WIP
This commit is contained in:
@@ -49,7 +49,7 @@ class WalletLayoutSerializer(I18nAwareModelSerializer):
|
|||||||
style = platform_styles[data["style"]]
|
style = platform_styles[data["style"]]
|
||||||
|
|
||||||
layout = PassLayout(style=style, layout=data["layout"])
|
layout = PassLayout(style=style, layout=data["layout"])
|
||||||
context = {"placeholders": {k: {"content": v['content']} for k,v in get_layout_variables(self.context['event']).items()}}
|
context = {"placeholders": get_layout_variables(self.context['event'])}
|
||||||
layout.validate(context=context)
|
layout.validate(context=context)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
from .base import (
|
from .base import (
|
||||||
FieldEntry,
|
|
||||||
FieldEntryType,
|
FieldEntryType,
|
||||||
FieldContentType,
|
|
||||||
ImageFieldGroup,
|
ImageFieldGroup,
|
||||||
PlaceholderFieldGroup,
|
PlaceholderFieldGroup,
|
||||||
|
PredefinedFieldGroup,
|
||||||
TextFieldGroup,
|
TextFieldGroup,
|
||||||
WalletPlatform,
|
WalletPlatform,
|
||||||
PassStyle,
|
PassStyle,
|
||||||
PlaceholderFieldEntry,
|
PlaceholderFieldEntry,
|
||||||
CustomFieldEntry,
|
|
||||||
)
|
)
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
@@ -17,10 +15,10 @@ import hashlib
|
|||||||
import zipfile
|
import zipfile
|
||||||
import cryptography
|
import cryptography
|
||||||
import cryptography.hazmat.primitives.serialization.pkcs7
|
import cryptography.hazmat.primitives.serialization.pkcs7
|
||||||
# from cryptography import x509
|
|
||||||
# from cryptography.hazmat.primitives import hashes, serialization
|
|
||||||
# from cryptography.hazmat.primitives.serialization import pkcs7
|
|
||||||
import json
|
import json
|
||||||
|
from django.contrib.staticfiles import finders
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ApplePlatform(WalletPlatform):
|
class ApplePlatform(WalletPlatform):
|
||||||
identifier = "apple"
|
identifier = "apple"
|
||||||
@@ -36,32 +34,39 @@ class StringResource:
|
|||||||
self.entries = {}
|
self.entries = {}
|
||||||
self.locales = set(locales)
|
self.locales = set(locales)
|
||||||
|
|
||||||
def add_entry(self, key: str, value: LazyI18nString): # TODO: replace LazyI18nString with dict or handle strings where data == ""
|
def add_entry(self, key: str, value: LazyI18nString):
|
||||||
if key in self.entries:
|
if key in self.entries:
|
||||||
raise ValueError(f"{key} already exists in this StringResource")
|
raise ValueError(f"{key} already exists in this StringResource")
|
||||||
self.entries[key] = value
|
self.entries[key] = value
|
||||||
if isinstance(value.data, dict):
|
|
||||||
self.locales |= value.data.keys()
|
|
||||||
|
|
||||||
def escape(self, string):
|
def escape(self, string):
|
||||||
return string.translate(str.maketrans({"\"": "\\\"", "\r": "\\r", "\n": "\\n", "\\": "\\\\"}))
|
return string.translate(
|
||||||
|
str.maketrans({'"': '\\"', "\r": "\\r", "\n": "\\n", "\\": "\\\\"})
|
||||||
|
)
|
||||||
|
|
||||||
def generate_resource(self, language):
|
def generate_resource(self, language):
|
||||||
output = ""
|
output = ""
|
||||||
for key, entry in self.entries.items():
|
for key, entry in self.entries.items():
|
||||||
output += f'"{self.escape(key)}" = "{self.escape(entry.localize(language))}";\n'
|
output += (
|
||||||
|
f'"{self.escape(key)}" = "{self.escape(entry.localize(language))}";\n'
|
||||||
|
)
|
||||||
return output.strip()
|
return output.strip()
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
return {language: self.generate_resource(language) for language in self.locales}
|
return {language: self.generate_resource(language) for language in self.locales}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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, certificate, key, password):
|
||||||
self.ca_certificate = cryptography.x509.load_pem_x509_certificate(ca_certificate)
|
self.ca_certificate = cryptography.x509.load_pem_x509_certificate(
|
||||||
|
ca_certificate
|
||||||
|
)
|
||||||
self.certificate = cryptography.x509.load_pem_x509_certificate(certificate)
|
self.certificate = cryptography.x509.load_pem_x509_certificate(certificate)
|
||||||
self.key = cryptography.hazmat.primitives.serialization.load_pem_private_key(key, password)
|
self.key = cryptography.hazmat.primitives.serialization.load_pem_private_key(
|
||||||
|
key, password
|
||||||
|
)
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
self.file = io.BytesIO()
|
self.file = io.BytesIO()
|
||||||
@@ -107,22 +112,45 @@ class SignedZipFile:
|
|||||||
class AppleWalletStyle(PassStyle):
|
class AppleWalletStyle(PassStyle):
|
||||||
platform = ApplePlatform
|
platform = ApplePlatform
|
||||||
|
|
||||||
def generate_pass_json(self, layout, context):
|
def pass_content(self, layout, context, strings):
|
||||||
pass_json = {}
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def generate_pass_json(self, layout, context, strings):
|
||||||
|
def add_from_context(key):
|
||||||
|
value = context.get(key)
|
||||||
|
if not value:
|
||||||
|
raise ValueError(f"{key} must be set to a truthy value")
|
||||||
|
return value
|
||||||
|
|
||||||
|
pass_json = {
|
||||||
|
"formatVersion": 1,
|
||||||
|
"description": add_from_context("description"),
|
||||||
|
"organizationName": add_from_context("organizationName"),
|
||||||
|
"passTypeIdentifier": add_from_context("passTypeIdentifier"),
|
||||||
|
"teamIdentifier": add_from_context("teamIdentifier"),
|
||||||
|
"serialNumber": add_from_context("serialNumber"),
|
||||||
|
**self.pass_content(layout, context, strings),
|
||||||
|
}
|
||||||
return pass_json
|
return pass_json
|
||||||
|
|
||||||
def generate(self, layout, context):
|
def generate(self, layout, context):
|
||||||
for key in ["certificate", "key", "wwdr_certificate", "password"]:
|
for key in ["ca_certificate", "certificate", "key", "password", "locales"]:
|
||||||
if key not in context:
|
if key not in context:
|
||||||
raise ValueError(f"{key} missing from context")
|
raise ValueError(f"{key} missing from context")
|
||||||
pkpass = SignedZipFile(
|
pkpass = SignedZipFile(
|
||||||
|
context["ca_certificate"],
|
||||||
context["certificate"],
|
context["certificate"],
|
||||||
context["key"],
|
context["key"],
|
||||||
context["wwdr_certificate"],
|
|
||||||
context["password"],
|
context["password"],
|
||||||
)
|
)
|
||||||
|
strings = StringResource(locales=context['locales'])
|
||||||
|
|
||||||
|
pass_json = self.generate_pass_json(layout, context, strings)
|
||||||
|
print(pass_json)
|
||||||
|
pkpass.add_file(
|
||||||
|
"icon.png", open(finders.find("pretix_passbook/icon.png"), "rb").read()
|
||||||
|
)
|
||||||
|
|
||||||
pass_json = self.generate_pass_json()
|
|
||||||
pkpass.add_file("pass.json", json.dumps(pass_json))
|
pkpass.add_file("pass.json", json.dumps(pass_json))
|
||||||
return pkpass.finish()
|
return pkpass.finish()
|
||||||
|
|
||||||
@@ -134,7 +162,7 @@ class AppleWalletEventTicket(AppleWalletStyle):
|
|||||||
ImageFieldGroup(
|
ImageFieldGroup(
|
||||||
identifier="logo",
|
identifier="logo",
|
||||||
name=_("Logo"),
|
name=_("Logo"),
|
||||||
min_entries=1,
|
min_entries=0,
|
||||||
max_entries=1,
|
max_entries=1,
|
||||||
labels=False,
|
labels=False,
|
||||||
default_entries=[
|
default_entries=[
|
||||||
@@ -166,3 +194,63 @@ class AppleWalletEventTicket(AppleWalletStyle):
|
|||||||
TextFieldGroup(identifier="back", name=_("Back")),
|
TextFieldGroup(identifier="back", name=_("Back")),
|
||||||
]
|
]
|
||||||
# preview_image = "apple/event_ticket.svg"
|
# preview_image = "apple/event_ticket.svg"
|
||||||
|
|
||||||
|
def get_pass_fields(self, layout, context):
|
||||||
|
fields = {}
|
||||||
|
for group in self.fieldgroups:
|
||||||
|
if isinstance(group, PredefinedFieldGroup):
|
||||||
|
pass
|
||||||
|
elif isinstance(group, PlaceholderFieldGroup):
|
||||||
|
group_fields = []
|
||||||
|
if group.identifier in layout["fieldgroups"]:
|
||||||
|
for field in layout["fieldgroups"][group.identifier]["entries"]:
|
||||||
|
field_entry = {}
|
||||||
|
if group.labels:
|
||||||
|
field_entry["label"] = LazyI18nString(field["label"])
|
||||||
|
if field["type"] == FieldEntryType.PLACEHOLDER.value:
|
||||||
|
placeholder = (
|
||||||
|
context.get("placeholders")
|
||||||
|
.get(group.content_type.value, {})
|
||||||
|
.get(field["content"])
|
||||||
|
)
|
||||||
|
if placeholder:
|
||||||
|
placeholder_value = placeholder["evaluate"](
|
||||||
|
*context.get("evaluation_context", [])
|
||||||
|
)
|
||||||
|
if placeholder_value:
|
||||||
|
field_entry["value"] = placeholder_value
|
||||||
|
elif field["type"] == FieldEntryType.TEXT.value:
|
||||||
|
placeholder_value = LazyI18nString(field["content"])
|
||||||
|
elif field["type"] == FieldEntryType.IMAGE.value:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Image placeholders not implemented"
|
||||||
|
)
|
||||||
|
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]
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown field group")
|
||||||
|
return fields
|
||||||
|
|
||||||
|
def convert_fields(self, strings, fields):
|
||||||
|
converted = []
|
||||||
|
for i,f in enumerate(fields):
|
||||||
|
converted_field = {**f, "key": f"primary-{i}"}
|
||||||
|
if "label" in converted_field and isinstance(converted_field['label'], LazyI18nString):
|
||||||
|
strings.add_entry(f"primary-{i}-label", converted_field['label'])
|
||||||
|
converted_field['label'] = f"primary-{i}-label"
|
||||||
|
|
||||||
|
converted.append(converted_field)
|
||||||
|
return converted
|
||||||
|
|
||||||
|
def pass_content(self, layout, context, strings):
|
||||||
|
fields = self.get_pass_fields(layout, context)
|
||||||
|
return {
|
||||||
|
"eventTicket": {
|
||||||
|
"primaryFields": self.convert_fields(strings, fields['primary'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -293,5 +293,6 @@ class PassLayout:
|
|||||||
raise ValidationError("Invalid layout: {}".format(str(e)))
|
raise ValidationError("Invalid layout: {}".format(str(e)))
|
||||||
|
|
||||||
def generate(self, context):
|
def generate(self, context):
|
||||||
|
# TODO: how to handle nonexisting placeholders here?
|
||||||
self.validate(context)
|
self.validate(context)
|
||||||
return self.style.generate(self.layout, context)
|
return self.style.generate(self.layout, context)
|
||||||
@@ -26,6 +26,11 @@ 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 .styles import AVAILABLE_STYLES_DICT
|
||||||
|
|
||||||
|
from .models import WalletLayout
|
||||||
|
from .views import get_layout_variables
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("pretix.plugins.wallet")
|
logger = logging.getLogger("pretix.plugins.wallet")
|
||||||
|
|
||||||
@@ -67,5 +72,60 @@ class AppleWalletTicketOutput(WalletOutput):
|
|||||||
verbose_name = _("Apple")
|
verbose_name = _("Apple")
|
||||||
download_button_text = "Add to Apple Wallet"
|
download_button_text = "Add to Apple Wallet"
|
||||||
|
|
||||||
|
def generate(self, op):
|
||||||
|
order = op.order
|
||||||
|
event = order.event
|
||||||
|
filename = "{}-{}.pkpass".format(order.event.slug, order.code)
|
||||||
|
|
||||||
|
# layout = self.override_layout_signal.send_chained(
|
||||||
|
# order.event, 'layout', orderposition=op, layout=self.layout_map.get(
|
||||||
|
# (op.item_id, self.override_channel or order.sales_channel.identifier),
|
||||||
|
# self.layout_map.get(
|
||||||
|
# (op.item_id, 'web'),
|
||||||
|
# self.default_layout
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
layout = WalletLayout.objects.get(pk=1)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
assert layout.platform == "apple"
|
||||||
|
data = AVAILABLE_STYLES_DICT[layout.platform][layout.style].generate(
|
||||||
|
layout.layout, context
|
||||||
|
)
|
||||||
|
return filename, "application/vnd.apple.pkpass", data
|
||||||
|
|
||||||
|
|
||||||
OUTPUTS = [WalletSettingsHolder, GoogleWalletTicketOutput, AppleWalletTicketOutput]
|
OUTPUTS = [WalletSettingsHolder, GoogleWalletTicketOutput, AppleWalletTicketOutput]
|
||||||
|
|||||||
@@ -15,19 +15,20 @@ from .styles import AVAILABLE_STYLES, AVAILABLE_PLATFORMS
|
|||||||
|
|
||||||
def get_layout_variables(event):
|
def get_layout_variables(event):
|
||||||
return {
|
return {
|
||||||
"text": {
|
"text": get_variables(event),
|
||||||
varname: {"label": var["label"], "editor_sample": var["editor_sample"]}
|
"image": get_images(event)
|
||||||
for varname, var in get_variables(event).items()
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
varname: {"label": var["label"]}
|
|
||||||
for varname, var in get_images(event).items()
|
|
||||||
}
|
|
||||||
| {"poweredby": {"label": _("pretix-Logo")}}, # TODO: image upload
|
| {"poweredby": {"label": _("pretix-Logo")}}, # TODO: image upload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_editor_variables(event):
|
def get_editor_variables(event):
|
||||||
return {t: {vid: {"label": v.get("label"), "editor_sample": v.get("editor_sample")} for vid,v in vs.items()} for t,vs in get_layout_variables(event).items()}
|
return {
|
||||||
|
t: {
|
||||||
|
vid: {"label": v.get("label"), "editor_sample": v.get("editor_sample")}
|
||||||
|
for vid, v in vs.items()
|
||||||
|
}
|
||||||
|
for t, vs in get_layout_variables(event).items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# TODO: should this even be a list view?
|
# TODO: should this even be a list view?
|
||||||
@@ -65,7 +66,10 @@ class LayoutEditorView(DetailView):
|
|||||||
style.identifier: style.asdict() for style in self.get_platform_styles()
|
style.identifier: style.asdict() for style in self.get_platform_styles()
|
||||||
}
|
}
|
||||||
context["variables"] = get_editor_variables(self.request.event)
|
context["variables"] = get_editor_variables(self.request.event)
|
||||||
context['locales'] = {l: dict(settings.LANGUAGES).get(l, l) for l in self.request.event.settings.get('locales')}
|
context["locales"] = {
|
||||||
|
l: dict(settings.LANGUAGES).get(l, l)
|
||||||
|
for l in self.request.event.settings.get("locales")
|
||||||
|
}
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|||||||
186
src/tests/plugins/wallet/test_apple.py
Normal file
186
src/tests/plugins/wallet/test_apple.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
from pretix.plugins.wallet.styles.apple import SignedZipFile, StringResource, AppleWalletEventTicket
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
import pytest
|
||||||
|
from i18nfield.strings import LazyI18nString
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
from cryptography.hazmat.primitives import serialization, hashes
|
||||||
|
from cryptography import x509
|
||||||
|
import datetime
|
||||||
|
import io
|
||||||
|
import zipfile
|
||||||
|
import json
|
||||||
|
import jsonschema
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def pkpass_context():
|
||||||
|
key_pw = b"TESTPW"
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
|
||||||
|
ca_cert = (
|
||||||
|
x509.CertificateBuilder()
|
||||||
|
.subject_name(
|
||||||
|
x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "TTDR")])
|
||||||
|
)
|
||||||
|
.issuer_name(
|
||||||
|
x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "ROOT Inc.")])
|
||||||
|
)
|
||||||
|
.public_key(ca_key.public_key())
|
||||||
|
.serial_number(1)
|
||||||
|
.not_valid_before(now)
|
||||||
|
.not_valid_after(now + datetime.timedelta(days=365))
|
||||||
|
.sign(ca_key, hashes.SHA256())
|
||||||
|
)
|
||||||
|
|
||||||
|
key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
|
||||||
|
cert = (
|
||||||
|
x509.CertificateBuilder()
|
||||||
|
.subject_name(
|
||||||
|
x509.Name(
|
||||||
|
[x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "UID=pass.test.test")]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.issuer_name(
|
||||||
|
x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "TTDR")])
|
||||||
|
)
|
||||||
|
.public_key(key.public_key())
|
||||||
|
.serial_number(2)
|
||||||
|
.not_valid_before(now)
|
||||||
|
.not_valid_after(now + datetime.timedelta(days=365))
|
||||||
|
.sign(ca_key, hashes.SHA256())
|
||||||
|
)
|
||||||
|
|
||||||
|
ca_cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM)
|
||||||
|
cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM)
|
||||||
|
key_pem = key.private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
|
encryption_algorithm=serialization.BestAvailableEncryption(key_pw),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"ca_certificate": ca_cert_pem,
|
||||||
|
"certificate": cert_pem,
|
||||||
|
"key": key_pem,
|
||||||
|
"password": key_pw,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_signed_zip(pkpass_context):
|
||||||
|
pkpass = SignedZipFile(**pkpass_context)
|
||||||
|
generated_pass = pkpass.finish()
|
||||||
|
|
||||||
|
with zipfile.ZipFile(io.BytesIO(generated_pass), "r") as zip_file:
|
||||||
|
assert set(zip_file.namelist()) == {"manifest.json", "signature"}
|
||||||
|
with zip_file.open("manifest.json") as f:
|
||||||
|
manifest = json.load(f)
|
||||||
|
assert manifest == {}
|
||||||
|
|
||||||
|
with zip_file.open("signature") as f:
|
||||||
|
signature = f.read()
|
||||||
|
|
||||||
|
assert signature
|
||||||
|
|
||||||
|
pkpass = SignedZipFile(**pkpass_context)
|
||||||
|
pkpass.add_file("test", b"test content")
|
||||||
|
generated_pass = pkpass.finish()
|
||||||
|
|
||||||
|
with zipfile.ZipFile(io.BytesIO(generated_pass), "r") as zip_file:
|
||||||
|
assert set(zip_file.namelist()) == {"test", "manifest.json", "signature"}
|
||||||
|
with zip_file.open("manifest.json") as f:
|
||||||
|
manifest = json.load(f)
|
||||||
|
assert manifest == {"test": "1eebdf4fdc9fc7bf283031b93f9aef3338de9052"}
|
||||||
|
|
||||||
|
with zip_file.open("signature") as f:
|
||||||
|
signature = f.read()
|
||||||
|
|
||||||
|
assert signature
|
||||||
|
|
||||||
|
pkpass = SignedZipFile(**pkpass_context)
|
||||||
|
pkpass.add_file("test/test", "test content")
|
||||||
|
generated_pass = pkpass.finish()
|
||||||
|
|
||||||
|
with zipfile.ZipFile(io.BytesIO(generated_pass), "r") as zip_file:
|
||||||
|
assert set(zip_file.namelist()) == {"test/test", "manifest.json", "signature"}
|
||||||
|
with zip_file.open("manifest.json") as f:
|
||||||
|
manifest = json.load(f)
|
||||||
|
assert manifest == {"test/test": "1eebdf4fdc9fc7bf283031b93f9aef3338de9052"}
|
||||||
|
|
||||||
|
with zip_file.open("signature") as f:
|
||||||
|
signature = f.read()
|
||||||
|
|
||||||
|
assert signature
|
||||||
|
|
||||||
|
|
||||||
|
def test_stringresource_minimal():
|
||||||
|
resource = StringResource(locales=["de", "en"])
|
||||||
|
resource.add_entry("TEST", LazyI18nString({"de": "test-de", "en": "test-en"}))
|
||||||
|
stringfiles = resource.generate()
|
||||||
|
|
||||||
|
assert stringfiles.keys() == {"de", "en"}
|
||||||
|
assert stringfiles["de"] == '"TEST" = "test-de";'
|
||||||
|
assert stringfiles["en"] == '"TEST" = "test-en";'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"input,output",
|
||||||
|
[
|
||||||
|
['te"st', 'te\\"st'],
|
||||||
|
["te\rst", "te\\rst"],
|
||||||
|
["te\nst", "te\\nst"],
|
||||||
|
["te\r\nst", "te\\r\\nst"],
|
||||||
|
["te\r\nst", "te\\r\\nst"],
|
||||||
|
["te\\st", "te\\\\st"],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_stringresource_escaping(input, output):
|
||||||
|
resource = StringResource(locales=["en"])
|
||||||
|
resource.add_entry("TEST", LazyI18nString({"en": input}))
|
||||||
|
stringfiles = resource.generate()
|
||||||
|
|
||||||
|
assert stringfiles.keys() == {"en"}
|
||||||
|
assert stringfiles["en"] == f'"TEST" = "{output}";'
|
||||||
|
|
||||||
|
resource = StringResource(locales=["en"])
|
||||||
|
resource.add_entry(input, LazyI18nString({"en": "test"}))
|
||||||
|
stringfiles = resource.generate()
|
||||||
|
|
||||||
|
assert stringfiles.keys() == {"en"}
|
||||||
|
assert stringfiles["en"] == f'"{output}" = "test";'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_stringresource_additional_locale():
|
||||||
|
resource = StringResource(locales=["de", "en", "fr"])
|
||||||
|
resource.add_entry("TEST", LazyI18nString({"de": "test-de", "en": "test-en"}))
|
||||||
|
stringfiles = resource.generate()
|
||||||
|
|
||||||
|
assert stringfiles.keys() == {"de", "en", "fr"}
|
||||||
|
assert stringfiles["de"] == '"TEST" = "test-de";'
|
||||||
|
assert stringfiles["en"] == '"TEST" = "test-en";'
|
||||||
|
assert stringfiles["fr"] == '"TEST" = "test-en";'
|
||||||
|
|
||||||
|
def test_generate_pass_json():
|
||||||
|
context = {
|
||||||
|
"placeholders": {
|
||||||
|
"text": {"test_placeholder": {"evaluate": lambda: "test placeholder"}}
|
||||||
|
},
|
||||||
|
"description": "Ticket for Test",
|
||||||
|
"organizationName": "TestOrg",
|
||||||
|
"serialNumber": "1",
|
||||||
|
"passTypeIdentifier": "pass.test.test",
|
||||||
|
"teamIdentifier": "ABCDEF123456"
|
||||||
|
}
|
||||||
|
layout = {"fieldgroups": {"primary": {"entries": [{"type": "placeholder", "label": "test", "content": "test_placeholder"}, {"type": "text", "label": {"de":"test-de", "en": "test-en"}, "content": "test content"}]}}}
|
||||||
|
style = AppleWalletEventTicket()
|
||||||
|
schema = style.layout_schema(context)
|
||||||
|
jsonschema.validate(schema, layout)
|
||||||
|
|
||||||
|
result = style.generate_pass_json(layout, context)
|
||||||
|
|
||||||
|
required_fields = ["description", "formatVersion", "organizationName", "passTypeIdentifier", "serialNumber", "teamIdentifier"]
|
||||||
|
for field in required_fields:
|
||||||
|
assert field in result
|
||||||
|
|
||||||
|
assert result['formatVersion'] == 1
|
||||||
|
|
||||||
|
breakpoint()
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from pretix.plugins.wallet.styles.base import (
|
from pretix.plugins.wallet.styles.base import (
|
||||||
PassStyle,
|
PassStyle,
|
||||||
|
PredefinedFieldGroup,
|
||||||
WalletPlatform,
|
WalletPlatform,
|
||||||
PlaceholderFieldGroup,
|
PlaceholderFieldGroup,
|
||||||
FieldContentType,
|
FieldContentType,
|
||||||
@@ -7,7 +8,6 @@ from pretix.plugins.wallet.styles.base import (
|
|||||||
FieldGroupType,
|
FieldGroupType,
|
||||||
FieldEntryType,
|
FieldEntryType,
|
||||||
)
|
)
|
||||||
from pretix.plugins.wallet.styles.apple import SignedZipFile, StringResource
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
import jsonschema
|
import jsonschema
|
||||||
import pytest
|
import pytest
|
||||||
@@ -65,9 +65,9 @@ class TicketTestStyle(PassStyle):
|
|||||||
for group in self.fieldgroups:
|
for group in self.fieldgroups:
|
||||||
if group.identifier in layout["fieldgroups"]:
|
if group.identifier in layout["fieldgroups"]:
|
||||||
output += f"Group: {group.name}\n"
|
output += f"Group: {group.name}\n"
|
||||||
if group.type == FieldGroupType.PREDEFINED:
|
if isinstance(group, PredefinedFieldGroup):
|
||||||
output += "PREDEFINED\n"
|
output += "PREDEFINED\n"
|
||||||
else:
|
elif isinstance(group, PlaceholderFieldGroup):
|
||||||
for field in layout["fieldgroups"][group.identifier]["entries"]:
|
for field in layout["fieldgroups"][group.identifier]["entries"]:
|
||||||
if group.labels:
|
if group.labels:
|
||||||
label = LazyI18nString(field["label"])
|
label = LazyI18nString(field["label"])
|
||||||
@@ -89,6 +89,8 @@ class TicketTestStyle(PassStyle):
|
|||||||
elif field["type"] == FieldEntryType.IMAGE.value:
|
elif field["type"] == FieldEntryType.IMAGE.value:
|
||||||
output += f"<IMG>{field['content']}</IMG>"
|
output += f"<IMG>{field['content']}</IMG>"
|
||||||
output += "\n"
|
output += "\n"
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown field group")
|
||||||
output += "\n"
|
output += "\n"
|
||||||
return output
|
return output
|
||||||
|
|
||||||
@@ -332,151 +334,3 @@ def test_layout_generate(layout_context):
|
|||||||
== "Generated Pass: Test Wallet Style Ticket\n\nGroup: Text 1\ntest-en: test placeholder\ntest: test content\n\n"
|
== "Generated Pass: Test Wallet Style Ticket\n\nGroup: Text 1\ntest-en: test placeholder\ntest: test content\n\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def pkpass_context():
|
|
||||||
key_pw = b"TESTPW"
|
|
||||||
now = datetime.datetime.now()
|
|
||||||
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
|
|
||||||
ca_cert = (
|
|
||||||
x509.CertificateBuilder()
|
|
||||||
.subject_name(
|
|
||||||
x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "TTDR")])
|
|
||||||
)
|
|
||||||
.issuer_name(
|
|
||||||
x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "ROOT Inc.")])
|
|
||||||
)
|
|
||||||
.public_key(ca_key.public_key())
|
|
||||||
.serial_number(1)
|
|
||||||
.not_valid_before(now)
|
|
||||||
.not_valid_after(now + datetime.timedelta(days=365))
|
|
||||||
.sign(ca_key, hashes.SHA256())
|
|
||||||
)
|
|
||||||
|
|
||||||
key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
|
|
||||||
cert = (
|
|
||||||
x509.CertificateBuilder()
|
|
||||||
.subject_name(
|
|
||||||
x509.Name(
|
|
||||||
[x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "UID=pass.test.test")]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.issuer_name(
|
|
||||||
x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "TTDR")])
|
|
||||||
)
|
|
||||||
.public_key(key.public_key())
|
|
||||||
.serial_number(2)
|
|
||||||
.not_valid_before(now)
|
|
||||||
.not_valid_after(now + datetime.timedelta(days=365))
|
|
||||||
.sign(ca_key, hashes.SHA256())
|
|
||||||
)
|
|
||||||
|
|
||||||
ca_cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM)
|
|
||||||
cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM)
|
|
||||||
key_pem = key.private_bytes(
|
|
||||||
encoding=serialization.Encoding.PEM,
|
|
||||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
||||||
encryption_algorithm=serialization.BestAvailableEncryption(key_pw),
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"ca_certificate": ca_cert_pem,
|
|
||||||
"certificate": cert_pem,
|
|
||||||
"key": key_pem,
|
|
||||||
"password": key_pw,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_signed_zip(pkpass_context):
|
|
||||||
pkpass = SignedZipFile(**pkpass_context)
|
|
||||||
generated_pass = pkpass.finish()
|
|
||||||
|
|
||||||
with zipfile.ZipFile(io.BytesIO(generated_pass), "r") as zip_file:
|
|
||||||
assert set(zip_file.namelist()) == {"manifest.json", "signature"}
|
|
||||||
with zip_file.open("manifest.json") as f:
|
|
||||||
manifest = json.load(f)
|
|
||||||
assert manifest == {}
|
|
||||||
|
|
||||||
with zip_file.open("signature") as f:
|
|
||||||
signature = f.read()
|
|
||||||
|
|
||||||
assert signature
|
|
||||||
|
|
||||||
pkpass = SignedZipFile(**pkpass_context)
|
|
||||||
pkpass.add_file("test", b"test content")
|
|
||||||
generated_pass = pkpass.finish()
|
|
||||||
|
|
||||||
with zipfile.ZipFile(io.BytesIO(generated_pass), "r") as zip_file:
|
|
||||||
assert set(zip_file.namelist()) == {"test", "manifest.json", "signature"}
|
|
||||||
with zip_file.open("manifest.json") as f:
|
|
||||||
manifest = json.load(f)
|
|
||||||
assert manifest == {"test": "1eebdf4fdc9fc7bf283031b93f9aef3338de9052"}
|
|
||||||
|
|
||||||
with zip_file.open("signature") as f:
|
|
||||||
signature = f.read()
|
|
||||||
|
|
||||||
assert signature
|
|
||||||
|
|
||||||
pkpass = SignedZipFile(**pkpass_context)
|
|
||||||
pkpass.add_file("test/test", "test content")
|
|
||||||
generated_pass = pkpass.finish()
|
|
||||||
|
|
||||||
with zipfile.ZipFile(io.BytesIO(generated_pass), "r") as zip_file:
|
|
||||||
assert set(zip_file.namelist()) == {"test/test", "manifest.json", "signature"}
|
|
||||||
with zip_file.open("manifest.json") as f:
|
|
||||||
manifest = json.load(f)
|
|
||||||
assert manifest == {"test/test": "1eebdf4fdc9fc7bf283031b93f9aef3338de9052"}
|
|
||||||
|
|
||||||
with zip_file.open("signature") as f:
|
|
||||||
signature = f.read()
|
|
||||||
|
|
||||||
assert signature
|
|
||||||
|
|
||||||
|
|
||||||
def test_stringresource_minimal():
|
|
||||||
resource = StringResource(locales=["de", "en"])
|
|
||||||
resource.add_entry("TEST", LazyI18nString({"de": "test-de", "en": "test-en"}))
|
|
||||||
stringfiles = resource.generate()
|
|
||||||
|
|
||||||
assert stringfiles.keys() == {"de", "en"}
|
|
||||||
assert stringfiles["de"] == '"TEST" = "test-de";'
|
|
||||||
assert stringfiles["en"] == '"TEST" = "test-en";'
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"input,output",
|
|
||||||
[
|
|
||||||
['te"st', 'te\\"st'],
|
|
||||||
["te\rst", "te\\rst"],
|
|
||||||
["te\nst", "te\\nst"],
|
|
||||||
["te\r\nst", "te\\r\\nst"],
|
|
||||||
["te\r\nst", "te\\r\\nst"],
|
|
||||||
["te\\st", "te\\\\st"],
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_stringresource_escaping(input, output):
|
|
||||||
resource = StringResource(locales=["en"])
|
|
||||||
resource.add_entry("TEST", LazyI18nString({"en": input}))
|
|
||||||
stringfiles = resource.generate()
|
|
||||||
|
|
||||||
assert stringfiles.keys() == {"en"}
|
|
||||||
assert stringfiles["en"] == f'"TEST" = "{output}";'
|
|
||||||
|
|
||||||
resource = StringResource(locales=["en"])
|
|
||||||
resource.add_entry(input, LazyI18nString({"en": "test"}))
|
|
||||||
stringfiles = resource.generate()
|
|
||||||
|
|
||||||
assert stringfiles.keys() == {"en"}
|
|
||||||
assert stringfiles["en"] == f'"{output}" = "test";'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_stringresource_additional_locale():
|
|
||||||
resource = StringResource(locales=["de", "en", "fr"])
|
|
||||||
resource.add_entry("TEST", LazyI18nString({"de": "test-de", "en": "test-en"}))
|
|
||||||
stringfiles = resource.generate()
|
|
||||||
|
|
||||||
assert stringfiles.keys() == {"de", "en", "fr"}
|
|
||||||
assert stringfiles["de"] == '"TEST" = "test-de";'
|
|
||||||
assert stringfiles["en"] == '"TEST" = "test-en";'
|
|
||||||
assert stringfiles["fr"] == '"TEST" = "test-en";'
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user