mirror of
https://github.com/pretix/pretix.git
synced 2026-09-25 18:14:41 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09cdf05f73 | ||
|
|
15cd6c6f5e |
@@ -1,241 +0,0 @@
|
||||
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.
|
||||
@@ -12,7 +12,6 @@ at :ref:`plugin-docs`.
|
||||
organizers
|
||||
events
|
||||
subevents
|
||||
event_meta_properties
|
||||
taxrules
|
||||
categories
|
||||
items
|
||||
|
||||
@@ -28,7 +28,6 @@ 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
|
||||
|
||||
@@ -41,10 +40,9 @@ 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, EventMetaProperty, GiftCard, GiftCardAcceptance,
|
||||
GiftCardTransaction, Membership, MembershipType, OrderPosition, Organizer,
|
||||
ReusableMedium, SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite,
|
||||
User,
|
||||
Customer, Device, 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 (
|
||||
@@ -642,88 +640,3 @@ 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
|
||||
|
||||
@@ -68,7 +68,6 @@ 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)
|
||||
|
||||
@@ -44,16 +44,15 @@ from pretix.api.models import OAuthAccessToken
|
||||
from pretix.api.pagination import TotalOrderingFilter
|
||||
from pretix.api.serializers.organizer import (
|
||||
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
|
||||
EventMetaPropertiesSerializer, GiftCardSerializer,
|
||||
GiftCardTransactionSerializer, MembershipSerializer,
|
||||
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
|
||||
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
|
||||
SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer,
|
||||
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
Customer, Device, Event, EventMetaProperty, GiftCard, GiftCardTransaction,
|
||||
LogEntry, Membership, MembershipType, Organizer, SalesChannel, SeatingPlan,
|
||||
Team, TeamAPIToken, TeamInvite, User,
|
||||
Customer, Device, Event, 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,
|
||||
@@ -847,49 +846,3 @@ 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
|
||||
|
||||
@@ -42,7 +42,7 @@ from django.conf import settings
|
||||
from django.core.mail import get_connection
|
||||
from django.core.validators import MinLengthValidator, RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
@@ -312,6 +312,25 @@ class Organizer(LoggedModel):
|
||||
)
|
||||
i += 1
|
||||
|
||||
def get_users_with_permission(self, permission):
|
||||
"""
|
||||
Returns a queryset of users who have a specific permission to this organizer.
|
||||
|
||||
:return: Iterable of User
|
||||
"""
|
||||
from .auth import User
|
||||
|
||||
if permission:
|
||||
qs = Team.objects.with_organizer_permission(permission)
|
||||
else:
|
||||
qs = Team.objects.all()
|
||||
|
||||
team_with_perm = qs.filter(
|
||||
members__pk=OuterRef('pk'),
|
||||
organizer=self,
|
||||
)
|
||||
return User.objects.annotate(twp=Exists(team_with_perm)).filter(twp=True)
|
||||
|
||||
|
||||
def generate_invite_token():
|
||||
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
|
||||
@@ -35,12 +35,13 @@
|
||||
import logging
|
||||
from collections import OrderedDict, namedtuple
|
||||
from itertools import groupby
|
||||
from typing import Union
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
|
||||
from pretix.base.models import Event, LogEntry
|
||||
from pretix.base.models import Event, LogEntry, Organizer
|
||||
from pretix.base.signals import register_notification_types
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.urls import mainreverse_absolute
|
||||
@@ -57,7 +58,8 @@ class Notification:
|
||||
"""
|
||||
Represents a notification that is sent/shown to a user. A notification consists of:
|
||||
|
||||
* one ``event`` reference
|
||||
* one ``event`` reference (can be ``Ǹone``)
|
||||
* one ``organizer`` reference
|
||||
* one ``title`` text that is shown e.g. in the email subject or in a headline
|
||||
* optionally one ``detail`` text that may or may not be shown depending on the notification method
|
||||
* optionally one ``url`` that should be absolute and point to the context of an notification (e.g. an order)
|
||||
@@ -67,8 +69,10 @@ class Notification:
|
||||
each consisting of a button label and an absolute URL to point to.
|
||||
"""
|
||||
|
||||
def __init__(self, event: Event, title: str, detail: str=None, url: str=None):
|
||||
def __init__(self, event: Event, title: str, detail: str=None, url: str=None, organizer: Organizer=None):
|
||||
assert event or organizer
|
||||
self.title = title
|
||||
self.organizer = organizer or event.organizer
|
||||
self.event = event
|
||||
self.detail = detail
|
||||
self.url = url
|
||||
@@ -91,8 +95,16 @@ class Notification:
|
||||
|
||||
|
||||
class NotificationType:
|
||||
def __init__(self, event: Event = None):
|
||||
self.event = event
|
||||
def __init__(self, event_or_organizer: Union[Event, Organizer] = None):
|
||||
if isinstance(event_or_organizer, Event):
|
||||
self.event = event_or_organizer
|
||||
self.organizer = event_or_organizer.organizer
|
||||
elif isinstance(event_or_organizer, Organizer):
|
||||
self.event = None
|
||||
self.organizer = event_or_organizer
|
||||
else:
|
||||
self.event = None
|
||||
self.organizer = None
|
||||
|
||||
def __repr__(self):
|
||||
return '<NotificationType: {}>'.format(self.action_type)
|
||||
@@ -121,31 +133,39 @@ class NotificationType:
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def is_event_level(self) -> bool:
|
||||
"""
|
||||
Return `True` if this notification type can be configured per event (the default).
|
||||
"""
|
||||
return True
|
||||
|
||||
def build_notification(self, logentry: LogEntry) -> Notification:
|
||||
"""
|
||||
This is the main function that you should override. It is supposed to turn a log entry
|
||||
object into a notification object that can then be rendered e.g. into an email.
|
||||
"""
|
||||
return Notification(
|
||||
logentry.event,
|
||||
logentry.display()
|
||||
event=logentry.event,
|
||||
title=logentry.display(),
|
||||
organizer=logentry.organizer,
|
||||
)
|
||||
|
||||
|
||||
def get_all_notification_types(event=None):
|
||||
def get_all_notification_types(event_or_organizer=None):
|
||||
global _ALL_TYPES
|
||||
|
||||
if event is None and _ALL_TYPES:
|
||||
if event_or_organizer is None and _ALL_TYPES:
|
||||
return _ALL_TYPES
|
||||
|
||||
types = OrderedDict()
|
||||
for recv, ret in register_notification_types.send(event):
|
||||
for recv, ret in register_notification_types.send(event_or_organizer):
|
||||
if isinstance(ret, (list, tuple)):
|
||||
for r in ret:
|
||||
types[r.action_type] = r
|
||||
else:
|
||||
types[ret.action_type] = ret
|
||||
if event is None:
|
||||
if event_or_organizer is None:
|
||||
_ALL_TYPES = types
|
||||
return types
|
||||
|
||||
@@ -181,6 +201,7 @@ class ParametrizedOrderNotificationType(NotificationType):
|
||||
|
||||
n = Notification(
|
||||
event=logentry.event,
|
||||
organizer=logentry.organizer,
|
||||
title=self._title.format(order=order, event=logentry.event),
|
||||
url=order_url
|
||||
)
|
||||
|
||||
@@ -34,7 +34,9 @@ from pretix.base.models import (
|
||||
from pretix.base.notifications import Notification, get_all_notification_types
|
||||
from pretix.base.services.mail import mail_send_task
|
||||
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
|
||||
from pretix.base.signals import notification
|
||||
from pretix.base.signals import (
|
||||
event_notification_sent, organizer_notification_sent,
|
||||
)
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers.celery import get_task_priority
|
||||
from pretix.helpers.urls import mainreverse_absolute
|
||||
@@ -47,14 +49,14 @@ def notify(logentry_ids: list):
|
||||
logentry_ids = [logentry_ids]
|
||||
|
||||
qs = LogEntry.all.select_related(
|
||||
'event', 'event__organizer'
|
||||
'event', 'event__organizer', 'organizer'
|
||||
).order_by(
|
||||
'action_type', 'event_id',
|
||||
'action_type', 'event_id', 'organizer_id',
|
||||
).filter(id__in=logentry_ids)
|
||||
|
||||
_event, _at, notify_specific, notify_global = None, None, None, None
|
||||
_event, _organizer, _at, notify_specific, notify_global = None, None, None, None, None
|
||||
for logentry in qs:
|
||||
if not logentry.event:
|
||||
if not logentry.event and not logentry.organizer:
|
||||
break # Ignore, we only have event-related notifications right now
|
||||
|
||||
notification_type = logentry.notification_type
|
||||
@@ -62,25 +64,36 @@ def notify(logentry_ids: list):
|
||||
if not notification_type:
|
||||
break # No suitable plugin
|
||||
|
||||
if _event != logentry.event or _at != logentry.action_type or notify_global is None:
|
||||
if _event != logentry.event or _organizer != logentry.organizer or _at != logentry.action_type or notify_global is None:
|
||||
_event = logentry.event
|
||||
_organizer = logentry.organizer
|
||||
_at = logentry.action_type
|
||||
# All users that have the permission to get the notification
|
||||
users = logentry.event.get_users_with_permission(
|
||||
notification_type.required_permission
|
||||
).filter(notifications_send=True, is_active=True)
|
||||
|
||||
if logentry.event:
|
||||
# All users that have the permission to get the notification
|
||||
users = logentry.event.get_users_with_permission(
|
||||
notification_type.required_permission
|
||||
).filter(notifications_send=True, is_active=True)
|
||||
else:
|
||||
users = logentry.organizer.get_users_with_permission(
|
||||
notification_type.required_permission
|
||||
).filter(notifications_send=True, is_active=True)
|
||||
|
||||
if logentry.user:
|
||||
users = users.exclude(pk=logentry.user.pk)
|
||||
|
||||
# Get all notification settings, both specific to this event as well as global
|
||||
notify_specific = {
|
||||
(ns.user, ns.method): ns.enabled
|
||||
for ns in NotificationSetting.objects.filter(
|
||||
event=logentry.event,
|
||||
action_type=notification_type.action_type,
|
||||
user__pk__in=users.values_list('pk', flat=True)
|
||||
)
|
||||
}
|
||||
if logentry.event:
|
||||
notify_specific = {
|
||||
(ns.user, ns.method): ns.enabled
|
||||
for ns in NotificationSetting.objects.filter(
|
||||
event=logentry.event,
|
||||
action_type=notification_type.action_type,
|
||||
user__pk__in=users.values_list('pk', flat=True)
|
||||
)
|
||||
}
|
||||
else:
|
||||
notify_specific = {}
|
||||
notify_global = {
|
||||
(ns.user, ns.method): ns.enabled
|
||||
for ns in NotificationSetting.objects.filter(
|
||||
@@ -106,7 +119,10 @@ def notify(logentry_ids: list):
|
||||
priority=get_task_priority("notifications", logentry.organizer_id),
|
||||
)
|
||||
|
||||
notification.send(logentry.event, logentry_id=logentry.id, notification_type=notification_type.action_type)
|
||||
if logentry.event:
|
||||
event_notification_sent.send(logentry.event, logentry_id=logentry.id, notification_type=notification_type.action_type)
|
||||
elif logentry.organizer:
|
||||
organizer_notification_sent.send(logentry.organizer, logentry_id=logentry.id, notification_type=notification_type.action_type)
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, acks_late=True, max_retries=9, default_retry_delay=900)
|
||||
@@ -158,13 +174,19 @@ def send_notification_mail(notification: Notification, user: User):
|
||||
body_plain = tpl_plain.render(ctx)
|
||||
|
||||
guid = uuid.uuid4()
|
||||
settings_holder = notification.event or notification.organizer
|
||||
prefix = settings_holder.settings.mail_prefix
|
||||
if not prefix and notification.event:
|
||||
prefix = notification.event.slug.upper()
|
||||
elif notification.organizer:
|
||||
prefix = notification.organizer.name
|
||||
m = OutgoingMail.objects.create(
|
||||
guid=guid,
|
||||
user=user,
|
||||
to=[user.email],
|
||||
subject='[{}] {}: {}'.format(
|
||||
settings.PRETIX_INSTANCE_NAME,
|
||||
notification.event.settings.mail_prefix or notification.event.slug.upper(),
|
||||
prefix,
|
||||
notification.title
|
||||
),
|
||||
body_plain=body_plain,
|
||||
|
||||
@@ -605,15 +605,14 @@ subclass of pretix.base.ticketoutput.BaseTicketOutput
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
register_notification_types = EventPluginSignal()
|
||||
register_notification_types = GlobalSignal()
|
||||
"""
|
||||
This signal is sent out to get all known notification types. Receivers should return an
|
||||
instance of a subclass of pretix.base.notifications.NotificationType or a list of such
|
||||
instances.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event,
|
||||
however for this signal, the ``sender`` **may also be None** to allow creating the general
|
||||
notification settings!
|
||||
When called for actually sending notifications, ``sender`` will be the event or organizer,
|
||||
depending on context.
|
||||
"""
|
||||
|
||||
register_event_permission_groups = GlobalSignal()
|
||||
@@ -628,7 +627,7 @@ This signal is sent out to get all known permissions. Receivers should return an
|
||||
instance of pretix.base.permissions.PermissionGroup or a list of such instances.
|
||||
"""
|
||||
|
||||
notification = EventPluginSignal()
|
||||
event_notification_sent = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``logentry_id``, ``notification_type``
|
||||
|
||||
@@ -637,6 +636,15 @@ This signal is sent out when a notification is sent.
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
organizer_notification_sent = OrganizerPluginSignal()
|
||||
"""
|
||||
Arguments: ``logentry_id``, ``notification_type``
|
||||
|
||||
This signal is sent out when a notification is sent.
|
||||
|
||||
As with all organizer-plugin signals, the ``sender`` keyword argument will contain the organizer.
|
||||
"""
|
||||
|
||||
register_sales_channel_types = GlobalSignal()
|
||||
"""
|
||||
This signal is sent out to get all known sales channels types. Receivers should return an
|
||||
|
||||
@@ -717,10 +717,6 @@ 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="{% trans "Cancel" %}"/>
|
||||
<input type="submit" class="btn btn-large btn-primary" name="allow" value="{% trans "Authorize" %}"/>
|
||||
<input type="submit" class="btn btn-large btn-default" value="Cancel"/>
|
||||
<input type="submit" class="btn btn-large btn-primary" name="allow" value="Authorize"/>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
|
||||
@@ -761,6 +761,7 @@ class UserNotificationsEditView(TemplateView):
|
||||
{k: a.get(t) for k, a in self.global_set.items()},
|
||||
)
|
||||
for t, tv in self.types.items()
|
||||
if tv.is_event_level or not self.event
|
||||
]
|
||||
ctx['event'] = self.event
|
||||
if self.event:
|
||||
|
||||
@@ -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 18:00+0000\n"
|
||||
"PO-Revision-Date: 2026-09-23 09:38+0000\n"
|
||||
"Last-Translator: Nate Horst <nate@agcthailand.org>\n"
|
||||
"Language-Team: Thai <https://translate.pretix.eu/projects/pretix/pretix/th/>"
|
||||
"\n"
|
||||
@@ -11509,23 +11509,11 @@ 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 "คุณได้รับเลือกจากรายชื่อสำรอง (Waiting list) สำหรับงาน {event}"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
#, python-brace-format
|
||||
@@ -11557,36 +11545,11 @@ 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 "คำสั่งซื้อถูกยกเลิก: {code}"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
#, python-brace-format
|
||||
@@ -11603,22 +11566,11 @@ 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 "คำสั่งซื้อได้รับการอนุมัติและกำลังรอการชำระเงิน: {code}"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
#, python-brace-format
|
||||
@@ -11637,19 +11589,6 @@ 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
|
||||
@@ -11664,20 +11603,11 @@ 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 "คำสั่งซื้อได้รับการอนุมัติและยืนยันแล้ว: {code}"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
#, python-brace-format
|
||||
@@ -11693,22 +11623,11 @@ 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 "คำสั่งซื้อถูกปฏิเสธ: {code}"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
#, python-brace-format
|
||||
@@ -11726,18 +11645,6 @@ 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
|
||||
@@ -11750,18 +11657,11 @@ 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 "ใบกำกับภาษี {invoice_number}"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
#, python-brace-format
|
||||
@@ -11775,14 +11675,6 @@ msgid ""
|
||||
"\n"
|
||||
"Your {event} team"
|
||||
msgstr ""
|
||||
"สวัสดี\n"
|
||||
"\n"
|
||||
"โปรดดูใบแจ้งหนี้/ใบกำกับภาษีใหม่ที่แนบมาพร้อมนี้สำหรับคำสั่งซื้อ {code} ของงาน {event} คำสั่งซื้อนี้"
|
||||
"ถูกทำรายการโดย {order_email}\n"
|
||||
"\n"
|
||||
"ขอแสดงความนับถือ\n"
|
||||
"\n"
|
||||
"ทีมงาน {event}"
|
||||
|
||||
#: pretix/base/settings.py
|
||||
#, python-brace-format
|
||||
@@ -11979,21 +11871,6 @@ 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."
|
||||
@@ -13180,96 +13057,95 @@ 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 "รายชื่อสำรอง (Waiting list) ถูกปิดใช้งาน"
|
||||
msgstr ""
|
||||
|
||||
#: 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 "สินค้า \"{name}\" เริ่มเปิดให้สั่งซื้อ"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/timeline.py
|
||||
#, python-brace-format
|
||||
msgctxt "timeline"
|
||||
msgid "Product \"{name}\" becomes unavailable"
|
||||
msgstr "สินค้า \"{name}\" ปิดการสั่งซื้อแล้ว"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/timeline.py
|
||||
#, python-brace-format
|
||||
msgctxt "timeline"
|
||||
msgid "Discount \"{name}\" becomes active"
|
||||
msgstr "ส่วนลด \"{name}\" เริ่มมีผลใช้งาน"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/timeline.py
|
||||
#, python-brace-format
|
||||
msgctxt "timeline"
|
||||
msgid "Discount \"{name}\" becomes inactive"
|
||||
msgstr "ส่วนลด \"{name}\" หมดผลใช้งานแล้ว"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/timeline.py
|
||||
#, python-brace-format
|
||||
msgctxt "timeline"
|
||||
msgid "Product variation \"{product} – {variation}\" becomes available"
|
||||
msgstr "แบบสินค้าย่อย \"{product} – {variation}\" เริ่มเปิดให้สั่งซื้อ"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/timeline.py
|
||||
#, python-brace-format
|
||||
msgctxt "timeline"
|
||||
msgid "Product variation \"{product} – {variation}\" becomes unavailable"
|
||||
msgstr "แบบสินค้าย่อย \"{product} – {variation}\" ปิดการสั่งซื้อแล้ว"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/timeline.py
|
||||
#, python-brace-format
|
||||
msgctxt "timeline"
|
||||
msgid "Payment provider \"{name}\" becomes active"
|
||||
msgstr "ผู้ให้บริการชำระเงิน \"{name}\" เปิดใช้งานแล้ว"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/timeline.py
|
||||
#, python-brace-format
|
||||
msgctxt "timeline"
|
||||
msgid "Payment provider \"{name}\" can no longer be selected"
|
||||
msgstr "ไม่สามารถเลือกผู้ให้บริการชำระเงิน \"{name}\" ได้อีกต่อไป"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/validators.py
|
||||
#, python-format
|
||||
msgid "This field has an invalid value: %(value)s."
|
||||
msgstr "ฟิลด์นี้มีค่าที่ไม่ถูกต้อง: %(value)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/validators.py
|
||||
#, python-format
|
||||
@@ -13277,7 +13153,6 @@ 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 ""
|
||||
@@ -13286,9 +13161,6 @@ 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 ""
|
||||
@@ -13296,8 +13168,6 @@ 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 ""
|
||||
@@ -13305,67 +13175,63 @@ 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 "UID"
|
||||
msgstr ""
|
||||
|
||||
#. 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 "VAT ID / P.IVA"
|
||||
msgstr ""
|
||||
|
||||
#. Translators: Translate to only "ΑΦΜ" in Greek
|
||||
#: pretix/base/views/js_helpers.py
|
||||
msgctxt "tax_id_greece"
|
||||
msgid "VAT ID / TIN"
|
||||
msgstr "VAT ID / TIN"
|
||||
msgstr ""
|
||||
|
||||
#. Translators: Translate to only "NIF" in Spanish
|
||||
#: pretix/base/views/js_helpers.py
|
||||
msgctxt "tax_id_spain"
|
||||
msgid "VAT ID / NIF"
|
||||
msgstr "VAT ID / NIF"
|
||||
msgstr ""
|
||||
|
||||
#. Translators: Translate to only "NIF" in Portuguese
|
||||
#: pretix/base/views/js_helpers.py
|
||||
msgctxt "tax_id_portugal"
|
||||
msgid "VAT ID / NIF"
|
||||
msgstr "VAT ID / NIF"
|
||||
msgstr ""
|
||||
|
||||
#: 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 "โปรดอย่าอัปโหลดไฟล์ที่มีขนาดใหญ่กว่า {size}!"
|
||||
msgstr ""
|
||||
|
||||
#: 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
|
||||
@@ -13375,21 +13241,16 @@ 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 ""
|
||||
|
||||
@@ -109,30 +109,7 @@ let form_handlers = function (el) {
|
||||
}
|
||||
$(this).datetimepicker(opts)
|
||||
})
|
||||
el.find("button[data-wait-seconds-enable], input[data-wait-seconds-enable]").each(function(i, input) {
|
||||
var s = parseInt(input.getAttribute("data-wait-seconds-enable")) || 0;
|
||||
var time = $("time", input) || $("time").appendTo(input);
|
||||
// for a11y do not disable input, but do not allow submit
|
||||
function disable_submit(e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (s) {
|
||||
input.addEventListener("click", disable_submit);
|
||||
}
|
||||
function wait() {
|
||||
time.attr("datetime", s+"s");
|
||||
if (s > 0) {
|
||||
time.text("(" + s + "s)");
|
||||
window.setTimeout(wait, 1000);
|
||||
s--;
|
||||
} else {
|
||||
time.remove();
|
||||
input.disabled = false;
|
||||
input.removeEventListener("click", disable_submit);
|
||||
}
|
||||
}
|
||||
wait();
|
||||
});
|
||||
|
||||
el.find('.input-item-count-dec, .input-item-count-inc').on('click', function (e) {
|
||||
e.preventDefault()
|
||||
let step = parseFloat(this.getAttribute('data-step'))
|
||||
|
||||
@@ -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+'-'+(e.subevent||'')", :event="e")
|
||||
EventCalendarEvent(v-for="e in day.events", :key="e.event_url", :event="e")
|
||||
</template>
|
||||
<style lang="sass">
|
||||
</style>
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -223,8 +223,6 @@ 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),
|
||||
|
||||
@@ -219,8 +219,14 @@ def test_specific_organizer_permission(event, user):
|
||||
def test_organizer_permissions_multiple_teams(event, user):
|
||||
team1 = Team.objects.create(organizer=event.organizer, limit_organizer_permissions={"organizer.settings.general:write": True})
|
||||
team2 = Team.objects.create(organizer=event.organizer, limit_organizer_permissions={"organizer.events:create": True})
|
||||
assert set(event.organizer.get_users_with_permission('organizer.settings.general:write')) == set()
|
||||
assert set(event.organizer.get_users_with_permission(None)) == set()
|
||||
team1.members.add(user)
|
||||
assert set(event.organizer.get_users_with_permission('organizer.settings.general:write')) == {user}
|
||||
assert set(event.organizer.get_users_with_permission(None)) == {user}
|
||||
assert set(event.organizer.get_users_with_permission('organizer.events:create')) == set()
|
||||
team2.members.add(user)
|
||||
assert set(event.organizer.get_users_with_permission('organizer.events:create')) == {user}
|
||||
orga2 = Organizer.objects.create(slug='d2', name='d2')
|
||||
team3 = Team.objects.create(organizer=orga2, limit_organizer_permissions={"organizer.teams:write": True})
|
||||
team3.members.add(user)
|
||||
|
||||
Reference in New Issue
Block a user