forked from CGM_Public/pretix_original
prettify & cleanup preview support multiline fields in preview add venue store active state for predefined fieldgroups
387 lines
13 KiB
Python
387 lines
13 KiB
Python
import enum
|
|
from typing import TypedDict
|
|
from i18nfield.strings import LazyI18nString
|
|
import jsonschema
|
|
from django.core.exceptions import ValidationError
|
|
from pretix.base.models import OrderPosition
|
|
from ..placeholders import WalletPlaceholderContext, get_wallet_placeholders
|
|
|
|
|
|
class WalletPlatform:
|
|
identifier: str
|
|
name: str
|
|
|
|
|
|
class LayoutContext(TypedDict):
|
|
placeholders: dict[str, dict]
|
|
|
|
|
|
class FieldGroupType(enum.Enum):
|
|
PLACEHOLDER = "placeholder"
|
|
PREDEFINED = "predefined"
|
|
|
|
|
|
class FieldGroupDisplay(enum.Enum):
|
|
PLAIN = "plain"
|
|
WITH_LABEL = "with_label"
|
|
CODE = "code"
|
|
|
|
|
|
class FieldGroup:
|
|
type: FieldGroupType
|
|
identifier: str
|
|
name: str
|
|
description: str
|
|
required: bool = False
|
|
|
|
def __init__(self, identifier: str, name: str, description=None, required=False):
|
|
self.identifier = identifier
|
|
self.name = name
|
|
self.required = required
|
|
self.description = description or ""
|
|
|
|
def layout_schema(
|
|
self,
|
|
remaining_fields: list["FieldGroup"],
|
|
context: LayoutContext,
|
|
) -> dict:
|
|
raise NotImplementedError()
|
|
|
|
def asdict(self):
|
|
return {
|
|
"type": self.type.value,
|
|
"identifier": self.identifier,
|
|
"name": self.name,
|
|
"description": self.description,
|
|
"required": self.required,
|
|
}
|
|
|
|
|
|
class FieldContentType(enum.Enum):
|
|
IMAGE = "image"
|
|
TEXT = "text"
|
|
|
|
|
|
class FieldEntryType(enum.Enum):
|
|
CUSTOM = "custom"
|
|
PLACEHOLDER = "placeholder"
|
|
|
|
|
|
class FieldEntry[T]:
|
|
type: FieldEntryType
|
|
label: LazyI18nString | None
|
|
content: T
|
|
|
|
def __init__(
|
|
self, type: FieldEntryType, content: T, label: LazyI18nString | None = None
|
|
):
|
|
self.type = type
|
|
self.label = label
|
|
self.content = content
|
|
|
|
def asdict(self) -> dict:
|
|
return {
|
|
"type": self.type.value,
|
|
"content": self.content,
|
|
"label": self.label.data if self.label else None,
|
|
}
|
|
|
|
|
|
class PlaceholderFieldEntry(FieldEntry[str]):
|
|
type = FieldEntryType.PLACEHOLDER
|
|
label: LazyI18nString | None
|
|
content: str
|
|
|
|
def __init__(self, content: str, label: LazyI18nString | None = None):
|
|
self.label = label
|
|
self.content = content
|
|
|
|
|
|
class CustomFieldEntry(FieldEntry[LazyI18nString]):
|
|
type: FieldEntryType
|
|
label: LazyI18nString | None
|
|
content: LazyI18nString
|
|
|
|
def asdict(self) -> dict:
|
|
return {
|
|
"type": self.type.value,
|
|
"content": self.content.data,
|
|
"label": self.label.data if self.label else None,
|
|
}
|
|
|
|
|
|
class PredefinedFieldGroup(FieldGroup):
|
|
type = FieldGroupType.PREDEFINED
|
|
|
|
def layout_schema(
|
|
self,
|
|
remaining_fields: list["FieldGroup"],
|
|
context: LayoutContext,
|
|
):
|
|
return {"type": "object", "properties": {"active": {"type": "boolean"}}}
|
|
|
|
|
|
class PlaceholderFieldGroup(FieldGroup):
|
|
type = FieldGroupType.PLACEHOLDER
|
|
content_type: FieldContentType
|
|
default_entries: list[FieldEntry]
|
|
display: FieldGroupDisplay
|
|
min_entries: int | None
|
|
max_entries: int | None
|
|
context_args: set[
|
|
str
|
|
] # what context arguments are available when rendering this fieldgroup
|
|
|
|
def __init__(
|
|
self,
|
|
identifier: str,
|
|
name: str,
|
|
content_type: FieldContentType,
|
|
description: str = "",
|
|
required=False,
|
|
default_entries=None,
|
|
min_entries=None,
|
|
max_entries=None,
|
|
display=FieldGroupDisplay.WITH_LABEL,
|
|
context_args: set[str] | None = None,
|
|
):
|
|
super().__init__(identifier, name, description, required)
|
|
self.content_type = content_type
|
|
self.default_entries = default_entries or []
|
|
self.min_entries = min_entries
|
|
self.max_entries = max_entries
|
|
self.display = display
|
|
self.context_args = context_args or set()
|
|
|
|
if self.required and (self.min_entries is None or self.min_entries < 1):
|
|
self.min_entries = 1
|
|
|
|
def asdict(self):
|
|
return {
|
|
**super().asdict(),
|
|
"content_type": self.content_type.value,
|
|
"default_entries": [x.asdict() for x in self.default_entries],
|
|
"display": self.display.value,
|
|
"min_entries": self.min_entries,
|
|
"max_entries": self.max_entries,
|
|
"context_args": list(sorted(self.context_args)),
|
|
}
|
|
|
|
def layout_schema(
|
|
self,
|
|
remaining_fields: list["FieldGroup"],
|
|
context: LayoutContext,
|
|
):
|
|
content_type_placeholders = (
|
|
context["placeholders"].get(self.content_type.value, {}).values()
|
|
)
|
|
available_placeholders = [
|
|
x.identifier
|
|
for x in content_type_placeholders
|
|
if WalletPlaceholderContext.is_available(x, self.context_args)
|
|
]
|
|
return {
|
|
"type": "object",
|
|
"properties": {
|
|
"entries": self.entries_schema(placeholders=available_placeholders),
|
|
"overflow": {
|
|
"anyOf": [
|
|
{"type": "null"},
|
|
{
|
|
"type": "string",
|
|
"enum": [
|
|
f.identifier
|
|
for f in remaining_fields
|
|
if isinstance(f, PlaceholderFieldGroup)
|
|
and f.content_type == self.content_type
|
|
],
|
|
},
|
|
]
|
|
},
|
|
},
|
|
"required": ["entries"],
|
|
}
|
|
|
|
def entries_schema(self, placeholders: list[str]):
|
|
baseprops = {}
|
|
if self.display == FieldGroupDisplay.WITH_LABEL:
|
|
baseprops["label"] = {"$ref": "#/$defs/I18nString"}
|
|
|
|
schema = {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"anyOf": [
|
|
{
|
|
"properties": {
|
|
**baseprops,
|
|
"type": {"const": "placeholder"},
|
|
"content": {"enum": placeholders},
|
|
}
|
|
},
|
|
{
|
|
"properties": {
|
|
**baseprops,
|
|
"type": {"const": "custom"},
|
|
"content": {"$ref": "#/$defs/I18nString"},
|
|
}
|
|
},
|
|
],
|
|
"required": ["type", "content"],
|
|
},
|
|
}
|
|
if self.display == FieldGroupDisplay.WITH_LABEL:
|
|
schema["items"]["required"].append("label")
|
|
if self.min_entries is not None:
|
|
schema["minItems"] = self.min_entries
|
|
# max_entries is not enforced here, as the layout can have more fields than that (null-fields are removed, rest is overspilled)
|
|
return schema
|
|
|
|
|
|
class TextFieldGroup(PlaceholderFieldGroup):
|
|
content_type = FieldContentType.TEXT
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(content_type=self.content_type, **kwargs)
|
|
|
|
|
|
class ImageFieldGroup(PlaceholderFieldGroup):
|
|
content_type = FieldContentType.IMAGE
|
|
display = FieldGroupDisplay.PLAIN
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(content_type=self.content_type, display=self.display, **kwargs)
|
|
|
|
|
|
class PassStyle:
|
|
identifier: str # unique within platform
|
|
name: str
|
|
# order here limits in what order users can configure field "overspilling" (if too many fields are defined, where should the rest go)
|
|
# -> can only go down in the list
|
|
# we evaluate the fields in this order, so they overspill in this order as well
|
|
fieldgroups: list[FieldGroup]
|
|
|
|
@property
|
|
def preview_layout(self) -> list | None:
|
|
return None
|
|
|
|
def asdict(self):
|
|
return {
|
|
"identifier": self.identifier,
|
|
"name": self.name,
|
|
"fieldgroups": [x.asdict() for x in self.fieldgroups],
|
|
"preview_layout": self.preview_layout,
|
|
}
|
|
|
|
def layout_schema(self):
|
|
context = LayoutContext(placeholders=self.placeholders)
|
|
schema = {
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
# TODO: $id
|
|
"title": self.name,
|
|
"type": "object",
|
|
"properties": {
|
|
"fieldgroups": {
|
|
"description": "Layout Field Groups",
|
|
"type": "object",
|
|
"properties": {
|
|
group.identifier: group.layout_schema(
|
|
context=context, remaining_fields=self.fieldgroups[i:]
|
|
)
|
|
for (i, group) in enumerate(self.fieldgroups)
|
|
},
|
|
"required": [
|
|
group.identifier for group in self.fieldgroups if group.required
|
|
],
|
|
}
|
|
},
|
|
"$defs": {
|
|
"I18nString": {
|
|
"oneOf": [
|
|
{"type": "string"},
|
|
{"type": "object", "additionalProperties": {"type": "string"}},
|
|
]
|
|
}
|
|
},
|
|
}
|
|
if any(group.required for group in self.fieldgroups):
|
|
schema["required"] = ["fieldgroups"]
|
|
|
|
return schema
|
|
|
|
def render_placeholder(self, context, content_type, content):
|
|
placeholder = self.placeholders.get(content_type, {}).get(content)
|
|
if placeholder:
|
|
placeholder_value = context.render_placeholder(placeholder)
|
|
if placeholder_value:
|
|
return placeholder.label, placeholder_value
|
|
|
|
return None, None
|
|
|
|
def __init__(self, event, layout):
|
|
self.event = event
|
|
self.layout = layout
|
|
self.placeholders = get_wallet_placeholders(self.event)
|
|
|
|
def validate(self):
|
|
schema = self.layout_schema()
|
|
try:
|
|
jsonschema.validate(self.layout, schema)
|
|
except jsonschema.ValidationError as e:
|
|
raise ValidationError("Invalid layout: {}".format(str(e)))
|
|
|
|
def get_pass_fields(self, op: OrderPosition):
|
|
context = WalletPlaceholderContext(
|
|
event=self.event, order=op.order, order_position=op
|
|
)
|
|
|
|
fields = {}
|
|
for group in self.fieldgroups:
|
|
if isinstance(group, PredefinedFieldGroup):
|
|
pass
|
|
|
|
elif isinstance(group, PlaceholderFieldGroup):
|
|
group_fields = fields.get(group.identifier, [])
|
|
if group.identifier in self.layout["fieldgroups"]:
|
|
for field in self.layout["fieldgroups"][group.identifier][
|
|
"entries"
|
|
]:
|
|
field_entry = {}
|
|
if group.display == FieldGroupDisplay.WITH_LABEL:
|
|
field_entry["label"] = LazyI18nString(field["label"])
|
|
if field["type"] == FieldEntryType.PLACEHOLDER.value:
|
|
label, field_entry["value"] = self.render_placeholder(
|
|
context, group.content_type.value, field["content"]
|
|
)
|
|
if (
|
|
group.display == FieldGroupDisplay.WITH_LABEL
|
|
and not str(field_entry["label"])
|
|
and label
|
|
):
|
|
field_entry["label"] = LazyI18nString(label)
|
|
|
|
elif field["type"] == FieldEntryType.CUSTOM.value:
|
|
field_entry["value"] = LazyI18nString(field["content"])
|
|
if "value" in field_entry and field_entry["value"]:
|
|
group_fields.append(field_entry)
|
|
if group.min_entries and len(group_fields) < group.min_entries:
|
|
raise ValueError(
|
|
f"Group {group.identifier} needs at least {group.min_entries} entries, but only {len(group_fields)} were provided"
|
|
)
|
|
fields[group.identifier] = group_fields[: group.max_entries]
|
|
if overflow_group := self.layout["fieldgroups"][group.identifier][
|
|
"overflow"
|
|
]:
|
|
fields.setdefault(overflow_group, [])
|
|
fields[overflow_group] += group_fields[group.max_entries :]
|
|
|
|
else:
|
|
raise ValueError("Unknown field group")
|
|
return fields
|
|
|
|
def group_is_active(self, identifier: str):
|
|
return self.layout['fieldgroups'].get(identifier, {}).get("active", False)
|
|
|
|
def generate(self, op: OrderPosition):
|
|
raise NotImplementedError()
|