WIP: i18n editor, start apple wallet generation

This commit is contained in:
Kara Engelhardt
2026-04-15 18:59:32 +02:00
parent c48d30919f
commit 9064069cf3
10 changed files with 689 additions and 62 deletions

View File

@@ -49,7 +49,7 @@ class WalletLayoutSerializer(I18nAwareModelSerializer):
style = platform_styles[data["style"]]
layout = PassLayout(style=style, layout=data["layout"])
context = {"placeholders": {k: list(v.keys()) for k,v in get_layout_variables(self.context['event']).items()}}
context = {"placeholders": {k: {"content": v['content']} for k,v in get_layout_variables(self.context['event']).items()}}
layout.validate(context=context)
return data

View File

@@ -1,15 +1,15 @@
<script setup lang="ts">
import { useId, watchEffect } from 'vue'
import { watchEffect } from 'vue'
defineOptions({
inheritAttrs: false
})
const props = defineProps<{
label?: string,
errors?: string[],
locales: Record<string, string>
}>()
}>();
const modelValue = defineModel<Record<string, string> | string>();
watchEffect(() => {
if (typeof modelValue.value === "string") {
@@ -17,12 +17,9 @@ watchEffect(() => {
modelValue.value = Object.fromEntries(Object.keys(props.locales).map((x): [string, string] => [x, oldVal]))
}
})
const id = useId()
</script>
<template lang="pug">
label.control-label(:for="id", v-if="props.label") {{ props.label }}
.i18n-form-group
input.form-control(v-for="(human_readable, locale) in locales" v-model="modelValue[locale]" v-bind="$attrs" :lang="locale" :title="human_readable" :placeholder="human_readable")
input.form-control(v-for="(human_readable, locale) in locales" v-model="modelValue[locale]" v-bind="$attrs" :lang="locale" :title="human_readable" :placeholder="human_readable")
.help-block(v-if="props.errors" v-for="error in props.errors") {{ error }}
</template>

View File

@@ -6,7 +6,7 @@ defineOptions({
})
const props = defineProps<{
label: string
label?: string
choices: Array<[string, string]>
errors?: string[]
}>()
@@ -24,7 +24,7 @@ watchEffect(() => {
<template lang="pug">
template(v-if="choices.length >= 1")
label.control-label(:for="id") {{ props.label }}
label.control-label(v-if="props.label" :for="id") {{ props.label }}
select.form-control(:id="id" v-model="modelValue" v-bind="$attrs" required)
option(v-for="choice in props.choices" :key="choice[0]" :value="choice[0]") {{ choice[1] }}
.help-block(v-if="props.errors" v-for="error in props.errors") {{ error }}

View File

@@ -8,7 +8,7 @@ import TextContent from "./text-content.vue";
const gettext = (window as any).gettext;
const props = defineProps<{
fieldgroup: FieldGroupDefinition;
fieldgroup: PlaceholderFieldGroupDefinition;
overflows: FieldGroupDefinition[];
variables: Variables;
locales: Record<string, string>;
@@ -48,26 +48,33 @@ watchEffect(() => {
.form-group()
span.text-muted(v-if="fieldgroup.description") {{ fieldgroup.description }}
h4 {{ gettext("Content") }}
.row.form-group(v-for="n in fieldConfig.entries.length")
.col-md-5(v-if="fieldgroup.labels")
I18nInput(:label="gettext('Label')" v-model="fieldConfig.entries[n-1].label" :locales="locales")
div(:class="'col-md-' + (fieldgroup.labels ? '6' : '11')")
TextContent(v-if='fieldgroup.content_type == "text"'
v-model="fieldConfig.entries[n-1]"
:variables="props.variables")
Select(:label="gettext('Content')"
v-else-if='fieldgroup.content_type == "image"'
v-model="fieldConfig.entries[n-1].content"
:choices="Object.entries(props.variables).map(([k,v]) => [k, v.label])"
)
.col-md-1
label.control-label &nbsp;
span.sr-only {{ gettext('Delete')}}
button.btn.btn-danger(type="button" @click="fieldConfig.entries.splice(n-1, 1)")
i.fa.fa-trash
span.sr-only {{ gettext('Delete')}}
table.table.table-hover
thead
tr
th.col-md-5(v-if="fieldgroup.labels") {{ gettext('Label') }}
th(:class="'col-md-' + (fieldgroup.labels ? '6' : '11')") {{ gettext('Content') }}
th.col-xs-1
tbody
tr(v-for="n,i in fieldConfig.entries.length" :key="i")
td(v-if="fieldgroup.labels")
.i18n-form-group
I18nInput(v-model="fieldConfig.entries[n-1].label" :locales="locales")
td
TextContent(v-if='fieldgroup.content_type == "text"'
v-model="fieldConfig.entries[n-1]"
:variables="props.variables"
:locales="locales")
Select(v-else-if='fieldgroup.content_type == "image"'
v-model="fieldConfig.entries[n-1].content"
:choices="Object.entries(props.variables).map(([k,v]) => [k, v.label])"
)
td.text-right
button.btn.btn-danger.form-control-static(type="button" @click="fieldConfig.entries.splice(n-1, 1)")
i.fa.fa-trash
span.sr-only {{ gettext('Delete')}}
button.btn.btn-default(type="button" @click="addVariable")
i.fa.fa-plus
span.sr-only {{ gettext("Add field") }}
| {{ gettext("Add field") }}
Select(:label="gettext('Overflow to …')" :choices="overflowOptions" v-model="fieldConfig.overflow")
</template>
</template>

View File

@@ -1,12 +1,13 @@
<script setup lang="ts">
import { computed, reactive } from 'vue'
import Select from './input/select.vue'
import Input from './input/input.vue'
import I18nInput from './input/i18ninput.vue'
const gettext = (window as any).gettext
const props = defineProps<{
variables: Variables
locales: Record<string, string>;
}>()
const entry = defineModel<FieldEntry>({ required: true })
@@ -29,7 +30,7 @@ const selection = computed({
set(newValue) {
if (newValue == "other") {
entry.value.type = "text"
entry.value.content = "";
entry.value.content = {};
} else {
entry.value.type = "placeholder"
entry.value.content = newValue
@@ -55,9 +56,10 @@ const textContent = computed({
</script>
<template lang="pug">
Select(:label="gettext('Content')"
v-model="selection"
:choices="selectChoices"
)
Input(v-model="textContent" v-if="selection === 'other'")
.i18n-form-group
Select(
v-model="selection"
:choices="selectChoices"
)
I18nInput(v-model="textContent" v-if="selection === 'other'" :locales="locales")
</template>

View File

@@ -5,7 +5,7 @@ type BaseFieldGroupDefinition = {
required: boolean;
}
type FieldGroupDefinition = PlaceholderFieldGroupDefinition | PredefinedFieldGroup;
type FieldGroupDefinition = PlaceholderFieldGroupDefinition | PredefinedFieldGroupDefinition;
type PlaceholderFieldGroupDefinition = BaseFieldGroupDefinition & {
type: 'placeholder';
@@ -24,13 +24,19 @@ type I18nString = string | Record<string, string>
type FieldContentType = 'text' | 'image';
type FieldEntry = {
type: 'placeholder' | FieldContentType;
type PlaceholderFieldEntry = {
type: 'placeholder';
label?: I18nString;
content?: string;
}
type ContentFieldEntry = {
type: FieldContentType;
label?: I18nString;
content?: I18nString;
}
type FieldEntry = PlaceholderFieldEntry | ContentFieldEntry;
type Style = {
identifier: string;

View File

@@ -1,25 +1,132 @@
from .base import (
FieldEntry,
FieldEntryContentType,
FieldEntryType,
FieldContentType,
ImageFieldGroup,
PlaceholderFieldGroup,
TextFieldGroup,
WalletPlatform,
PassStyle,
PlaceholderFieldEntry,
CustomFieldEntry,
)
from django.utils.translation import gettext as _
from i18nfield.strings import LazyI18nString
import io
import hashlib
import zipfile
import cryptography
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
class ApplePlatform(WalletPlatform):
identifier = "apple"
name = _("Apple")
class StringResource:
# mapping string in default event locale -> LazyI18nString
entries: dict[str, LazyI18nString]
locales: set[str]
def __init__(self, locales):
self.entries = {}
self.locales = set(locales)
def add_entry(self, key: str, value: LazyI18nString): # TODO: replace LazyI18nString with dict or handle strings where data == ""
if key in self.entries:
raise ValueError(f"{key} already exists in this StringResource")
self.entries[key] = value
if isinstance(value.data, dict):
self.locales |= value.data.keys()
def escape(self, string):
return string.translate(str.maketrans({"\"": "\\\"", "\r": "\\r", "\n": "\\n", "\\": "\\\\"}))
def generate_resource(self, language):
output = ""
for key, entry in self.entries.items():
output += f'"{self.escape(key)}" = "{self.escape(entry.localize(language))}";\n'
return output.strip()
def generate(self):
return {language: self.generate_resource(language) for language in self.locales}
class SignedZipFile:
""" Generates a zip-file with manifest and signature as apple expects a pkpass file to be """
def __init__(self, ca_certificate, certificate, key, password):
self.ca_certificate = cryptography.x509.load_pem_x509_certificate(ca_certificate)
self.certificate = cryptography.x509.load_pem_x509_certificate(certificate)
self.key = cryptography.hazmat.primitives.serialization.load_pem_private_key(key, password)
self.password = password
self.file = io.BytesIO()
self.zip_file = zipfile.ZipFile(self.file, "w")
self.manifest = {}
def sign(self, data: bytes):
return (
cryptography.hazmat.primitives.serialization.pkcs7.PKCS7SignatureBuilder()
.set_data(data)
.add_signer(
self.certificate,
self.key,
cryptography.hazmat.primitives.hashes.SHA256(),
)
.add_certificate(self.ca_certificate)
.sign(
cryptography.hazmat.primitives.serialization.Encoding.DER,
[
cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options.Binary,
cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options.DetachedSignature,
],
)
)
def finish(self):
manifest = json.dumps(self.manifest).encode()
signature = self.sign(manifest)
self.add_file("manifest.json", manifest)
self.add_file("signature", signature)
self.zip_file.close()
return self.file.getvalue()
def add_file(self, filename: str, content: str | bytes):
if isinstance(content, str):
content = content.encode()
with self.zip_file.open(filename, "w") as f:
f.write(content)
self.manifest[filename] = hashlib.sha1(content).hexdigest()
class AppleWalletStyle(PassStyle):
platform = ApplePlatform
def generate_pass_json(self, layout, context):
pass_json = {}
return pass_json
def generate(self, layout, context):
for key in ["certificate", "key", "wwdr_certificate", "password"]:
if key not in context:
raise ValueError(f"{key} missing from context")
pkpass = SignedZipFile(
context["certificate"],
context["key"],
context["wwdr_certificate"],
context["password"],
)
pass_json = self.generate_pass_json()
pkpass.add_file("pass.json", json.dumps(pass_json))
return pkpass.finish()
class AppleWalletEventTicket(AppleWalletStyle):
identifier = "event_1"
name = _("Event Ticket Layout 1")
@@ -31,10 +138,8 @@ class AppleWalletEventTicket(AppleWalletStyle):
max_entries=1,
labels=False,
default_entries=[
FieldEntry(
type=FieldEntryContentType.IMAGE,
label=LazyI18nString("logo"),
content="event:image",
PlaceholderFieldEntry(
content="poweredby",
)
],
),
@@ -44,13 +149,12 @@ class AppleWalletEventTicket(AppleWalletStyle):
min_entries=1,
max_entries=1,
default_entries=[
FieldEntry(
type=FieldEntryContentType.PLACEHOLDER,
PlaceholderFieldEntry(
label=LazyI18nString({"de": "Tickettyp", "en": "Ticket type"}),
content="item",
)
], # TODO: support Lazyi18nproxy here
description=_("These fields appear prominently featured on the pass.")
description=_("These fields appear prominently featured on the pass."),
),
TextFieldGroup(
identifier="secondary", name=_("Secondary"), max_entries=4
@@ -62,5 +166,3 @@ class AppleWalletEventTicket(AppleWalletStyle):
TextFieldGroup(identifier="back", name=_("Back")),
]
# preview_image = "apple/event_ticket.svg"

View File

@@ -1,6 +1,7 @@
import enum
from i18nfield.strings import LazyI18nString
import jsonschema
from django.core.exceptions import ValidationError
class WalletPlatform:
identifier: str
@@ -47,19 +48,19 @@ class FieldContentType(enum.Enum):
TEXT = "text"
class FieldEntryContentType(enum.Enum):
class FieldEntryType(enum.Enum):
IMAGE = "image"
TEXT = "text"
PLACEHOLDER = "placeholder"
class FieldEntry:
type: FieldEntryContentType
class FieldEntry[T]:
type: FieldEntryType
label: LazyI18nString | None
content: str
content: T
def __init__(
self, type: FieldEntryContentType, label: LazyI18nString | None, content: str
self, type: FieldEntryType, content: T, label: LazyI18nString | None = None
):
self.type = type
self.label = label
@@ -68,6 +69,27 @@ class FieldEntry:
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
@@ -126,13 +148,13 @@ class PlaceholderFieldGroup(FieldGroup):
remaining_fields: list["FieldGroup"],
context: dict,
):
placeholders = context.get("placeholders", {}).get(self.content_type.value, [])
placeholders = list(context.get("placeholders", {}).get(self.content_type.value, {}).keys())
return {
"type": "object",
"properties": {
"entries": self.entries_schema(placeholders=placeholders),
"overflow": {
"oneOf": [
"anyOf": [
{"type": "null"},
{
"type": "string",
@@ -158,7 +180,7 @@ class PlaceholderFieldGroup(FieldGroup):
"type": "array",
"items": {
"type": "object",
"oneOf": [
"anyOf": [
{
"properties": {
**baseprops,
@@ -170,7 +192,7 @@ class PlaceholderFieldGroup(FieldGroup):
"properties": {
**baseprops,
"type": {"const": self.content_type.value},
"content": {"type": "string"},
"content": {"$ref": "#/$defs/I18nString"},
}
},
],
@@ -252,6 +274,8 @@ class PassStyle:
return schema
def generate(self, layout, context):
raise NotImplementedError()
class PassLayout:
style: PassStyle
@@ -267,3 +291,7 @@ class PassLayout:
jsonschema.validate(self.layout, schema)
except jsonschema.ValidationError as e:
raise ValidationError("Invalid layout: {}".format(str(e)))
def generate(self, context):
self.validate(context)
return self.style.generate(self.layout, context)

View File

@@ -26,6 +26,9 @@ def get_layout_variables(event):
| {"poweredby": {"label": _("pretix-Logo")}}, # TODO: image upload
}
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()}
# TODO: should this even be a list view?
class LayoutListView(EventPermissionRequiredMixin, ListView):
@@ -61,7 +64,7 @@ class LayoutEditorView(DetailView):
context["styles"] = {
style.identifier: style.asdict() for style in self.get_platform_styles()
}
context["variables"] = get_layout_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')}
return context

View File

@@ -0,0 +1,482 @@
from pretix.plugins.wallet.styles.base import (
PassStyle,
WalletPlatform,
PlaceholderFieldGroup,
FieldContentType,
PassLayout,
FieldGroupType,
FieldEntryType,
)
from pretix.plugins.wallet.styles.apple import SignedZipFile, StringResource
from django.utils.translation import gettext as _
import jsonschema
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
class WalletTestPlatform(WalletPlatform):
identifier = "test_platform"
name = _("Test Wallet Platform")
class MinimalTestStyle(PassStyle):
platform = WalletTestPlatform
identifier = "test_style"
name = _("Test Wallet Style")
fieldgroups = []
class TicketTestStyle(PassStyle):
platform = WalletTestPlatform
identifier = "test_ticket"
name = _("Test Wallet Style Ticket")
fieldgroups = [
PlaceholderFieldGroup(
identifier="text1",
name=_("Text 1"),
content_type=FieldContentType.TEXT,
required=True,
),
PlaceholderFieldGroup(
identifier="text2",
name=_("Text 2"),
content_type=FieldContentType.TEXT,
required=False,
labels=False,
),
PlaceholderFieldGroup(
identifier="image1",
name=_("Image 1"),
content_type=FieldContentType.IMAGE,
required=False,
labels=False,
),
]
def generate(self, layout, context):
output = f"Generated Pass: {self.name}\n\n"
for group in self.fieldgroups:
if group.identifier in layout["fieldgroups"]:
output += f"Group: {group.name}\n"
if group.type == FieldGroupType.PREDEFINED:
output += "PREDEFINED\n"
else:
for field in layout["fieldgroups"][group.identifier]["entries"]:
if group.labels:
label = LazyI18nString(field["label"])
output += f"{label}: "
if field["type"] == FieldEntryType.PLACEHOLDER.value:
placeholder = (
context.get("placeholders")
.get(group.content_type.value, {})
.get(field["content"])
)
if placeholder:
output += placeholder["evaluate"](
*context.get("evaluation_context", [])
)
else:
output += f"UNKNOWN: {field['content']}"
elif field["type"] == FieldEntryType.TEXT.value:
output += str(LazyI18nString(field["content"]))
elif field["type"] == FieldEntryType.IMAGE.value:
output += f"<IMG>{field['content']}</IMG>"
output += "\n"
output += "\n"
return output
@pytest.fixture
def layout_context():
return {
"placeholders": {
"text": {"test_placeholder": {"evaluate": lambda: "test placeholder"}}
}
}
def test_schema_generation_minimal():
style = MinimalTestStyle()
context = {}
schema = style.layout_schema(context)
assert isinstance(schema, dict)
assert "properties" in schema
assert "fieldgroups" in schema["properties"]
jsonschema.validate({}, schema)
jsonschema.validate({"fieldgroups": {}}, schema)
def test_schema_ticket_generation(layout_context):
style = TicketTestStyle()
schema = style.layout_schema(layout_context)
assert isinstance(schema, dict)
assert "properties" in schema
assert "fieldgroups" in schema["properties"]
@pytest.mark.parametrize(
"layout",
[
{
"fieldgroups": {
"text1": {
"entries": [
{
"type": "placeholder",
"label": "test",
"content": "test_placeholder",
}
]
}
}
},
{
"fieldgroups": {
"text1": {
"entries": [
{
"type": "placeholder",
"label": {"de": "test-de", "en": "test-en"},
"content": "test_placeholder",
}
]
}
}
},
{
"fieldgroups": {
"text1": {
"entries": [
{"type": "text", "label": "test", "content": "test content"}
]
}
}
},
{
"fieldgroups": {
"text1": {
"entries": [
{
"type": "placeholder",
"label": {"de": "test-de", "en": "test-en"},
"content": "test_placeholder",
},
{"type": "text", "label": "test", "content": "test content"},
]
}
}
},
{
"fieldgroups": {
"text1": {
"entries": [
{
"type": "placeholder",
"label": {"de": "test-de", "en": "test-en"},
"content": "test_placeholder",
},
{"type": "text", "label": "test", "content": "test content"},
],
"overflow": "text2",
}
}
},
],
)
def test_schema_ticket_valid(layout_context, layout):
style = TicketTestStyle()
schema = style.layout_schema(layout_context)
jsonschema.validate(layout, schema)
@pytest.mark.parametrize(
"layout",
[
{},
{"fieldgroups": {}},
{"fieldgroups": {"text1": {}}},
{"fieldgroups": {"text1": {"entries": []}}},
{"fieldgroups": {"text1": {"overflow": "test"}}},
{
"fieldgroups": {
"text1": {
"entries": [{"type": "placeholder", "content": "test_placeholder"}]
}
}
},
{
"fieldgroups": {
"text1": {
"entries": [
{
"type": "placeholder",
"label": [],
"content": "test_placeholder",
}
]
}
}
},
{
"fieldgroups": {
"text1": {"entries": [{"type": "text", "content": "test content"}]}
}
},
{
"fieldgroups": {
"text1": {
"entries": [
{
"type": "placeholder",
"label": "test",
"content": "test_placeholder",
}
],
"overflow": "invalid_group",
}
}
},
{
"fieldgroups": {
"text1": {
"entries": [
{
"type": "placeholder",
"label": "test",
"content": "test_placeholder",
}
],
"overflow": "image1",
}
}
},
{
"fieldgroups": {
"text1": {
"entries": [
{
"type": "placeholder",
"label": "test",
"content": "test_placeholder",
}
],
},
"text2": {
"entries": [
{
"type": "placeholder",
"label": "test",
"content": "test_placeholder",
}
],
"overflow": "text1",
},
}
},
],
)
def test_schema_ticket_invalid(layout_context, layout):
style = TicketTestStyle()
schema = style.layout_schema(layout_context)
with pytest.raises(jsonschema.ValidationError):
jsonschema.validate(layout, schema)
def test_style_representation():
style = TicketTestStyle()
style_dict = style.asdict()
assert style_dict["platform"] == "test_platform"
assert style_dict["identifier"] == "test_ticket"
assert style_dict["name"] == _("Test Wallet Style Ticket")
assert style_dict["fieldgroups"][0]["identifier"] == "text1"
assert style_dict["fieldgroups"][0]["name"] == "Text 1"
assert style_dict["fieldgroups"][0]["content_type"] == "text"
assert style_dict["fieldgroups"][0]["labels"] == True
assert style_dict["fieldgroups"][0]["required"] == True
def test_layout_generate(layout_context):
style = TicketTestStyle()
layout = {
"fieldgroups": {
"text1": {
"entries": [
{
"type": "placeholder",
"label": {"de": "test-de", "en": "test-en"},
"content": "test_placeholder",
},
{"type": "text", "label": "test", "content": "test content"},
],
"overflow": "text2",
}
}
}
pass_layout = PassLayout(style, layout)
generated_pass = pass_layout.generate(layout_context)
assert (
generated_pass
== "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";'