forked from CGM_Public/pretix_original
Writable API for ticket layouts (#3004)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
co-authored by
Richard Schreiber
parent
2e702b87de
commit
e5528f7784
@@ -17,9 +17,13 @@ Field Type Description
|
|||||||
id integer Internal layout ID
|
id integer Internal layout ID
|
||||||
name string Internal layout description
|
name string Internal layout description
|
||||||
default boolean ``true`` if this is the default layout
|
default boolean ``true`` if this is the default layout
|
||||||
layout object Layout specification for libpretixprint
|
layout list Dynamic layout specification. Each list element
|
||||||
|
corresponds to one dynamic element of the layout.
|
||||||
|
The current version of the schema in use can be found
|
||||||
|
`here`_.
|
||||||
|
Submitting invalid content can lead to application errors.
|
||||||
background URL Background PDF file
|
background URL Background PDF file
|
||||||
item_assignments list of objects Products this layout is assigned to
|
item_assignments list of objects Products this layout is assigned to (currently read-only)
|
||||||
├ sales_channel string Sales channel (defaults to ``web``).
|
├ sales_channel string Sales channel (defaults to ``web``).
|
||||||
└ item integer Item ID
|
└ item integer Item ID
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
@@ -58,7 +62,7 @@ Endpoints
|
|||||||
"name": "Default layout",
|
"name": "Default layout",
|
||||||
"default": true,
|
"default": true,
|
||||||
"layout": {…},
|
"layout": {…},
|
||||||
"background": {},
|
"background": null,
|
||||||
"item_assignments": []
|
"item_assignments": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -96,7 +100,7 @@ Endpoints
|
|||||||
"name": "Default layout",
|
"name": "Default layout",
|
||||||
"default": true,
|
"default": true,
|
||||||
"layout": {…},
|
"layout": {…},
|
||||||
"background": {},
|
"background": null,
|
||||||
"item_assignments": []
|
"item_assignments": []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,3 +151,122 @@ Endpoints
|
|||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
|
||||||
|
|
||||||
|
Creates a new ticket layout
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/ticketlayouts/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Default layout",
|
||||||
|
"default": true,
|
||||||
|
"layout": […],
|
||||||
|
"background": null,
|
||||||
|
"item_assignments": []
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Default layout",
|
||||||
|
"default": true,
|
||||||
|
"layout": […],
|
||||||
|
"background": null,
|
||||||
|
"item_assignments": []
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer of the event to create a layout for
|
||||||
|
:param event: The ``slug`` field of the event to create a layout for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The layout could not be created due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/(id)/
|
||||||
|
|
||||||
|
Update a layout. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||||
|
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||||
|
want to change.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/ticketlayouts/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 94
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Default layout"
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Default layout",
|
||||||
|
"default": true,
|
||||||
|
"layout": […],
|
||||||
|
"background": null,
|
||||||
|
"item_assignments": []
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param id: The ``id`` field of the layout to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The layout could not be modified due to invalid submitted data
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/(id)/
|
||||||
|
|
||||||
|
Delete a layout.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/events/sampleconf/ticketlayouts/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param id: The ``id`` field of the layout to delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||||
|
|
||||||
|
|
||||||
|
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/pdf-layout.schema.json
|
||||||
|
|||||||
+25
-2
@@ -35,6 +35,7 @@
|
|||||||
import copy
|
import copy
|
||||||
import hashlib
|
import hashlib
|
||||||
import itertools
|
import itertools
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -46,12 +47,15 @@ from collections import OrderedDict
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
import jsonschema
|
||||||
from arabic_reshaper import ArabicReshaper
|
from arabic_reshaper import ArabicReshaper
|
||||||
from bidi.algorithm import get_display
|
from bidi.algorithm import get_display
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Max, Min
|
from django.db.models import Max, Min
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
from django.utils.deconstruct import deconstructible
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject
|
||||||
from django.utils.html import conditional_escape
|
from django.utils.html import conditional_escape
|
||||||
@@ -740,9 +744,9 @@ class Renderer:
|
|||||||
|
|
||||||
if o['content'] == 'other' or o['content'] == 'other_i18n':
|
if o['content'] == 'other' or o['content'] == 'other_i18n':
|
||||||
if o['content'] == 'other_i18n':
|
if o['content'] == 'other_i18n':
|
||||||
text = str(LazyI18nString(o['text_i18n']))
|
text = str(LazyI18nString(o.get('text_i18n', {})))
|
||||||
else:
|
else:
|
||||||
text = o['text']
|
text = o.get('text', '')
|
||||||
|
|
||||||
def replace(x):
|
def replace(x):
|
||||||
if x.group(1).startswith('itemmeta:'):
|
if x.group(1).startswith('itemmeta:'):
|
||||||
@@ -975,3 +979,22 @@ class Renderer:
|
|||||||
output.write(outbuffer)
|
output.write(outbuffer)
|
||||||
outbuffer.seek(0)
|
outbuffer.seek(0)
|
||||||
return outbuffer
|
return outbuffer
|
||||||
|
|
||||||
|
|
||||||
|
@deconstructible
|
||||||
|
class PdfLayoutValidator:
|
||||||
|
def __call__(self, value):
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
try:
|
||||||
|
val = json.loads(value)
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError(_('Your layout file is not a valid JSON file.'))
|
||||||
|
else:
|
||||||
|
val = value
|
||||||
|
with open(finders.find('schema/pdf-layout.schema.json'), 'r') as f:
|
||||||
|
schema = json.loads(f.read())
|
||||||
|
try:
|
||||||
|
jsonschema.validate(val, schema)
|
||||||
|
except jsonschema.ValidationError as e:
|
||||||
|
e = str(e).replace('%', '%%')
|
||||||
|
raise ValidationError(_('Your layout file is not a valid layout. Error message: {}').format(e))
|
||||||
|
|||||||
@@ -458,7 +458,9 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="text/plain" id="schema-url">{% static "schema/pdf-layout.schema.json" %}</script>
|
||||||
<script type="text/javascript" src="{% static "pdfjs/pdf.js" %}"></script>
|
<script type="text/javascript" src="{% static "pdfjs/pdf.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "ajv/ajv2020.bundle.min.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "fabric/fabric.min.js" %}"></script>
|
<script type="text/javascript" src="{% static "fabric/fabric.min.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/editor.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/editor.js" %}"></script>
|
||||||
<img src="{% static 'pretixpresale/pdf/powered_by_pretix_dark.png' %}" id="poweredby-dark" class="sr-only">
|
<img src="{% static 'pretixpresale/pdf/powered_by_pretix_dark.png' %}" id="poweredby-dark" class="sr-only">
|
||||||
|
|||||||
@@ -19,11 +19,16 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import transaction
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.order import CompatibleJSONField
|
from pretix.api.serializers.order import CompatibleJSONField
|
||||||
|
|
||||||
|
from ...api.serializers.fields import UploadedFileField
|
||||||
|
from ...base.pdf import PdfLayoutValidator
|
||||||
from ...multidomain.utils import static_absolute
|
from ...multidomain.utils import static_absolute
|
||||||
from .models import TicketLayout, TicketLayoutItem
|
from .models import TicketLayout, TicketLayoutItem
|
||||||
|
|
||||||
@@ -43,8 +48,13 @@ class NestedItemAssignmentSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class TicketLayoutSerializer(I18nAwareModelSerializer):
|
class TicketLayoutSerializer(I18nAwareModelSerializer):
|
||||||
layout = CompatibleJSONField()
|
layout = CompatibleJSONField(
|
||||||
item_assignments = NestedItemAssignmentSerializer(many=True)
|
validators=[PdfLayoutValidator()]
|
||||||
|
)
|
||||||
|
item_assignments = NestedItemAssignmentSerializer(many=True, read_only=True)
|
||||||
|
background = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||||
|
'application/pdf',
|
||||||
|
), max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TicketLayout
|
model = TicketLayout
|
||||||
@@ -56,8 +66,17 @@ class TicketLayoutSerializer(I18nAwareModelSerializer):
|
|||||||
d['background'] = static_absolute(instance.event, "pretixpresale/pdf/ticket_default_a4.pdf")
|
d['background'] = static_absolute(instance.event, "pretixpresale/pdf/ticket_default_a4.pdf")
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
if attrs.get('default') and self.context['event'].ticket_layouts.filter(default=True).exists:
|
||||||
|
raise ValidationError('You cannot have two layouts with default = True')
|
||||||
|
return attrs
|
||||||
|
|
||||||
class TicketLayoutViewSet(viewsets.ReadOnlyModelViewSet):
|
def create(self, validated_data):
|
||||||
|
validated_data["event"] = self.context["event"]
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
class TicketLayoutViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = TicketLayoutSerializer
|
serializer_class = TicketLayoutSerializer
|
||||||
queryset = TicketLayout.objects.none()
|
queryset = TicketLayout.objects.none()
|
||||||
lookup_field = 'id'
|
lookup_field = 'id'
|
||||||
@@ -65,6 +84,45 @@ class TicketLayoutViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.ticket_layouts.all()
|
return self.request.event.ticket_layouts.all()
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
return {
|
||||||
|
**super().get_serializer_context(),
|
||||||
|
'event': self.request.event,
|
||||||
|
}
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
instance.log_action(
|
||||||
|
action='pretix.plugins.ticketoutputpdf.layout.deleted',
|
||||||
|
user=self.request.user, auth=self.request.auth
|
||||||
|
)
|
||||||
|
super().perform_destroy(instance)
|
||||||
|
if not self.request.event.ticket_layouts.filter(default=True).exists():
|
||||||
|
f = self.request.event.ticket_layouts.first()
|
||||||
|
if f:
|
||||||
|
f.default = True
|
||||||
|
f.save(update_fields=['default'])
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
super().perform_create(serializer)
|
||||||
|
serializer.instance.log_action(
|
||||||
|
action='pretix.plugins.ticketoutputpdf.layout.added',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=self.request.data,
|
||||||
|
)
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
super().perform_update(serializer)
|
||||||
|
serializer.instance.log_action(
|
||||||
|
action='pretix.plugins.ticketoutputpdf.layout.changed',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=self.request.data,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TicketLayoutItemViewSet(viewsets.ReadOnlyModelViewSet):
|
class TicketLayoutItemViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = ItemAssignmentSerializer
|
serializer_class = ItemAssignmentSerializer
|
||||||
|
|||||||
+7
File diff suppressed because one or more lines are too long
@@ -118,6 +118,7 @@ var editor = {
|
|||||||
uploaded_file_id: null,
|
uploaded_file_id: null,
|
||||||
_window_loaded: false,
|
_window_loaded: false,
|
||||||
_fabric_loaded: false,
|
_fabric_loaded: false,
|
||||||
|
schema: null,
|
||||||
|
|
||||||
_px2mm: function (v) {
|
_px2mm: function (v) {
|
||||||
return v / editor.pdf_scale / 72 * editor.pdf_page.userUnit * 25.4;
|
return v / editor.pdf_scale / 72 * editor.pdf_page.userUnit * 25.4;
|
||||||
@@ -988,8 +989,26 @@ var editor = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
_source_save: function () {
|
_source_save: function () {
|
||||||
editor.load(JSON.parse($("#source-textarea").val()));
|
try {
|
||||||
$("#source-container").hide();
|
var Ajv = window.ajv2020
|
||||||
|
var ajv = new Ajv()
|
||||||
|
var validate = ajv.compile(editor.schema)
|
||||||
|
var data = JSON.parse($("#source-textarea").val())
|
||||||
|
var valid = validate(data)
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
console.log(validate.errors)
|
||||||
|
alert("Invalid input syntax. If you're familiar with this, check out the developer console for a full " +
|
||||||
|
"error log. Otherwise, please contact support.")
|
||||||
|
} else {
|
||||||
|
editor.load(data);
|
||||||
|
$("#source-container").hide();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
alert("Parsing error. If you're familiar with this, check out the developer console for a full " +
|
||||||
|
"error log. Otherwise, please contact support.")
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_create_empty_background: function () {
|
_create_empty_background: function () {
|
||||||
@@ -1098,6 +1117,10 @@ var editor = {
|
|||||||
$("#toolbox-source").bind('click', editor._source_show);
|
$("#toolbox-source").bind('click', editor._source_show);
|
||||||
$("#source-close").bind('click', editor._source_close);
|
$("#source-close").bind('click', editor._source_close);
|
||||||
$("#source-save").bind('click', editor._source_save);
|
$("#source-save").bind('click', editor._source_save);
|
||||||
|
|
||||||
|
$.getJSON($("#schema-url").text(), function (data) {
|
||||||
|
editor.schema = data;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,371 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"title": "Ticket Layout",
|
||||||
|
"description": "Dynamic elements for a PDF layout",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Layout object",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"left",
|
||||||
|
"bottom",
|
||||||
|
"size"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "barcodearea"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"description": "Page number this will be shown on, defaults to 1.",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"left": {
|
||||||
|
"description": "Position of the element on the x axis in millimeters.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bottom": {
|
||||||
|
"description": "Position of the element on the y axis in millimeters.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"description": "Name of a variable to use. The available values depend on event configuration and installed plugins. Defaults to 'secret'.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"description": "Custom text. Only used when 'content' is set to 'other'.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"text_i18n": {
|
||||||
|
"description": "Custom text in multiple languages. Only used when 'content' is set to 'other_i18n'.",
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"[a-zA-Z-]+": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"description": "Size of the barcode in millimeters.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nowhitespace": {
|
||||||
|
"description": "Whether a barcode should be rendered without margins. Only used for type 'barcodearea'.",
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"left",
|
||||||
|
"bottom",
|
||||||
|
"content",
|
||||||
|
"width",
|
||||||
|
"height"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "imagearea"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"description": "Page number this will be shown on, defaults to 1.",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"left": {
|
||||||
|
"description": "Position of the element on the x axis in millimeters.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bottom": {
|
||||||
|
"description": "Position of the element on the y axis in millimeters.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"description": "Name of a variable to use. The available values depend on event configuration and installed plugins.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"description": "Width of the element in millimeters.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"description": "Height of the element in millimeters.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"left",
|
||||||
|
"bottom",
|
||||||
|
"content",
|
||||||
|
"width",
|
||||||
|
"fontsize",
|
||||||
|
"fontfamily",
|
||||||
|
"bold",
|
||||||
|
"italic",
|
||||||
|
"align",
|
||||||
|
"color"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "textarea"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"description": "Page number this will be shown on, defaults to 1.",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"left": {
|
||||||
|
"description": "Position of the element on the x axis in millimeters.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bottom": {
|
||||||
|
"description": "Position of the element on the y axis in millimeters.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"description": "Width of the element in millimeters.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"description": "Name of a variable to use. The available values depend on event configuration and installed plugins.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"description": "Custom text. Only used when 'content' is set to 'other'.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"text_i18n": {
|
||||||
|
"description": "Custom text in multiple languages. Only used when 'content' is set to 'other_i18n'.",
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"[a-zA-Z-]+": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"locale": {
|
||||||
|
"description": "Locale to render the text in.",
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"pattern": "[a-zA-Z-]*"
|
||||||
|
},
|
||||||
|
"fontsize": {
|
||||||
|
"description": "Font size.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fontfamily": {
|
||||||
|
"description": "Font family. The available values depend on installed plugins.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"bold": {
|
||||||
|
"description": "Use bold font variant.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"italic": {
|
||||||
|
"description": "Use italic font variant.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"align": {
|
||||||
|
"description": "Text alignment.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"left",
|
||||||
|
"center",
|
||||||
|
"right"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"description": "Text color as a tuple of three integers in the 0-255 range and one float in the 0-1 range. The last value (alpha) is ignored by the current implementation but might be used in the future.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 255
|
||||||
|
},
|
||||||
|
"minItems": 3,
|
||||||
|
"maxItems": 4
|
||||||
|
},
|
||||||
|
"downward": {
|
||||||
|
"description": "Downward rendering of text (recommended for new layouts), but default is false for backwards-compatibility.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"rotation": {
|
||||||
|
"description": "Rotation in degrees.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"left",
|
||||||
|
"bottom",
|
||||||
|
"content",
|
||||||
|
"size"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "poweredby"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"description": "Page number this will be shown on, defaults to 1.",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"left": {
|
||||||
|
"description": "Position of the element on the x axis in millimeters.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bottom": {
|
||||||
|
"description": "Position of the element on the y axis in millimeters.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"description": "Name of a style to use. The available values currently are 'dark' and 'white'.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"dark",
|
||||||
|
"white"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"description": "Size of the logo in millimeters.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,9 +23,12 @@ import copy
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from pretix.base.models import Event, Item, Organizer, Team, User
|
from pretix.base.models import Event, Item, Organizer, Team
|
||||||
from pretix.plugins.ticketoutputpdf.models import TicketLayoutItem
|
from pretix.plugins.ticketoutputpdf.models import TicketLayoutItem
|
||||||
|
|
||||||
|
|
||||||
@@ -36,14 +39,25 @@ def env():
|
|||||||
organizer=o, name='Dummy', slug='dummy',
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
date_from=now(), plugins='pretix.plugins.banktransfer'
|
date_from=now(), plugins='pretix.plugins.banktransfer'
|
||||||
)
|
)
|
||||||
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
|
||||||
t = Team.objects.create(organizer=event.organizer)
|
t = Team.objects.create(organizer=event.organizer)
|
||||||
t.members.add(user)
|
|
||||||
t.limit_events.add(event)
|
t.limit_events.add(event)
|
||||||
item1 = Item.objects.create(event=event, name="Ticket", default_price=23)
|
item1 = Item.objects.create(event=event, name="Ticket", default_price=23)
|
||||||
tl = event.ticket_layouts.create(name="Foo", default=True, layout='[{"a": 2}]')
|
tl = event.ticket_layouts.create(name="Foo", default=True, layout='[{"a": 2}]')
|
||||||
TicketLayoutItem.objects.create(layout=tl, item=item1)
|
TicketLayoutItem.objects.create(layout=tl, item=item1)
|
||||||
return event, user, tl, item1
|
return event, tl, item1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
return APIClient()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
@scopes_disabled()
|
||||||
|
def token_client(client, env):
|
||||||
|
t = env[0].organizer.teams.get().tokens.create(name="Foo")
|
||||||
|
client.credentials(HTTP_AUTHORIZATION="Token " + t.token)
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
RES_LAYOUT = {
|
RES_LAYOUT = {
|
||||||
@@ -57,32 +71,145 @@ RES_LAYOUT = {
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_api_list(env, client):
|
def test_api_list(env, token_client):
|
||||||
res = copy.copy(RES_LAYOUT)
|
res = copy.copy(RES_LAYOUT)
|
||||||
res['id'] = env[2].pk
|
res['id'] = env[1].pk
|
||||||
res['item_assignments'][0]['item'] = env[3].pk
|
res['item_assignments'][0]['item'] = env[2].pk
|
||||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
r = token_client.get('/api/v1/organizers/{}/events/{}/ticketlayouts/'.format(
|
||||||
r = json.loads(
|
env[0].organizer.slug, env[0].slug)).data
|
||||||
client.get('/api/v1/organizers/{}/events/{}/ticketlayouts/'.format(
|
|
||||||
env[0].slug, env[0].organizer.slug)).content.decode('utf-8')
|
|
||||||
)
|
|
||||||
assert r['results'] == [res]
|
assert r['results'] == [res]
|
||||||
r = json.loads(
|
r = token_client.get('/api/v1/organizers/{}/events/{}/ticketlayoutitems/'.format(
|
||||||
client.get('/api/v1/organizers/{}/events/{}/ticketlayoutitems/'.format(
|
env[0].organizer.slug, env[0].slug)).data
|
||||||
env[0].slug, env[0].organizer.slug)).content.decode('utf-8')
|
assert r['results'] == [{'item': env[2].pk, 'layout': env[1].pk, 'id': env[1].item_assignments.first().pk,
|
||||||
)
|
|
||||||
assert r['results'] == [{'item': env[3].pk, 'layout': env[2].pk, 'id': env[2].item_assignments.first().pk,
|
|
||||||
'sales_channel': 'web'}]
|
'sales_channel': 'web'}]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_api_detail(env, client):
|
def test_api_detail(env, token_client):
|
||||||
res = copy.copy(RES_LAYOUT)
|
res = copy.copy(RES_LAYOUT)
|
||||||
res['id'] = env[2].pk
|
res['id'] = env[1].pk
|
||||||
res['item_assignments'][0]['item'] = env[3].pk
|
res['item_assignments'][0]['item'] = env[2].pk
|
||||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
r = token_client.get('/api/v1/organizers/{}/events/{}/ticketlayouts/{}/'.format(
|
||||||
r = json.loads(
|
env[0].organizer.slug, env[0].slug, env[1].pk)).data
|
||||||
client.get('/api/v1/organizers/{}/events/{}/ticketlayouts/{}/'.format(
|
|
||||||
env[0].slug, env[0].organizer.slug, env[2].pk)).content.decode('utf-8')
|
|
||||||
)
|
|
||||||
assert r == res
|
assert r == res
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_api_create(env, token_client):
|
||||||
|
r = token_client.post(
|
||||||
|
'/api/v1/upload',
|
||||||
|
data={
|
||||||
|
'media_type': 'application/pdf',
|
||||||
|
'file': ContentFile('file.pdf', 'invalid pdf content')
|
||||||
|
},
|
||||||
|
format='upload',
|
||||||
|
HTTP_CONTENT_DISPOSITION='attachment; filename="file.pdf"',
|
||||||
|
)
|
||||||
|
assert r.status_code == 201
|
||||||
|
file_id_png = r.data['id']
|
||||||
|
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/ticketlayouts/'.format(env[0].slug, env[0].slug),
|
||||||
|
{
|
||||||
|
'name': 'Foo',
|
||||||
|
'default': False,
|
||||||
|
"background": file_id_png,
|
||||||
|
'layout': [],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
tl = env[0].ticket_layouts.get(pk=resp.data["id"])
|
||||||
|
assert tl.background
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_api_create_validate_default(env, token_client):
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/ticketlayouts/'.format(env[0].slug, env[0].slug),
|
||||||
|
{
|
||||||
|
'name': 'Foo',
|
||||||
|
'default': True,
|
||||||
|
'layout': [],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.data == {"non_field_errors": ["You cannot have two layouts with default = True"]}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_api_create_validate_layout(env, token_client):
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/ticketlayouts/'.format(env[0].slug, env[0].slug),
|
||||||
|
{
|
||||||
|
'name': 'Foo',
|
||||||
|
'default': True,
|
||||||
|
'layout': [
|
||||||
|
{
|
||||||
|
"foo": "bar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.data["layout"][0].startswith("Your layout file is not a valid layout. Error message:")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_api_update(env, token_client):
|
||||||
|
r = token_client.post(
|
||||||
|
'/api/v1/upload',
|
||||||
|
data={
|
||||||
|
'media_type': 'application/pdf',
|
||||||
|
'file': ContentFile('file.pdf', 'invalid pdf content')
|
||||||
|
},
|
||||||
|
format='upload',
|
||||||
|
HTTP_CONTENT_DISPOSITION='attachment; filename="file.pdf"',
|
||||||
|
)
|
||||||
|
assert r.status_code == 201
|
||||||
|
file_id_png = r.data['id']
|
||||||
|
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/ticketlayouts/{}/'.format(env[0].slug, env[0].slug, env[1].pk),
|
||||||
|
{
|
||||||
|
"name": "Bar",
|
||||||
|
"background": file_id_png,
|
||||||
|
"layout": [
|
||||||
|
{"type": "barcodearea", "left": "7.00", "bottom": "11.15", "size": "45.00", "content": "secret"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
env[1].refresh_from_db()
|
||||||
|
assert env[1].name == "Bar"
|
||||||
|
assert env[1].background
|
||||||
|
assert json.loads(env[1].layout) == [
|
||||||
|
{"type": "barcodearea", "left": "7.00", "bottom": "11.15", "size": "45.00", "content": "secret"}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_api_update_validate_default(env, token_client):
|
||||||
|
tl2 = env[0].ticket_layouts.create(name="Foo", default=False, layout='[{"a": 2}]')
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/ticketlayouts/{}/'.format(env[0].slug, env[0].slug, tl2.pk),
|
||||||
|
{
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.data == {"non_field_errors": ["You cannot have two layouts with default = True"]}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_api_delete(env, token_client):
|
||||||
|
resp = token_client.delete(
|
||||||
|
'/api/v1/organizers/{}/events/{}/ticketlayouts/{}/'.format(env[0].slug, env[0].slug, env[1].pk),
|
||||||
|
)
|
||||||
|
assert resp.status_code == 204
|
||||||
|
assert not env[0].ticket_layouts.exists()
|
||||||
|
|||||||
Reference in New Issue
Block a user