Compare commits

...
Author SHA1 Message Date
robbi5 488746044b Don't select last added element on load 2026-09-24 18:53:17 +02:00
robbi5 81d7dfbbc4 Select the new element right after adding it in the pdf editor 2026-09-24 18:15:25 +02:00
Richard Schreiber ed68719731 Widget: fix calendar view showing old events due to non-unique key (Z#23247433) (#6585)
* Widget: fix calendar view showing old events due to cache-key (Z#23247433)

* Update EventCalendarCell.vue
2026-09-24 12:00:57 +02:00
9aeb4b9731 Organizer API: Add endpoint for event-meta-properties
* Add API-endpoint for event-meta-properties

* Add new doc-file to index, fix spelling and description

* Fix logentry

* Fix logentry again

* validate and add tests

* add meta_properties from organizer only

* fix choices validation

* filter unknown keys from choices due to django-formsets

* Apply batched suggestions from code review

Co-authored-by: Richard Schreiber <wiffbi@gmail.com>

* add safe-guard normalization to None to to_representation

* Apply batched suggestions from code review

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>

* update tests to check for error-messages as well

* fix flake8

* fix permission tests

* Make ObjectListField more flexibel for re-use

* fix validation result

* Improve validation

* Change to I18nField for validation

* update MetaPropertyDictField

* fix docs for i18n strings

* make label_child configurable if MetaPropertyDictField should contain non-localized stuff

* undo test changes in events test

* fix flake8

* Apply batched suggestions from code review

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>

* improve code formatting

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
Co-authored-by: Richard Schreiber <wiffbi@gmail.com>
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2026-09-24 10:51:23 +02:00
Minding 9324887790 Translate OAuth buttons (#6590)
* Translate OAuth buttons

* Revert .po changes
2026-09-24 09:52:47 +02:00
Nate Horst d353384555 Translations: Update Thai
Currently translated at 53.0% (3407 of 6419 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/th/

powered by weblate
2026-09-24 09:16:49 +02:00
12 changed files with 801 additions and 44 deletions
+241
View File
@@ -0,0 +1,241 @@
Event Meta Properties
=====================
Resource description
--------------------
An event meta property is used to to define meta information fields for its events.
This information can be re-used, for example, in ticket layouts.
The event meta property resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Unique ID for this property
name string Name of the property
default string Value of the default option
required boolean If ``true``, an event can only be taken live if the
property is set. In event series, it's always optional
to set a value for individual dates
protected boolean If ``true``, the value for an event can only be changed
by organizer-level administrators
filter_public boolean If ``true``, this property will be shown to filter
events in the public event list and calendar
public_label string Public name of the property
filter_allowed boolean If ``true``, this property will be shown to filter
events or reports in the backend, and it can also be
used for hidden filter parameters in the frontend
choices list of objects List of JSON objects representing all permitted values
for this property, or ``null`` for no limitation.
Each choice object has a required internal name named
``key`` and optional public name named ``label``
consisting of a dictionary of i18n string translations
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/event_meta_properties/
Returns a list of all meta properties for the organizer.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/meta_properties/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"name": "Color",
"default": "blue",
"required": false,
"protected": false,
"filter_public": false,
"public_label": {},
"filter_allowed": true,
"choices": [
{
"key": "blue",
"label": {
"en": "Blue"
},
}
]
}
]
}
:param organizer: The ``slug`` field of the organizer
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/event_meta_properties/(id)/
Returns information on one property, identified by its id.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/event_meta_properties/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
{
"id": 1,
"name": "Color",
"default": "blue",
"required": false,
"protected": false,
"filter_public": false,
"public_label": {},
"filter_allowed": true,
"choices": null
}
:param organizer: The ``slug`` field of the organizer
:param id: The ``id`` field of the meta property to retrieve
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/event_meta_properties/
Creates a new meta property
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/event_meta_properties/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"name": "ref-code",
"default": "abcde",
"required": true,
"choices": null
}
**Example response**:
.. sourcecode:: http
{
"id": 2,
"name": "reference",
"default": "abcde",
"required": true,
"protected": false,
"filter_public": false,
"public_label": null,
"filter_allowed": true,
"choices": null
}
:param organizer: The ``slug`` field of the organizer
:statuscode 201: no error
:statuscode 400: The meta property could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/event_meta_properties/(id)/
Update a meta property. 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.
You can change all fields of the resource except the ``id`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/event_meta_properties/2/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"required": false
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 3,
"name": "reference",
"default": "abcde",
"required": false,
"protected": false,
"filter_public": false,
"public_label": null,
"filter_allowed": true,
"choices": null
}
:param organizer: The ``slug`` field of the organizer
:param id: The ``id`` field of the meta property to modify
:statuscode 200: no error
:statuscode 400: The property could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/event_meta_properties/(id)/
Delete a meta property.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/event_meta_properties/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
:param id: The ``id`` field of the meta property to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.
+1
View File
@@ -12,6 +12,7 @@ at :ref:`plugin-docs`.
organizers
events
subevents
event_meta_properties
taxrules
categories
items
+90 -3
View File
@@ -28,6 +28,7 @@ from django.db import transaction
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.translation import gettext, gettext_lazy as _
from i18nfield.rest_framework import I18nField
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
@@ -40,9 +41,10 @@ from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.auth import get_auth_backends
from pretix.base.i18n import get_language_without_region
from pretix.base.models import (
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
Customer, Device, EventMetaProperty, GiftCard, GiftCardAcceptance,
GiftCardTransaction, Membership, MembershipType, OrderPosition, Organizer,
ReusableMedium, SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite,
User,
)
from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.permissions import (
@@ -640,3 +642,88 @@ class OrganizerSettingsSerializer(SettingsSerializer):
)
# TODO: make sure pub is always correct
return 'pub/' + fname
class MetaPropertyListField(serializers.ListField):
def __init__(self, *args, **kwargs):
kwargs["validators"] = kwargs.pop("validators", [])
def validate_keys_unique(choices):
if not choices:
return
keys = [c.get("key") for c in choices]
if len(set(keys)) < len(keys):
raise ValidationError("The key for each meta property value option must be unique.")
kwargs["validators"].append(
validate_keys_unique
)
super().__init__(*args, **kwargs)
class MetaPropertyDictField(serializers.DictField):
def __init__(self, **kwargs):
self.label_child = kwargs.pop("label_child", I18nField())
super().__init__(**kwargs)
def to_representation(self, value):
# django added unneccessary keys DELETE, ORDER through formsets, filter them here for backwards compat
d = {
"key": value["key"]
}
if "label" in value:
d["label"] = self.label_child.to_representation(value["label"])
return super().to_representation(d)
def to_internal_value(self, data):
if not isinstance(data, dict):
raise ValidationError("Meta property value options must be a dict.")
if not isinstance(data.get("key"), str):
raise ValidationError("Meta property value options must have a key of type string.")
if any(k not in {"key", "label"} for k in data.keys()):
raise ValidationError("Meta property value options may only have a key and optionally a label.")
if "label" in data:
try:
data["label"] = self.label_child.to_internal_value(data["label"])
except ValidationError as e:
raise ValidationError({"label": e.detail})
return super().to_internal_value(data)
class EventMetaPropertiesSerializer(I18nAwareModelSerializer):
choices = MetaPropertyListField(
child=MetaPropertyDictField(
label_child=I18nField()
),
allow_null=True,
)
class Meta:
model = EventMetaProperty
fields = (
'id', 'name', 'default', 'required', 'protected', 'filter_public', 'public_label', 'filter_allowed',
'choices'
)
def validate(self, data):
data = super().validate(data)
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
choices = full_data.get("choices")
default = full_data.get("default")
if choices and default:
choice_keys = [c.get("key") for c in choices]
if default not in choice_keys:
raise ValidationError("You cannot set a default value that is not a valid value.")
if not choices and "choices" in data:
# normalize empty dict to None
data["choices"] = None
return data
+1
View File
@@ -68,6 +68,7 @@ orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportVie
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
orga_router.register(r'transactions', order.OrganizerTransactionViewSet)
orga_router.register(r'orderpositions', order.OrganizerOrderPositionViewSet, basename='orderpositions')
orga_router.register(r'event_meta_properties', organizer.EventMetaPropertiesViewSet)
team_router = routers.DefaultRouter()
team_router.register(r'members', organizer.TeamMemberViewSet)
+51 -4
View File
@@ -44,15 +44,16 @@ from pretix.api.models import OAuthAccessToken
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.organizer import (
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
EventMetaPropertiesSerializer, GiftCardSerializer,
GiftCardTransactionSerializer, MembershipSerializer,
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer,
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
)
from pretix.base.models import (
Customer, Device, Event, GiftCard, GiftCardTransaction, LogEntry,
Membership, MembershipType, Organizer, SalesChannel, SeatingPlan, Team,
TeamAPIToken, TeamInvite, User,
Customer, Device, Event, EventMetaProperty, GiftCard, GiftCardTransaction,
LogEntry, Membership, MembershipType, Organizer, SalesChannel, SeatingPlan,
Team, TeamAPIToken, TeamInvite, User,
)
from pretix.base.plugins import (
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
@@ -846,3 +847,49 @@ class SalesChannelViewSet(viewsets.ModelViewSet):
data={'id': instance.pk}
)
instance.delete()
class EventMetaPropertiesViewSet(viewsets.ModelViewSet):
serializer_class = EventMetaPropertiesSerializer
queryset = EventMetaProperty.objects.none()
write_permission = 'organizer.settings.general:write'
def get_queryset(self):
return self.request.organizer.meta_properties.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
return ctx
@transaction.atomic()
def perform_destroy(self, instance):
instance.log_action(
'pretix.property.deleted',
user=self.request.user,
auth=self.request.auth,
data={'id': instance.pk}
)
instance.delete()
@transaction.atomic()
def perform_create(self, serializer):
inst = serializer.save(organizer_id=self.request.organizer.pk)
serializer.instance.log_action(
'pretix.property.created',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
@transaction.atomic()
def perform_update(self, serializer):
inst = serializer.save(organizer_id=self.request.organizer.pk)
serializer.instance.log_action(
'pretix.property.changed',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
+4
View File
@@ -717,6 +717,10 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
'pretix.organizer.outgoingmails.retried': _('Failed emails have been scheduled to be retried.'),
'pretix.organizer.outgoingmails.aborted': _('Queued emails have been aborted.'),
'pretix.property.created': _('An organizer meta property has been created.'),
'pretix.property.deleted': _('An organizer meta property has been deleted.'),
'pretix.property.changed': _('An organizer meta property has been changed.'),
'pretix.property.reordered': _('An organizer meta property has been reordered.'),
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'),
@@ -45,8 +45,8 @@
</p>
<div class="form-group buttons">
<input type="submit" class="btn btn-large btn-default" value="Cancel"/>
<input type="submit" class="btn btn-large btn-primary" name="allow" value="Authorize"/>
<input type="submit" class="btn btn-large btn-default" value="{% trans "Cancel" %}"/>
<input type="submit" class="btn btn-large btn-primary" name="allow" value="{% trans "Authorize" %}"/>
</div>
</form>
{% else %}
+173 -34
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-09-23 09:38+0000\n"
"PO-Revision-Date: 2026-09-23 18:00+0000\n"
"Last-Translator: Nate Horst <nate@agcthailand.org>\n"
"Language-Team: Thai <https://translate.pretix.eu/projects/pretix/pretix/th/>"
"\n"
@@ -11509,11 +11509,23 @@ msgid ""
"Best regards, \n"
"Your {event} team"
msgstr ""
"สวัสดี\n"
"\n"
"การพยายามชำระเงินสำหรับคำสั่งซื้อของคุณสำหรับงาน {event} ล้มเหลว\n"
"\n"
"คำสั่งซื้อของคุณยังคงมีผลสมบูรณ์ และคุณสามารถพยายามชำระเงินอีกครั้งโดยใช้วิธีการชำระเงินเดิมหรือวิ"
"ธีการอื่น โปรดทำรายการชำระเงินให้เสร็จสิ้นก่อน {expire_date}\n"
"\n"
"คุณสามารถลองชำระเงินใหม่และดูสถานะคำสั่งซื้อของคุณได้ที่ \n"
"{url}\n"
"\n"
"ขอแสดงความนับถือ\n"
"ทีมงาน {event}"
#: pretix/base/settings.py
#, python-brace-format
msgid "You have been selected from the waitinglist for {event}"
msgstr ""
msgstr "คุณได้รับเลือกจากรายชื่อสำรอง (Waiting list) สำหรับงาน {event}"
#: pretix/base/settings.py
#, python-brace-format
@@ -11545,11 +11557,36 @@ msgid ""
"Best regards, \n"
"Your {event} team"
msgstr ""
"สวัสดี\n"
"\n"
"คุณได้ลงทะเบียนในรายชื่อสำรองสำหรับงาน {event}\n"
"ในรายการสินค้า {product}\n"
"\n"
"ขณะนี้เรามีตั๋วพร้อมสำหรับคุณแล้ว! คุณสามารถใช้สิทธิ์รับตั๋วได้ในร้านค้าตั๋วของเราภายใน\n"
" {hours} ชั่วโมงข้างหน้า โดยกรอกรหัสวอเชอร์ดังต่อไปนี้:\n"
"\n"
"{code}\n"
"\n"
"หรือคุณสามารถคลิกที่ลิงก์ต่อไปนี้ได้ทันที:\n"
"\n"
"{url}\n"
"\n"
"โปรดทราบว่าลิงก์นี้ใช้ได้ภายใน {hours} ชั่วโมงข้างหน้าเท่านั้น!\n"
"หากคุณไม่ใช้สิทธิ์วอเชอร์ภายในระยะเวลาดังกล่าว\n"
"เราจะจัดสรรตั๋วให้กับบุคคลถัดไปในรายชื่อสำรอง\n"
"\n"
"หากคุณไม่ต้องการตั๋วแล้ว เราขอความกรุณาคลิกที่ลิงก์ต่อไปนี้เพื่อแจ้งให้เราทราบ\n"
"เพื่อที่เราจะได้จัดส่งตั๋วให้กับบุคคลถัดไปในรายชื่อสำรองโดยเร็วที่สุด:\n"
"\n"
"{url_remove}\n"
"\n"
"ขอแสดงความนับถือ\n"
"ทีมงาน {event}"
#: pretix/base/settings.py
#, python-brace-format
msgid "Order canceled: {code}"
msgstr ""
msgstr "คำสั่งซื้อถูกยกเลิก: {code}"
#: pretix/base/settings.py
#, python-brace-format
@@ -11566,11 +11603,22 @@ msgid ""
"Best regards, \n"
"Your {event} team"
msgstr ""
"สวัสดี\n"
"\n"
"คำสั่งซื้อ {code} ของคุณสำหรับงาน {event} ถูกยกเลิกเรียบร้อยแล้ว\n"
"\n"
"{comment}\n"
"\n"
"คุณสามารถดูรายละเอียดคำสั่งซื้อของคุณได้ที่\n"
" {url}\n"
"\n"
"ขอแสดงความนับถือ\n"
"ทีมงาน {event}"
#: pretix/base/settings.py
#, python-brace-format
msgid "Order approved and awaiting payment: {code}"
msgstr ""
msgstr "คำสั่งซื้อได้รับการอนุมัติและกำลังรอการชำระเงิน: {code}"
#: pretix/base/settings.py
#, python-brace-format
@@ -11589,6 +11637,19 @@ msgid ""
"Best regards, \n"
"Your {event} team"
msgstr ""
"สวัสดี\n"
"\n"
"เราได้อนุมัติคำสั่งซื้อของคุณสำหรับงาน {event} แล้ว\n"
"และยินดีเป็นอย่างยิ่งที่จะได้ต้อนรับคุณในงานของเรา\n"
"\n"
"โปรดดำเนินการชำระเงินสำหรับคำสั่งซื้อของคุณก่อน {expire_date}\n"
"\n"
"คุณสามารถเลือกวิธีการชำระเงินและทำรายการชำระเงินได้ที่นี่:\n"
"\n"
"{url}\n"
"\n"
"ขอแสดงความนับถือ\n"
"ทีมงาน {event}"
#: pretix/base/settings.py
#, python-brace-format
@@ -11603,11 +11664,20 @@ msgid ""
"Best regards, \n"
"Your {event} team"
msgstr ""
"สวัสดี\n"
"\n"
"เราได้อนุมัติการสั่งซื้อตั๋วสำหรับคุณสำหรับงาน {event} แล้ว\n"
"\n"
"คุณสามารถดูรายละเอียดและสถานะตั๋วของคุณได้ที่นี่:\n"
"{url}\n"
"\n"
"ขอแสดงความนับถือ\n"
"ทีมงาน {event}"
#: pretix/base/settings.py
#, python-brace-format
msgid "Order approved and confirmed: {code}"
msgstr ""
msgstr "คำสั่งซื้อได้รับการอนุมัติและยืนยันแล้ว: {code}"
#: pretix/base/settings.py
#, python-brace-format
@@ -11623,11 +11693,22 @@ msgid ""
"Best regards, \n"
"Your {event} team"
msgstr ""
"สวัสดี\n"
"\n"
"เราได้อนุมัติคำสั่งซื้อของคุณสำหรับงาน {event} แล้ว และยินดีเป็นอย่างยิ่งที่จะได้ต้อนรับคุณในงานของเ"
"รา\n"
"เนื่องจากคุณสั่งซื้อเฉพาะรายการสินค้าฟรี จึงไม่มีค่าใช้จ่ายที่ต้องชำระ\n"
"\n"
"คุณสามารถแก้ไขรายละเอียดคำสั่งซื้อและดูสถานะคำสั่งซื้อของคุณได้ที่ \n"
"{url}\n"
"\n"
"ขอแสดงความนับถือ\n"
"ทีมงาน {event}"
#: pretix/base/settings.py
#, python-brace-format
msgid "Order denied: {code}"
msgstr ""
msgstr "คำสั่งซื้อถูกปฏิเสธ: {code}"
#: pretix/base/settings.py
#, python-brace-format
@@ -11645,6 +11726,18 @@ msgid ""
"Best regards, \n"
"Your {event} team"
msgstr ""
"สวัสดี\n"
"\n"
"ขออภัยเป็นอย่างยิ่ง เราได้ปฏิเสธคำขอสั่งซื้อของคุณสำหรับงาน {event}\n"
"\n"
"{comment}\n"
"\n"
"คุณสามารถดูรายละเอียดคำสั่งซื้อของคุณได้ที่นี่:\n"
"\n"
"{url}\n"
"\n"
"ขอแสดงความนับถือ\n"
"ทีมงาน {event}"
#: pretix/base/settings.py
#, python-brace-format
@@ -11657,11 +11750,18 @@ msgid ""
"Best regards, \n"
"Your {event} team"
msgstr ""
"สวัสดีค่ะ/ครับ\n"
"\n"
"คุณสามารถแก้ไขรายละเอียดคำสั่งซื้อและดูสถานะคำสั่งซื้อของคุณได้ที่\n"
"{url}\n"
"\n"
"ขอแสดงความนับถือ\n"
"ทีมงาน {event}"
#: pretix/base/settings.py
#, python-brace-format
msgid "Invoice {invoice_number}"
msgstr ""
msgstr "ใบกำกับภาษี {invoice_number}"
#: pretix/base/settings.py
#, python-brace-format
@@ -11675,6 +11775,14 @@ msgid ""
"\n"
"Your {event} team"
msgstr ""
"สวัสดี\n"
"\n"
"โปรดดูใบแจ้งหนี้/ใบกำกับภาษีใหม่ที่แนบมาพร้อมนี้สำหรับคำสั่งซื้อ {code} ของงาน {event} คำสั่งซื้อนี้"
"ถูกทำรายการโดย {order_email}\n"
"\n"
"ขอแสดงความนับถือ\n"
"\n"
"ทีมงาน {event}"
#: pretix/base/settings.py
#, python-brace-format
@@ -11871,6 +11979,21 @@ msgid ""
"\n"
"Your {organizer} team"
msgstr ""
"สวัสดี {name}\n"
"\n"
"มีการเปลี่ยนแปลงต่อไปนี้เกิดขึ้นกับบัญชีของคุณที่ {organizer}:\n"
"\n"
"{message}\n"
"\n"
"คุณสามารถตรวจสอบและเปลี่ยนแปลงการตั้งค่าบัญชีของคุณได้ที่นี่:\n"
"\n"
"{url}\n"
"\n"
"หากการเปลี่ยนแปลงนี้ไม่ได้ดำเนินการโดยคุณ โปรดติดต่อเราโดยทันที\n"
"\n"
"ขอแสดงความนับถือ\n"
"\n"
"ทีมงาน {organizer}"
#: pretix/base/settings.py
msgid "Please enter the hexadecimal code of a color, e.g. #990000."
@@ -13057,95 +13180,96 @@ msgstr ""
#: pretix/base/timeline.py
msgctxt "timeline"
msgid "Customers can no longer modify their order information"
msgstr ""
msgstr "ลูกค้าไม่สามารถแก้ไขข้อมูลคำสั่งซื้อของตนเองได้อีกต่อไป"
#: pretix/base/timeline.py
msgctxt "timeline"
msgid "No more payments can be completed"
msgstr ""
msgstr "ไม่สามารถทำรายการชำระเงินเพิ่มเติมได้อีกต่อไป"
#: pretix/base/timeline.py
msgctxt "timeline"
msgid "Tickets can be downloaded"
msgstr ""
msgstr "สามารถดาวน์โหลดตั๋วได้แล้ว"
#: pretix/base/timeline.py
msgctxt "timeline"
msgid "Customers can no longer cancel free or unpaid orders"
msgstr ""
"ลูกค้าไม่สามารถยกเลิกคำสั่งซื้อฟรีหรือรายการที่ยังไม่ได้ชำระเงินได้อีกต่อไป"
#: pretix/base/timeline.py
msgctxt "timeline"
msgid "Customers can no longer cancel paid orders"
msgstr ""
msgstr "ลูกค้าไม่สามารถยกเลิกคำสั่งซื้อที่ชำระเงินแล้วได้อีกต่อไป"
#: pretix/base/timeline.py
msgctxt "timeline"
msgid "Customers can no longer make changes to their orders"
msgstr ""
msgstr "ลูกค้าไม่สามารถเปลี่ยนแปลงรายการในคำสั่งซื้อได้อีกต่อไป"
#: pretix/base/timeline.py
msgctxt "timeline"
msgid "Waiting list is disabled"
msgstr ""
msgstr "รายชื่อสำรอง (Waiting list) ถูกปิดใช้งาน"
#: pretix/base/timeline.py
msgctxt "timeline"
msgid "Download reminders are being sent out"
msgstr ""
msgstr "กำลังจัดส่งอีเมลแจ้งเตือนให้ดาวน์โหลดตั๋ว"
#: pretix/base/timeline.py
#, python-brace-format
msgctxt "timeline"
msgid "Product \"{name}\" becomes available"
msgstr ""
msgstr "สินค้า \"{name}\" เริ่มเปิดให้สั่งซื้อ"
#: pretix/base/timeline.py
#, python-brace-format
msgctxt "timeline"
msgid "Product \"{name}\" becomes unavailable"
msgstr ""
msgstr "สินค้า \"{name}\" ปิดการสั่งซื้อแล้ว"
#: pretix/base/timeline.py
#, python-brace-format
msgctxt "timeline"
msgid "Discount \"{name}\" becomes active"
msgstr ""
msgstr "ส่วนลด \"{name}\" เริ่มมีผลใช้งาน"
#: pretix/base/timeline.py
#, python-brace-format
msgctxt "timeline"
msgid "Discount \"{name}\" becomes inactive"
msgstr ""
msgstr "ส่วนลด \"{name}\" หมดผลใช้งานแล้ว"
#: pretix/base/timeline.py
#, python-brace-format
msgctxt "timeline"
msgid "Product variation \"{product} – {variation}\" becomes available"
msgstr ""
msgstr "แบบสินค้าย่อย \"{product} – {variation}\" เริ่มเปิดให้สั่งซื้อ"
#: pretix/base/timeline.py
#, python-brace-format
msgctxt "timeline"
msgid "Product variation \"{product} – {variation}\" becomes unavailable"
msgstr ""
msgstr "แบบสินค้าย่อย \"{product} – {variation}\" ปิดการสั่งซื้อแล้ว"
#: pretix/base/timeline.py
#, python-brace-format
msgctxt "timeline"
msgid "Payment provider \"{name}\" becomes active"
msgstr ""
msgstr "ผู้ให้บริการชำระเงิน \"{name}\" เปิดใช้งานแล้ว"
#: pretix/base/timeline.py
#, python-brace-format
msgctxt "timeline"
msgid "Payment provider \"{name}\" can no longer be selected"
msgstr ""
msgstr "ไม่สามารถเลือกผู้ให้บริการชำระเงิน \"{name}\" ได้อีกต่อไป"
#: pretix/base/validators.py
#, python-format
msgid "This field has an invalid value: %(value)s."
msgstr ""
msgstr "ฟิลด์นี้มีค่าที่ไม่ถูกต้อง: %(value)s"
#: pretix/base/validators.py
#, python-format
@@ -13153,6 +13277,7 @@ msgid ""
"You entered an URL, which is not allowed. Please remove %(match)s from your "
"input."
msgstr ""
"คุณกรอก URL ซึ่งเป็นค่าที่ไม่ได้รับอนุญาต โปรดลบ %(match)s ออกจากการกรอกข้อมูลของคุณ"
#: pretix/base/views/errors.py
msgid ""
@@ -13161,6 +13286,9 @@ msgid ""
"required for security reasons, to ensure that your browser is not being "
"hijacked by third parties."
msgstr ""
"คุณเห็นข้อความนี้เนื่องจากเว็บไซต์ HTTPS นี้ต้องการให้เว็บเบราว์เซอร์ของคุณส่ง 'Referer "
"header' แต่ไม่มีการส่งข้อมูลดังกล่าว ส่วนหัว (header) นี้จำเป็นด้วยเหตุผลด้านความปลอดภัย เพื่อให้"
"แน่ใจว่าเบราว์เซอร์ของคุณจะไม่ถูกดักจับหรือจู่โจมโดยบุคคลภายนอก"
#: pretix/base/views/errors.py
msgid ""
@@ -13168,6 +13296,8 @@ msgid ""
"enable them, at least for this site, or for HTTPS connections, or for 'same-"
"origin' requests."
msgstr ""
"หากคุณได้ตั้งค่าเบราว์เซอร์เพื่อปิดใช้งาน 'Referer' headers โปรดเปิดใช้งานอีกครั้ง อย่างน้อยสำห"
"รับเว็บไซต์นี้ หรือสำหรับการเชื่อมต่อ HTTPS หรือสำหรับคำขอประเภท 'same-origin'"
#: pretix/base/views/errors.py
msgid ""
@@ -13175,63 +13305,67 @@ msgid ""
"submitting forms. This cookie is required for security reasons, to ensure "
"that your browser is not being hijacked by third parties."
msgstr ""
"คุณเห็นข้อความนี้เนื่องจากเว็บไซต์นี้จำเป็นต้องใช้คุกกี้ CSRF เมื่อส่งแบบฟอร์ม คุกกี้นี้จำเป็นด้วยเหตุผลด้า"
"นความปลอดภัย เพื่อให้แน่ใจว่าเบราว์เซอร์ของคุณจะไม่ถูกดักจับหรือจู่โจมโดยบุคคลภายนอก"
#: pretix/base/views/errors.py
msgid ""
"If you have configured your browser to disable cookies, please re-enable "
"them, at least for this site, or for 'same-origin' requests."
msgstr ""
"หากคุณได้ตั้งค่าเบราว์เซอร์เพื่อปิดใช้งานคุกกี้ โปรดเปิดใช้งานอีกครั้ง อย่างน้อยสำหรับเว็บไซต์นี้ หรือสำ"
"หรับคำขอประเภท 'same-origin'"
#. Translators: Only translate to French (IDE) and Italien (IDI), otherwise keep the same
#: pretix/base/views/js_helpers.py
msgctxt "tax_id_swiss"
msgid "UID"
msgstr ""
msgstr "UID"
#. Translators: Translate to only "P.IVA" in Italian, keep second part as-is in other languages
#: pretix/base/views/js_helpers.py
msgctxt "tax_id_italy"
msgid "VAT ID / P.IVA"
msgstr ""
msgstr "VAT ID / P.IVA"
#. Translators: Translate to only "ΑΦΜ" in Greek
#: pretix/base/views/js_helpers.py
msgctxt "tax_id_greece"
msgid "VAT ID / TIN"
msgstr ""
msgstr "VAT ID / TIN"
#. Translators: Translate to only "NIF" in Spanish
#: pretix/base/views/js_helpers.py
msgctxt "tax_id_spain"
msgid "VAT ID / NIF"
msgstr ""
msgstr "VAT ID / NIF"
#. Translators: Translate to only "NIF" in Portuguese
#: pretix/base/views/js_helpers.py
msgctxt "tax_id_portugal"
msgid "VAT ID / NIF"
msgstr ""
msgstr "VAT ID / NIF"
#: pretix/base/views/tasks.py
msgid "An unexpected error has occurred, please try again later."
msgstr ""
msgstr "เกิดข้อผิดพลาดที่ไม่คาดคิด โปรดลองใหม่อีกครั้งในภายหลัง"
#: pretix/base/views/tasks.py
msgid "The task has been completed."
msgstr ""
msgstr "ดำเนินการเสร็จสมบูรณ์แล้ว"
#: pretix/control/forms/__init__.py
#, python-brace-format
msgid "Please do not upload files larger than {size}!"
msgstr ""
msgstr "โปรดอย่าอัปโหลดไฟล์ที่มีขนาดใหญ่กว่า {size}!"
#: pretix/control/forms/__init__.py
msgid "Filetype not allowed!"
msgstr ""
msgstr "ประเภทไฟล์ไม่ได้รับอนุญาต!"
#: pretix/control/forms/__init__.py
msgid "Community translations"
msgstr ""
msgstr "คำแปลภาษาจากชุมชนผู้ใช้งาน"
#: pretix/control/forms/__init__.py
#, python-brace-format
@@ -13241,16 +13375,21 @@ msgid ""
"translated and will show in English instead. You can <a "
"href=\"{translate_url}\" target=\"_blank\">help translating</a>."
msgstr ""
"คำแปลเหล่านี้ไม่ได้ดูแลโดยทีมงาน pretix เราไม่สามารถรับประกันความถูกต้องได้ และคุณสมบัติใหม่หรื"
"อที่เพิ่งเปลี่ยนแปลงอาจยังไม่ได้แปลและจะแสดงเป็นภาษาอังกฤษแทน คุณสามารถ <a href=\""
"{translate_url}\" target=\"_blank\">ช่วยแปลภาษาได้</a>"
#: pretix/control/forms/__init__.py
msgid "Development only"
msgstr ""
msgstr "สำหรับสภาพแวดล้อมการพัฒนาเท่านั้น"
#: pretix/control/forms/__init__.py
msgid ""
"These translations are still in progress. These languages can currently only "
"be selected on development installations of pretix, not in production."
msgstr ""
"คำแปลเหล่านี้ยังอยู่ในระหว่างดำเนินการ ปัจจุบันสามารถเลือกภาษาเหล่านี้ได้เฉพาะในระบบ pretix ส"
"ำหรับการพัฒนาเท่านั้น ไม่สามารถใช้ในระบบใช้งานจริง (Production) ได้"
#: pretix/control/forms/checkin.py
msgid ""
@@ -448,6 +448,7 @@ var editor = {
var d = data[i], o
editor._add_from_data(d)
}
editor.fabric.discardActiveObject()
editor.fabric.renderAll()
editor._update_toolbox_values()
},
@@ -956,6 +957,7 @@ var editor = {
mtr: true
})
editor.fabric.add(text)
editor.fabric.setActiveObject(text)
editor._create_savepoint()
return text
},
@@ -972,6 +974,7 @@ var editor = {
rect.scaleToHeight(126)
rect.setControlsVisibility({ mtr: false, mb: false, mt: false, mr: false, ml: false })
editor.fabric.add(rect)
editor.fabric.setActiveObject(rect)
editor._create_savepoint()
return rect
},
@@ -1009,6 +1012,7 @@ var editor = {
mtr: true
})
editor.fabric.add(rect)
editor.fabric.setActiveObject(rect)
editor._create_savepoint()
$('#version-notice').show()
return rect
@@ -1026,6 +1030,7 @@ var editor = {
})
rect.setControlsVisibility({ mtr: false })
editor.fabric.add(rect)
editor.fabric.setActiveObject(rect)
editor._create_savepoint()
return rect
},
@@ -1044,6 +1049,7 @@ var editor = {
})
rect.setControlsVisibility({ mtr: false, mb: false, mt: false, mr: false, ml: false })
editor.fabric.add(rect)
editor.fabric.setActiveObject(rect)
editor._create_savepoint()
return rect
},
@@ -114,7 +114,7 @@ td(
)
.pretix-widget-event-calendar-day(v-if="day", :aria-label="dateStr") {{ daynum }}
.pretix-widget-event-calendar-events(v-if="day")
EventCalendarEvent(v-for="e in day.events", :key="e.event_url", :event="e")
EventCalendarEvent(v-for="e in day.events", :key="e.event_url+'-'+(e.subevent||'')", :event="e")
</template>
<style lang="sass">
</style>
+229
View File
@@ -0,0 +1,229 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#
import pytest
from django_scopes import scopes_disabled
from i18nfield.strings import LazyI18nString
@pytest.fixture
def event_meta_property(organizer):
return organizer.meta_properties.create(
name="Color",
default="Red",
required=False,
choices=[
{
"key": "Red",
"label": LazyI18nString("Rot"),
"DELETE": False,
"ORDER": 1,
}
],
)
TEST_TYPE_RES = {
"name": "Color",
"default": "Red",
"required": False,
"choices": [{"key": "Red", "label": {"en": "Rot"}}],
'filter_allowed': True,
'filter_public': False,
'protected': False,
'public_label': None,
}
@pytest.mark.django_db
def test_meta_property_list(token_client, organizer, event_meta_property):
res = dict(TEST_TYPE_RES)
resp = token_client.get('/api/v1/organizers/{}/event_meta_properties/'.format(organizer.slug))
assert resp.status_code == 200
event_meta_property.refresh_from_db()
res["id"] = event_meta_property.pk
assert res in resp.data['results']
assert len(resp.data['results']) == 1
@pytest.mark.django_db
def test_meta_property_detail(token_client, organizer, event_meta_property):
res = TEST_TYPE_RES
resp = token_client.get('/api/v1/organizers/{}/event_meta_properties/{}/'.format(organizer.slug, event_meta_property.pk))
assert resp.status_code == 200
event_meta_property.refresh_from_db()
res["id"] = event_meta_property.pk
assert res == resp.data
@pytest.mark.django_db
def test_meta_property_create(token_client, organizer):
url = '/api/v1/organizers/{}/event_meta_properties/'.format(organizer.slug)
resp = token_client.post(
url,
format='json',
data={
"name": "Color",
"default": "",
"required": False,
"choices": [
{"key": {"foo": "bar"}},
"blabla",
{"label": "Green"},
{"key": "g", "label": "Green", "foo": "bar"},
],
}
)
assert resp.status_code == 400
assert str(resp.data["choices"][0][0]) == "Meta property value options must have a key of type string."
assert str(resp.data["choices"][1][0]) == "Meta property value options must be a dict."
assert str(resp.data["choices"][2][0]) == "Meta property value options must have a key of type string."
assert str(resp.data["choices"][3][0]) == "Meta property value options may only have a key and optionally a label."
resp = token_client.post(
url,
format='json',
data={
"name": "Color",
"default": "Red",
"required": False,
"choices": {"key": "r", "label": "Red"},
}
)
assert resp.status_code == 400
assert str(resp.data["choices"][0]) == 'Expected a list of items but got type "dict".'
resp = token_client.post(
url,
format='json',
data={
"name": "Color",
"default": "r",
"required": False,
"choices": [
{"key": "r", "label": {"en": "Red"}},
{"key": "r", "label": {"en": "Razzmatazz"}},
],
}
)
assert resp.status_code == 400
assert str(resp.data["choices"][0]) == "The key for each meta property value option must be unique."
choices = [
{"key": "r", "label": "Red"},
{"key": "g", "label": "Green"},
{"key": "b", "label": "Blue"},
]
resp = token_client.post(
url,
format='json',
data={
"name": "Color",
"default": "k",
"required": False,
"choices": choices,
}
)
assert resp.status_code == 400
assert str(resp.data["non_field_errors"][0]) == "You cannot set a default value that is not a valid value."
resp = token_client.post(
url,
format='json',
data={
"name": "Color",
"default": "r",
"required": False,
"choices": choices,
}
)
assert resp.status_code == 201
with scopes_disabled():
event_meta_property = organizer.meta_properties.get(id=resp.data['id'])
assert event_meta_property.name == "Color"
assert event_meta_property.default == "r"
assert event_meta_property.choices == choices
assert not event_meta_property.required
assert len(organizer.meta_properties.all()) == 1
@pytest.mark.django_db
def test_meta_property_patch(token_client, organizer, event_meta_property):
url = '/api/v1/organizers/{}/event_meta_properties/{}/'.format(organizer.slug, event_meta_property.pk)
resp = token_client.patch(
url,
format='json',
data={
# existing default is not in choices
"choices": [{'key': 'k', 'label': 'Black'}],
}
)
assert resp.status_code == 400
assert str(resp.data["non_field_errors"][0]) == "You cannot set a default value that is not a valid value."
resp = token_client.patch(
"/api/v1/organizers/{}/event_meta_properties/{}/"
.format(organizer.slug, event_meta_property.pk),
format="json",
data={
"choices": [
{"key": "r", "label": ["wrong"]},
{"key": "g", "label": {"de": {"de": 123, "en": "wrong"}}}
],
}
)
assert resp.status_code == 400
assert str(resp.data["choices"][0]["label"][0]) == "Invalid data type."
assert str(resp.data["choices"][1]["label"][0]) == "All entries must be strings."
resp = token_client.patch(
url,
format='json',
data={
"choices": [],
}
)
assert resp.status_code == 200
event_meta_property.refresh_from_db()
assert event_meta_property.choices is None
resp = token_client.patch(
url,
format='json',
data={
"required": True,
"choices": None,
}
)
assert resp.status_code == 200
event_meta_property.refresh_from_db()
assert event_meta_property.required
assert event_meta_property.choices is None
@pytest.mark.django_db
def test_meta_property_delete(token_client, organizer, event_meta_property):
resp = token_client.delete(
'/api/v1/organizers/{}/event_meta_properties/{}/'.format(organizer.slug, event_meta_property.pk),
)
assert resp.status_code == 204
assert len(organizer.meta_properties.all()) == 0
+2
View File
@@ -223,6 +223,8 @@ org_permission_sub_urls = [
('post', 'organizer.customers:write', 'customers/1/anonymize/', 404),
('put', 'organizer.customers:write', 'customers/1/', 404),
('delete', 'organizer.customers:write', 'customers/1/', 404),
('post', 'organizer.settings.general:write', 'event_meta_properties/', 400),
('patch', 'organizer.settings.general:write', 'event_meta_properties/0/', 404),
('get', 'organizer.customers:read', 'memberships/', 200),
('post', 'organizer.customers:write', 'memberships/', 400),
('get', 'organizer.customers:read', 'memberships/1/', 404),