mirror of
https://github.com/pretix/pretix.git
synced 2026-01-05 21:32:26 +00:00
Compare commits
17 Commits
api-expand
...
pdf-editor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c835e04edd | ||
|
|
e796dc3a65 | ||
|
|
545625b732 | ||
|
|
9bf302e5ae | ||
|
|
0c7c50cffc | ||
|
|
2c094f4c30 | ||
|
|
e820424bdf | ||
|
|
cb3d88a923 | ||
|
|
530ce06155 | ||
|
|
9017128513 | ||
|
|
5d3fc62ba4 | ||
|
|
243db008e1 | ||
|
|
5ea9f819e6 | ||
|
|
a5eb009e55 | ||
|
|
5129ed3846 | ||
|
|
f51906338f | ||
|
|
d67e1116f4 |
@@ -203,35 +203,9 @@ Query parameters
|
||||
Most list endpoints allow a filtering of the results using query parameters. In this case, booleans should be passed
|
||||
as the string values ``true`` and ``false``.
|
||||
|
||||
Ordering
|
||||
--------
|
||||
|
||||
If the ``ordering`` parameter is documented for a resource, you can use it to sort the result set by one of the allowed
|
||||
fields. Prepend a ``-`` to the field name to reverse the sort order.
|
||||
|
||||
Filtering and expanding fields
|
||||
------------------------------
|
||||
|
||||
On many endpoints, you can modify what fields are being returned:
|
||||
|
||||
- Using the ``include`` query parameter, you can chose which fields will be returned as part of the response.
|
||||
For example, if you pass ``include=code&include=email`` to the list of orders, you will receive a list of only
|
||||
order codes and email addresses.
|
||||
|
||||
- Using the ``exclude`` query parameter, you can chose which fields will not be returned as part of the response.
|
||||
For example, if you pass ``exclude=payments&exclude=refunds`` to the list of orders, you will receive a list
|
||||
without the payment and refund objects.
|
||||
|
||||
- Using the ``expand`` query parameter, you can chose which fields will be expanded into full objects. For example,
|
||||
if you pass ``expand=voucher`` to the list of order positions, the response will contain a full voucher object
|
||||
instead of just the ID. If you do not have permission to view vouchers, a 403 status code is returned.
|
||||
For performance reasons, this option is only available for a limited number of fields that are noted as
|
||||
"expandable" in the documentation of the respective object.
|
||||
|
||||
In all of these, you can use dotted notation to address fields of sub-objects, such as ``positions.checkins.gate``.
|
||||
|
||||
These options are not available everywhere as we are slowly rolling them out throughout the codebase. Please check
|
||||
the individual endpoint documentation for availability.
|
||||
|
||||
Idempotency
|
||||
-----------
|
||||
|
||||
@@ -152,8 +152,6 @@ Endpoints
|
||||
and ``null`` for "status unknown". These values might be served from a cache. This parameter can make the response
|
||||
slow.
|
||||
:query search: Only return events matching a given search query.
|
||||
:query string include: Limit the output to the given field. Can be passed multiple times.
|
||||
:query string exclude: Exclude a field from the output. Can be passed multiple times.
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
@@ -225,8 +223,6 @@ Endpoints
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:query string include: Limit the output to the given field. Can be passed multiple times.
|
||||
:query string exclude: Exclude a field from the output. Can be passed multiple times.
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
||||
|
||||
@@ -19,7 +19,7 @@ name multi-lingual string The item's vi
|
||||
internal_name string An optional name that is only used in the backend
|
||||
default_price money (string) The item price that is applied if the price is not
|
||||
overwritten by variations or other options.
|
||||
category integer (expandable) The ID of the category this item belongs to
|
||||
category integer The ID of the category this item belongs to
|
||||
(or ``null``).
|
||||
active boolean If ``false``, the item is hidden from all public lists
|
||||
and will not be sold.
|
||||
@@ -33,7 +33,7 @@ free_price_suggestion money (string) A suggested p
|
||||
``free_price`` is set (or ``null``).
|
||||
tax_rate decimal (string) The VAT rate to be applied for this item (read-only,
|
||||
set through ``tax_rule``).
|
||||
tax_rule integer (expandable) The internal ID of the applied tax rule (or ``null``).
|
||||
tax_rule integer The internal ID of the applied tax rule (or ``null``).
|
||||
admission boolean ``true`` for items that grant admission to the event
|
||||
(such as primary tickets) and ``false`` for others
|
||||
(such as add-ons or merchandise).
|
||||
@@ -390,9 +390,6 @@ Endpoints
|
||||
will be returned.
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
|
||||
Default: ``position``
|
||||
:query string include: Limit the output to the given field. Can be passed multiple times.
|
||||
:query string exclude: Exclude a field from the output. Can be passed multiple times.
|
||||
:query string expand: Expand an object reference with the referenced object. Can be passed multiple times.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
@@ -534,9 +531,6 @@ Endpoints
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param id: The ``id`` field of the item to fetch
|
||||
:query string include: Limit the output to the given field. Can be passed multiple times.
|
||||
:query string exclude: Exclude a field from the output. Can be passed multiple times.
|
||||
:query string expand: Expand an object reference with the referenced object. Can be passed multiple times.
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
@@ -157,8 +157,8 @@ order string Order code of t
|
||||
positionid integer Number of the position within the order
|
||||
canceled boolean Whether or not this position has been canceled. Note that
|
||||
by default, only non-canceled positions are shown.
|
||||
item integer (expandable) ID of the purchased item
|
||||
variation integer (expandable) ID of the purchased variation (or ``null``)
|
||||
item integer ID of the purchased item
|
||||
variation integer ID of the purchased variation (or ``null``)
|
||||
price money (string) Price of this position
|
||||
attendee_name string Specified attendee name for this position (or ``null``)
|
||||
attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name)
|
||||
@@ -170,7 +170,7 @@ city string Attendee city (
|
||||
country string Attendee country code (or ``null``)
|
||||
state string Attendee state (ISO 3166-2 code). Only supported in
|
||||
AU, BR, CA, CN, MY, MX, and US, otherwise ``null``.
|
||||
voucher integer (expandable) Internal ID of the voucher used for this position (or ``null``)
|
||||
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
||||
voucher_budget_use money (string) Amount of money discounted by the voucher, corresponding
|
||||
to how much of the ``budget`` of the voucher is consumed.
|
||||
**Important:** Do not rely on this amount to be a useful
|
||||
@@ -182,7 +182,7 @@ tax_code string Codified reason
|
||||
tax_rule integer The ID of the used tax rule (or ``null``)
|
||||
secret string Secret code printed on the tickets for validation
|
||||
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
|
||||
subevent integer (expandable) ID of the date inside an event series this position belongs to (or ``null``).
|
||||
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
|
||||
discount integer ID of a discount that has been used during the creation of this position in some way (or ``null``).
|
||||
blocked list of strings A list of strings, or ``null``. Whenever not ``null``, the ticket may not be used (e.g. for check-in).
|
||||
valid_from datetime The ticket will not be valid before this time. Can be ``null``.
|
||||
|
||||
@@ -61,8 +61,6 @@ Endpoints
|
||||
:query page: The page number in case of a multi-page result set, default is 1
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``slug`` and
|
||||
``name``. Default: ``slug``.
|
||||
:query string include: Limit the output to the given field. Can be passed multiple times.
|
||||
:query string exclude: Exclude a field from the output. Can be passed multiple times.
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
|
||||
@@ -93,8 +91,6 @@ Endpoints
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:query string include: Limit the output to the given field. Can be passed multiple times.
|
||||
:query string exclude: Exclude a field from the output. Can be passed multiple times.
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||
|
||||
@@ -146,70 +146,10 @@ Endpoints
|
||||
attribute with values of 100 for "tickets available", values less than 100 for "tickets sold out or reserved",
|
||||
and ``null`` for "status unknown". These values might be served from a cache. This parameter can make the response
|
||||
slow.
|
||||
:query string include: Limit the output to the given field. Can be passed multiple times.
|
||||
:query string exclude: Exclude a field from the output. Can be passed multiple times.
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
|
||||
|
||||
Returns information on one sub-event, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/subevents/1/ 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
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "First Sample Conference"},
|
||||
"event": "sampleconf",
|
||||
"active": false,
|
||||
"is_public": true,
|
||||
"date_from": "2017-12-27T10:00:00Z",
|
||||
"date_to": null,
|
||||
"date_admission": null,
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
"disabled": false,
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"price": "12.00"
|
||||
}
|
||||
],
|
||||
"variation_price_overrides": [],
|
||||
"meta_data": {}
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the main event
|
||||
:param id: The ``id`` field of the sub-event to fetch
|
||||
:query string include: Limit the output to the given field. Can be passed multiple times.
|
||||
:query string exclude: Exclude a field from the output. Can be passed multiple times.
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/
|
||||
|
||||
Creates a new subevent.
|
||||
@@ -297,6 +237,63 @@ Endpoints
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
|
||||
|
||||
Returns information on one sub-event, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/subevents/1/ 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
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "First Sample Conference"},
|
||||
"event": "sampleconf",
|
||||
"active": false,
|
||||
"is_public": true,
|
||||
"date_from": "2017-12-27T10:00:00Z",
|
||||
"date_to": null,
|
||||
"date_admission": null,
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
"disabled": false,
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"price": "12.00"
|
||||
}
|
||||
],
|
||||
"variation_price_overrides": [],
|
||||
"meta_data": {}
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the main event
|
||||
:param id: The ``id`` field of the sub-event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
|
||||
|
||||
Updates a sub-event, identified by its ID. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to
|
||||
|
||||
@@ -33,7 +33,7 @@ dependencies = [
|
||||
"celery==5.5.*",
|
||||
"chardet==5.2.*",
|
||||
"cryptography>=44.0.0",
|
||||
"css-inline==0.14.*",
|
||||
"css-inline==0.15.*",
|
||||
"defusedcsv>=1.1.0",
|
||||
"Django[argon2]==4.2.*,>=4.2.15",
|
||||
"django-bootstrap3==25.1",
|
||||
@@ -64,7 +64,7 @@ dependencies = [
|
||||
"kombu==5.5.*",
|
||||
"libsass==0.23.*",
|
||||
"lxml",
|
||||
"markdown==3.8", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||
"markdown==3.8.2", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
||||
"mt-940==4.30.*",
|
||||
"oauthlib==3.3.*",
|
||||
@@ -91,7 +91,7 @@ dependencies = [
|
||||
"redis==6.2.*",
|
||||
"reportlab==4.4.*",
|
||||
"requests==2.31.*",
|
||||
"sentry-sdk==2.30.*",
|
||||
"sentry-sdk==2.31.*",
|
||||
"sepaxml==2.6.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
@@ -100,7 +100,7 @@ dependencies = [
|
||||
"ua-parser==1.0.*",
|
||||
"vat_moss_forked==2020.3.20.0.11.0",
|
||||
"vobject==0.9.*",
|
||||
"webauthn==2.5.*",
|
||||
"webauthn==2.6.*",
|
||||
"zeep==4.3.*"
|
||||
]
|
||||
|
||||
@@ -111,7 +111,7 @@ dev = [
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.30.*",
|
||||
"flake8==7.2.*",
|
||||
"flake8==7.3.*",
|
||||
"freezegun",
|
||||
"isort==6.0.*",
|
||||
"pep8-naming==0.15.*",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.17 on 2025-06-24 14:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixapi", "0012_oauthapplication_post_logout_redirect_uris"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="webhookcallretry",
|
||||
name="retry_not_before",
|
||||
field=models.DateTimeField(),
|
||||
),
|
||||
]
|
||||
@@ -157,7 +157,7 @@ class WebHookCallRetry(models.Model):
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='retries')
|
||||
logentry = models.ForeignKey('pretixbase.LogEntry', on_delete=models.CASCADE, related_name='webhook_retries')
|
||||
retry_not_before = models.DateTimeField(auto_now_add=True)
|
||||
retry_not_before = models.DateTimeField()
|
||||
retry_count = models.PositiveIntegerField(default=0)
|
||||
action_type = models.CharField(max_length=255)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import json
|
||||
|
||||
from django.db.models import prefetch_related_objects
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
|
||||
class AsymmetricField(serializers.Field):
|
||||
@@ -132,136 +132,6 @@ class SalesChannelMigrationMixin:
|
||||
s.identifier for s in
|
||||
self.organizer.sales_channels.all()
|
||||
])
|
||||
elif "limit_sales_channels" in value:
|
||||
else:
|
||||
value["sales_channels"] = value["limit_sales_channels"]
|
||||
return value
|
||||
|
||||
|
||||
class ConfigurableSerializerMixin:
|
||||
expand_fields = {}
|
||||
|
||||
def get_exclude_requests(self):
|
||||
if hasattr(self, "initial_data"):
|
||||
# Do not support include requests when the serializer is used for writing
|
||||
# TODO: think about this
|
||||
return set()
|
||||
if getattr(self, "parent", None):
|
||||
# Field selection is always handled by top-level serializer
|
||||
return set()
|
||||
if 'exclude' in self.context:
|
||||
return self.context['exclude']
|
||||
elif 'request' in self.context:
|
||||
return self.context['request'].query_params.getlist('exclude')
|
||||
raise TypeError("Could not discover list of fields to exclude")
|
||||
|
||||
def get_include_requests(self):
|
||||
if hasattr(self, "initial_data"):
|
||||
# Do not support include requests when the serializer is used for writing
|
||||
# TODO: think about this
|
||||
return set()
|
||||
if getattr(self, "parent", None):
|
||||
# Field selection is always handled by top-level serializer
|
||||
return set()
|
||||
if 'include' in self.context:
|
||||
return self.context['include']
|
||||
elif 'request' in self.context:
|
||||
return self.context['request'].query_params.getlist('include')
|
||||
raise TypeError("Could not discover list of fields to include")
|
||||
|
||||
def get_expand_requests(self):
|
||||
if hasattr(self, "initial_data"):
|
||||
# Do not support expand requests when the serializer is used for writing
|
||||
# TODO: think about this
|
||||
return set()
|
||||
if getattr(self, "parent", None):
|
||||
# Field selection is always handled by top-level serializer
|
||||
return set()
|
||||
if 'expand' in self.context:
|
||||
return self.context['expand']
|
||||
elif 'request' in self.context:
|
||||
return self.context['request'].query_params.getlist('expand')
|
||||
raise TypeError("Could not discover list of fields to expand")
|
||||
|
||||
def _exclude_field(self, serializer, path):
|
||||
if path[0] not in serializer.fields:
|
||||
return # field does not exist, nothing to do
|
||||
|
||||
if len(path) == 1:
|
||||
del serializer.fields[path[0]]
|
||||
elif len(path) >= 2 and hasattr(serializer.fields[path[0]], "child"):
|
||||
self._exclude_field(serializer.fields[path[0]].child, path[1:])
|
||||
elif len(path) >= 2 and isinstance(serializer.fields[path[0]], serializers.Serializer):
|
||||
self._exclude_field(serializer.fields[path[0]], path[1:])
|
||||
|
||||
def _filter_fields_to_included(self, serializer, includes):
|
||||
any_field_remaining = False
|
||||
for fname, field in list(serializer.fields.items()):
|
||||
if fname in includes:
|
||||
any_field_remaining = True
|
||||
continue
|
||||
elif hasattr(field, 'child'): # Nested list serializers
|
||||
child_includes = {i.removeprefix(f'{fname}.') for i in includes if i.startswith(f'{fname}.')}
|
||||
if child_includes and self._filter_fields_to_included(field.child, child_includes):
|
||||
any_field_remaining = True
|
||||
continue
|
||||
serializer.fields.pop(fname)
|
||||
elif isinstance(field, serializers.Serializer): # Nested serializers
|
||||
child_includes = {i.removeprefix(f'{fname}.') for i in includes if i.startswith(f'{fname}.')}
|
||||
if child_includes and self._filter_fields_to_included(field, child_includes):
|
||||
any_field_remaining = True
|
||||
continue
|
||||
serializer.fields.pop(fname)
|
||||
else:
|
||||
serializer.fields.pop(fname)
|
||||
return any_field_remaining
|
||||
|
||||
def _expand_field(self, serializer, path, original_field):
|
||||
if path[0] not in serializer.fields or not self.is_field_expandable(original_field):
|
||||
return False # field does not exist, nothing to do
|
||||
|
||||
if len(path) == 1:
|
||||
serializer.fields[path[0]] = self.get_expand_serializer(original_field)
|
||||
return True
|
||||
elif len(path) >= 2 and hasattr(serializer.fields[path[0]], "child"):
|
||||
return self._expand_field(serializer.fields[path[0]].child, path[1:], original_field)
|
||||
elif len(path) >= 2 and isinstance(serializer.fields[path[0]], serializers.Serializer):
|
||||
return self._expand_field(serializer.fields[path[0]], path[1:], original_field)
|
||||
|
||||
def is_field_expandable(self, field):
|
||||
return field in self.expand_fields
|
||||
|
||||
def get_expand_serializer(self, field):
|
||||
from pretix.base.models import Device, TeamAPIToken
|
||||
|
||||
ef = self.expand_fields[field]
|
||||
if "permission" in ef:
|
||||
request = self.context["request"]
|
||||
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
|
||||
if not perm_holder.has_event_permission(request.organizer, request.event, ef["permission"], request=request):
|
||||
raise PermissionDenied(f"No permission to expand field {field}")
|
||||
|
||||
if hasattr(self, "instance") and "prefetch" in ef:
|
||||
for prefetch in ef["prefetch"]:
|
||||
prefetch_related_objects(
|
||||
self.instance if hasattr(self.instance, '__iter__') else [self.instance],
|
||||
prefetch
|
||||
)
|
||||
|
||||
return ef["serializer"](
|
||||
read_only=True,
|
||||
context=self.context,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
expanded = False
|
||||
for expand in sorted(list(self.get_expand_requests())):
|
||||
expanded = self._expand_field(self, expand.split('.'), expand) or expanded
|
||||
|
||||
includes = set(self.get_include_requests())
|
||||
if includes:
|
||||
self._filter_fields_to_included(self, includes)
|
||||
|
||||
for exclude_field in self.get_exclude_requests():
|
||||
self._exclude_field(self, exclude_field.split('.'))
|
||||
|
||||
@@ -23,19 +23,15 @@ from django.utils.translation import gettext as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers import ConfigurableSerializerMixin
|
||||
from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import Checkin, CheckinList
|
||||
|
||||
|
||||
class CheckinListSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||
class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
checkin_count = serializers.IntegerField(read_only=True)
|
||||
position_count = serializers.IntegerField(read_only=True)
|
||||
expand_fields = {
|
||||
"subevent": SubEventSerializer,
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
@@ -46,6 +42,17 @@ class CheckinListSerializer(ConfigurableSerializerMixin, I18nAwareModelSerialize
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
||||
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
||||
|
||||
for exclude_field in self.context['request'].query_params.getlist('exclude'):
|
||||
p = exclude_field.split('.')
|
||||
if p[0] in self.fields:
|
||||
if len(p) == 1:
|
||||
del self.fields[p[0]]
|
||||
elif len(p) == 2:
|
||||
self.fields[p[0]].child.fields.pop(p[1])
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
event = self.context['event']
|
||||
|
||||
@@ -48,8 +48,7 @@ from rest_framework.fields import ChoiceField, Field
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
from pretix.api.serializers import (
|
||||
CompatibleJSONField, ConfigurableSerializerMixin,
|
||||
SalesChannelMigrationMixin,
|
||||
CompatibleJSONField, SalesChannelMigrationMixin,
|
||||
)
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
@@ -168,7 +167,7 @@ class ValidKeysField(Field):
|
||||
}
|
||||
|
||||
|
||||
class EventSerializer(SalesChannelMigrationMixin, ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||
class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
item_meta_properties = MetaPropertyField(required=False, source='*')
|
||||
plugins = PluginsField(required=False, source='*')
|
||||
@@ -199,11 +198,10 @@ class EventSerializer(SalesChannelMigrationMixin, ConfigurableSerializerMixin, I
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not hasattr(self.context['request'], 'event'):
|
||||
self.fields.pop('valid_keys', None)
|
||||
self.fields.pop('valid_keys')
|
||||
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
||||
self.fields.pop('best_availability_state', None)
|
||||
if 'limit_sales_channels' in self.fields:
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['organizer'].sales_channels.all()
|
||||
self.fields.pop('best_availability_state')
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['organizer'].sales_channels.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -485,7 +483,7 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer):
|
||||
fields = ('variation', 'price', 'disabled', 'available_from', 'available_until')
|
||||
|
||||
|
||||
class SubEventSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||
class SubEventSerializer(I18nAwareModelSerializer):
|
||||
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True, required=False)
|
||||
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True, required=False)
|
||||
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
||||
@@ -504,7 +502,7 @@ class SubEventSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
||||
self.fields.pop('best_availability_state', None)
|
||||
self.fields.pop('best_availability_state')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -42,10 +42,8 @@ from django.utils.functional import cached_property, lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers import (
|
||||
ConfigurableSerializerMixin, SalesChannelMigrationMixin,
|
||||
)
|
||||
from pretix.api.serializers.event import MetaDataField, TaxRuleSerializer
|
||||
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||
from pretix.api.serializers.event import MetaDataField
|
||||
from pretix.api.serializers.fields import UploadedFileField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import (
|
||||
@@ -248,29 +246,7 @@ class ItemTaxRateField(serializers.Field):
|
||||
return str(Decimal('0.00'))
|
||||
|
||||
|
||||
class ItemCategorySerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ItemCategory
|
||||
fields = (
|
||||
'id', 'name', 'internal_name', 'description', 'position',
|
||||
'is_addon', 'cross_selling_mode',
|
||||
'cross_selling_condition', 'cross_selling_match_products'
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
if full_data.get('is_addon') and full_data.get('cross_selling_mode'):
|
||||
raise ValidationError('is_addon and cross_selling_mode are mutually exclusive')
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class ItemSerializer(SalesChannelMigrationMixin, ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||
class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
addons = InlineItemAddOnSerializer(many=True, required=False)
|
||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||
@@ -286,16 +262,6 @@ class ItemSerializer(SalesChannelMigrationMixin, ConfigurableSerializerMixin, I1
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
expand_fields = {
|
||||
"category": {
|
||||
"serializer": ItemCategorySerializer,
|
||||
"prefetch": ["category"],
|
||||
},
|
||||
"tax_rule": {
|
||||
"serializer": TaxRuleSerializer,
|
||||
"prefetch": ["tax_rule"],
|
||||
},
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
@@ -318,18 +284,13 @@ class ItemSerializer(SalesChannelMigrationMixin, ConfigurableSerializerMixin, I1
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'default_price' in self.fields:
|
||||
self.fields['default_price'].allow_null = False
|
||||
self.fields['default_price'].required = True
|
||||
self.fields['default_price'].allow_null = False
|
||||
self.fields['default_price'].required = True
|
||||
if not self.read_only:
|
||||
if 'require_membership_types' in self.fields:
|
||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
if 'grant_membership_type' in self.fields:
|
||||
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
if 'limit_sales_channels' in self.fields:
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
if 'variations' in self.fields and 'limit_sales_channels' in self.fields['variations'].child.fields:
|
||||
self.fields['variations'].child.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
self.fields['variations'].child.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -476,6 +437,28 @@ class ItemSerializer(SalesChannelMigrationMixin, ConfigurableSerializerMixin, I1
|
||||
return item
|
||||
|
||||
|
||||
class ItemCategorySerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ItemCategory
|
||||
fields = (
|
||||
'id', 'name', 'internal_name', 'description', 'position',
|
||||
'is_addon', 'cross_selling_mode',
|
||||
'cross_selling_condition', 'cross_selling_match_products'
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
if full_data.get('is_addon') and full_data.get('cross_selling_mode'):
|
||||
raise ValidationError('is_addon and cross_selling_mode are mutually exclusive')
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
||||
identifier = serializers.CharField(allow_null=True)
|
||||
|
||||
@@ -522,6 +505,11 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
Question._clean_identifier(self.context['event'], value, self.instance)
|
||||
return value
|
||||
|
||||
def validate_type(self, value):
|
||||
if self.instance:
|
||||
self.instance.clean_type_change(self.instance.type, value)
|
||||
return value
|
||||
|
||||
def validate_dependency_question(self, value):
|
||||
if value:
|
||||
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
|
||||
|
||||
@@ -40,15 +40,12 @@ from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from pretix.api.serializers import (
|
||||
CompatibleJSONField, ConfigurableSerializerMixin,
|
||||
)
|
||||
from pretix.api.serializers import CompatibleJSONField
|
||||
from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.item import (
|
||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||
)
|
||||
from pretix.api.serializers.voucher import VoucherSerializer
|
||||
from pretix.api.signals import order_api_details, orderposition_api_details
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.i18n import language
|
||||
@@ -178,7 +175,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def to_representation(self, instance):
|
||||
r = super().to_representation(instance)
|
||||
if r.get('answer') and r.get('answer').startswith('file://') and instance.orderposition:
|
||||
if r['answer'].startswith('file://') and instance.orderposition:
|
||||
r['answer'] = reverse('api-v1:orderposition-answer', kwargs={
|
||||
'organizer': instance.orderposition.order.event.organizer.slug,
|
||||
'event': instance.orderposition.order.event.slug,
|
||||
@@ -760,7 +757,7 @@ class OrderPluginDataField(serializers.Field):
|
||||
return d
|
||||
|
||||
|
||||
class OrderSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||
class OrderSerializer(I18nAwareModelSerializer):
|
||||
event = SlugRelatedField(slug_field='slug', read_only=True)
|
||||
invoice_address = InvoiceAddressSerializer(allow_null=True)
|
||||
positions = OrderPositionSerializer(many=True, read_only=True)
|
||||
@@ -778,39 +775,6 @@ class OrderSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||
required=False,
|
||||
)
|
||||
plugin_data = OrderPluginDataField(source='*', allow_null=True, read_only=True)
|
||||
expand_fields = {
|
||||
"positions.voucher": {
|
||||
"serializer": VoucherSerializer,
|
||||
"permission": "can_view_vouchers",
|
||||
"prefetch": ["positions__voucher"],
|
||||
},
|
||||
"positions.item": {
|
||||
"serializer": ItemSerializer,
|
||||
"prefetch": [
|
||||
"positions__item",
|
||||
"positions__item__addons",
|
||||
"positions__item__bundles",
|
||||
"positions__item__meta_values",
|
||||
"positions__item__variations",
|
||||
"positions__item__tax_rule",
|
||||
],
|
||||
},
|
||||
"positions.variation": {
|
||||
"serializer": ItemSerializer,
|
||||
"prefetch": ["positions__variation", "positions__variation__meta_values"],
|
||||
},
|
||||
"positions.subevent": {
|
||||
"serializer": SubEventSerializer,
|
||||
"prefetch": [
|
||||
"positions__subevent",
|
||||
"positions__subevent__event",
|
||||
"positions__subevent__subeventitem_set",
|
||||
"positions__subevent__subeventitemvariation_set",
|
||||
"positions__subevent__seat_category_mappings",
|
||||
"positions__subevent__meta_values",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@@ -829,14 +793,47 @@ class OrderSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if "sales_channel" in self.fields:
|
||||
if "organizer" in self.context:
|
||||
self.fields["sales_channel"].queryset = self.context["organizer"].sales_channels.all()
|
||||
else:
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
if not self.context['pdf_data'] and "positions" in self.fields:
|
||||
if "organizer" in self.context:
|
||||
self.fields["sales_channel"].queryset = self.context["organizer"].sales_channels.all()
|
||||
else:
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
if not self.context['pdf_data']:
|
||||
self.fields['positions'].child.fields.pop('pdf_data', None)
|
||||
|
||||
includes = set(self.context['include'])
|
||||
if includes:
|
||||
for fname, field in list(self.fields.items()):
|
||||
if fname in includes:
|
||||
continue
|
||||
elif hasattr(field, 'child'): # Nested list serializers
|
||||
found_any = False
|
||||
for childfname, childfield in list(field.child.fields.items()):
|
||||
if f'{fname}.{childfname}' not in includes:
|
||||
field.child.fields.pop(childfname)
|
||||
else:
|
||||
found_any = True
|
||||
if not found_any:
|
||||
self.fields.pop(fname)
|
||||
elif isinstance(field, serializers.Serializer): # Nested serializers
|
||||
found_any = False
|
||||
for childfname, childfield in list(field.fields.items()):
|
||||
if f'{fname}.{childfname}' not in includes:
|
||||
field.fields.pop(childfname)
|
||||
else:
|
||||
found_any = True
|
||||
if not found_any:
|
||||
self.fields.pop(fname)
|
||||
else:
|
||||
self.fields.pop(fname)
|
||||
|
||||
for exclude_field in self.context['exclude']:
|
||||
p = exclude_field.split('.')
|
||||
if p[0] in self.fields:
|
||||
if len(p) == 1:
|
||||
del self.fields[p[0]]
|
||||
elif len(p) == 2:
|
||||
self.fields[p[0]].child.fields.pop(p[1])
|
||||
|
||||
def validate_locale(self, l):
|
||||
if l not in set(k for k in self.instance.event.settings.locales):
|
||||
raise ValidationError('"{}" is not a supported locale for this event.'.format(l))
|
||||
|
||||
@@ -31,7 +31,7 @@ from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.auth.devicesecurity import get_all_security_profiles
|
||||
from pretix.api.serializers import AsymmetricField, ConfigurableSerializerMixin
|
||||
from pretix.api.serializers import AsymmetricField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import CompatibleJSONField
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
@@ -51,7 +51,7 @@ from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OrganizerSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||
class OrganizerSerializer(I18nAwareModelSerializer):
|
||||
public_url = serializers.SerializerMethodField('get_organizer_url', read_only=True)
|
||||
|
||||
def get_organizer_url(self, organizer):
|
||||
|
||||
@@ -121,7 +121,6 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
ctx['request'] = self.request
|
||||
return ctx
|
||||
|
||||
def perform_update(self, serializer):
|
||||
|
||||
@@ -476,7 +476,7 @@ def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int, retr
|
||||
300, # + 5 minutes
|
||||
1200, # + 20 minutes
|
||||
3600, # + 60 minutes
|
||||
1440, # + 4 hours
|
||||
14400, # + 4 hours
|
||||
21600, # + 6 hours
|
||||
43200, # + 12 hours
|
||||
43200, # + 24 hours
|
||||
@@ -527,8 +527,10 @@ def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int, retr
|
||||
if retry_count >= len(retry_intervals):
|
||||
return 'retry-given-up'
|
||||
elif retry_intervals[retry_count] < retry_celery_cutoff:
|
||||
send_webhook.apply_async(args=(logentry_id, action_type, webhook_id, retry_count + 1),
|
||||
countdown=retry_intervals[retry_count])
|
||||
send_webhook.apply_async(
|
||||
args=(logentry_id, action_type, webhook_id, retry_count + 1),
|
||||
countdown=retry_intervals[retry_count]
|
||||
)
|
||||
return 'retry-via-celery'
|
||||
else:
|
||||
webhook.retries.update_or_create(
|
||||
@@ -555,7 +557,10 @@ def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int, retr
|
||||
if retry_count >= len(retry_intervals):
|
||||
return 'retry-given-up'
|
||||
elif retry_intervals[retry_count] < retry_celery_cutoff:
|
||||
send_webhook.apply_async(args=(logentry_id, action_type, webhook_id, retry_count + 1))
|
||||
send_webhook.apply_async(
|
||||
args=(logentry_id, action_type, webhook_id, retry_count + 1),
|
||||
countdown=retry_intervals[retry_count]
|
||||
)
|
||||
return 'retry-via-celery'
|
||||
else:
|
||||
webhook.retries.update_or_create(
|
||||
|
||||
@@ -896,10 +896,17 @@ class BaseQuestionsForm(forms.Form):
|
||||
'Please enter a date no later than {max}.',
|
||||
max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
if initial and initial.answer:
|
||||
try:
|
||||
_initial = dateutil.parser.parse(initial.answer).date()
|
||||
except dateutil.parser.ParserError:
|
||||
_initial = None
|
||||
else:
|
||||
_initial = None
|
||||
field = forms.DateField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else None,
|
||||
initial=_initial,
|
||||
widget=DatePickerWidget(attrs),
|
||||
)
|
||||
if q.valid_date_min:
|
||||
@@ -907,10 +914,17 @@ class BaseQuestionsForm(forms.Form):
|
||||
if q.valid_date_max:
|
||||
field.validators.append(MaxDateValidator(q.valid_date_max))
|
||||
elif q.type == Question.TYPE_TIME:
|
||||
if initial and initial.answer:
|
||||
try:
|
||||
_initial = dateutil.parser.parse(initial.answer).time()
|
||||
except dateutil.parser.ParserError:
|
||||
_initial = None
|
||||
else:
|
||||
_initial = None
|
||||
field = forms.TimeField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=dateutil.parser.parse(initial.answer).time() if initial and initial.answer else None,
|
||||
initial=_initial,
|
||||
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
|
||||
)
|
||||
elif q.type == Question.TYPE_DATETIME:
|
||||
@@ -931,10 +945,19 @@ class BaseQuestionsForm(forms.Form):
|
||||
'Please enter a date and time no later than {max}.',
|
||||
max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
|
||||
if initial and initial.answer:
|
||||
try:
|
||||
_initial = dateutil.parser.parse(initial.answer).astimezone(tz)
|
||||
except dateutil.parser.ParserError:
|
||||
_initial = None
|
||||
else:
|
||||
_initial = None
|
||||
|
||||
field = SplitDateTimeField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None,
|
||||
initial=_initial,
|
||||
widget=SplitDateTimePickerWidget(
|
||||
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
|
||||
min_date=q.valid_datetime_min,
|
||||
|
||||
@@ -1925,6 +1925,25 @@ class Question(LoggedModel):
|
||||
raise ValidationError(_("The maximum value must not be lower than the minimum value."))
|
||||
super().clean()
|
||||
|
||||
def clean_type_change(self, old_type, new_type):
|
||||
if old_type == new_type:
|
||||
return True
|
||||
if not self.pk or not self.answers.exists():
|
||||
return True
|
||||
if new_type == self.TYPE_TEXT and old_type != self.TYPE_FILE:
|
||||
# All types can be converted to text except file
|
||||
return True
|
||||
if new_type == self.TYPE_STRING and old_type not in (self.TYPE_TEXT, self.TYPE_FILE):
|
||||
# All types can be converted to string except text or file
|
||||
return True
|
||||
if new_type == self.TYPE_CHOICE_MULTIPLE and old_type == self.TYPE_CHOICE:
|
||||
# Single-choice can be converted to multiple choice without loss
|
||||
return True
|
||||
raise ValidationError(
|
||||
_("The system already contains answers to this question that are not compatible with changing the "
|
||||
"type of question without data loss. Consider hiding this question and creating a new one instead.")
|
||||
)
|
||||
|
||||
|
||||
class QuestionOption(models.Model):
|
||||
question = models.ForeignKey('Question', related_name='options', on_delete=models.CASCADE)
|
||||
|
||||
@@ -3749,7 +3749,7 @@ COUNTRIES_WITH_STATE_IN_ADDRESS = {
|
||||
# 'CN': (['Province', 'Autonomous region', 'Munincipality'], 'long'),
|
||||
'JP': (['Prefecture'], 'long'),
|
||||
'MY': (['State', 'Federal territory'], 'long'),
|
||||
'MX': (['State', 'Federal district'], 'short'),
|
||||
'MX': (['State', 'Federal district', 'Federal entity'], 'short'),
|
||||
'US': (['State', 'Outlying area', 'District'], 'short'),
|
||||
'IT': (['Province', 'Free municipal consortium', 'Metropolitan city', 'Autonomous province',
|
||||
'Free municipal consortium', 'Decentralized regional entity'], 'short'),
|
||||
|
||||
@@ -201,6 +201,12 @@ class QuestionForm(I18nModelForm):
|
||||
|
||||
return val
|
||||
|
||||
def clean_type(self):
|
||||
val = self.cleaned_data.get('type')
|
||||
if self.instance:
|
||||
self.instance.clean_type_change(self.instance.type, val)
|
||||
return val
|
||||
|
||||
def clean_identifier(self):
|
||||
val = self.cleaned_data.get('identifier')
|
||||
Question._clean_identifier(self.instance.event, val, self.instance)
|
||||
|
||||
@@ -548,23 +548,24 @@ class OrderDetail(OrderView):
|
||||
|
||||
unsent_invoices = [ii.pk for ii in ctx['invoices'] if not ii.sent_to_customer]
|
||||
if unsent_invoices:
|
||||
ctx['invoices_send_link'] = reverse('control:event.order.sendmail', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'code': self.order.code
|
||||
}) + '?' + urlencode({
|
||||
'subject': ngettext('Your invoice', 'Your invoices', len(unsent_invoices)),
|
||||
'message': ngettext(
|
||||
'Hello,\n\nplease find your invoice attached to this email.\n\n'
|
||||
'Your {event} team',
|
||||
'Hello,\n\nplease find your invoices attached to this email.\n\n'
|
||||
'Your {event} team',
|
||||
len(unsent_invoices)
|
||||
).format(
|
||||
event="{event}",
|
||||
),
|
||||
'attach_invoices': unsent_invoices
|
||||
}, doseq=True)
|
||||
with language(self.order.locale):
|
||||
ctx['invoices_send_link'] = reverse('control:event.order.sendmail', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'code': self.order.code
|
||||
}) + '?' + urlencode({
|
||||
'subject': ngettext('Your invoice', 'Your invoices', len(unsent_invoices)),
|
||||
'message': ngettext(
|
||||
'Hello,\n\nplease find your invoice attached to this email.\n\n'
|
||||
'Your {event} team',
|
||||
'Hello,\n\nplease find your invoices attached to this email.\n\n'
|
||||
'Your {event} team',
|
||||
len(unsent_invoices)
|
||||
).format(
|
||||
event="{event}",
|
||||
),
|
||||
'attach_invoices': unsent_invoices
|
||||
}, doseq=True)
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-30 10:35+0000\n"
|
||||
"PO-Revision-Date: 2025-06-12 17:00+0000\n"
|
||||
"Last-Translator: Richard Schreiber <schreiber@rami.io>\n"
|
||||
"PO-Revision-Date: 2025-06-24 23:00+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
|
||||
">\n"
|
||||
"Language: de\n"
|
||||
@@ -1424,7 +1424,7 @@ msgstr "Postleitzahl"
|
||||
#: pretix/plugins/checkinlists/exporters.py:536
|
||||
#: pretix/plugins/reports/exporters.py:842
|
||||
msgid "City"
|
||||
msgstr "Ort"
|
||||
msgstr "Stadt"
|
||||
|
||||
#: pretix/base/exporters/invoices.py:210 pretix/base/exporters/invoices.py:218
|
||||
#: pretix/base/exporters/invoices.py:336 pretix/base/exporters/invoices.py:344
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-30 10:35+0000\n"
|
||||
"PO-Revision-Date: 2025-05-30 11:15+0000\n"
|
||||
"PO-Revision-Date: 2025-06-24 23:00+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
"pretix/pretix/de_Informal/>\n"
|
||||
@@ -1425,7 +1425,7 @@ msgstr "Postleitzahl"
|
||||
#: pretix/plugins/checkinlists/exporters.py:536
|
||||
#: pretix/plugins/reports/exporters.py:842
|
||||
msgid "City"
|
||||
msgstr "Ort"
|
||||
msgstr "Stadt"
|
||||
|
||||
#: pretix/base/exporters/invoices.py:210 pretix/base/exporters/invoices.py:218
|
||||
#: pretix/base/exporters/invoices.py:336 pretix/base/exporters/invoices.py:344
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-30 10:35+0000\n"
|
||||
"PO-Revision-Date: 2025-06-11 06:58+0000\n"
|
||||
"PO-Revision-Date: 2025-06-25 06:56+0000\n"
|
||||
"Last-Translator: 조정화 <junghwa.jo@om.org>\n"
|
||||
"Language-Team: Korean <https://translate.pretix.eu/projects/pretix/pretix/ko/"
|
||||
">\n"
|
||||
@@ -290,8 +290,8 @@ msgid ""
|
||||
"Updating add-ons, bundles, or variations via PATCH/PUT is not supported. "
|
||||
"Please use the dedicated nested endpoint."
|
||||
msgstr ""
|
||||
"추가 기능, 묶음 상품들, 또는 변형은 PATCH/PUT를 통해 업데이트 할 수 없습니"
|
||||
"다. 전용 중첩은 마지막 지점에서 사용해주세요."
|
||||
"추가 기능, 묶음 상품들, 또는 변형은 PATCH/PUT를 통해 업데이트 할 수 "
|
||||
"없습니다. 전용 중첩은 마지막 지점에서 사용하세요"
|
||||
|
||||
#: pretix/api/serializers/item.py:306
|
||||
msgid "Only admission products can currently be personalized."
|
||||
@@ -660,15 +660,17 @@ msgid "Your password must contain both numeric and alphabetic characters."
|
||||
msgstr "비밀번호는 숫자와 알파벳 문자가 모두 포함되어야 합니다"
|
||||
|
||||
#: pretix/base/auth.py:202 pretix/base/auth.py:212
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "Your password may not be the same as your previous password."
|
||||
msgid_plural ""
|
||||
"Your password may not be the same as one of your %(history_length)s previous "
|
||||
"passwords."
|
||||
msgstr[0] ""
|
||||
"단수형\n"
|
||||
"비밀번호가 이전 비밀번호와 동일하지 않을 수 있습니다.\n"
|
||||
"귀하의 비밀번호는 %(history_length)s 이전 비밀번호와 동일하지 않을 수 있습니"
|
||||
"다."
|
||||
"복수형\n"
|
||||
"귀하의 비밀번호는 %(history_length)s 이전 비밀번호 중 하나와 동일하지 않을 "
|
||||
"수 있습니다."
|
||||
|
||||
#: pretix/base/channels.py:168
|
||||
msgid "Online shop"
|
||||
@@ -690,13 +692,14 @@ msgstr ""
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "powered by {name} based on <a {a_attr}>pretix</a>"
|
||||
msgid "<a {a_name_attr}>powered by {name}</a> <a {a_attr}>based on pretix</a>"
|
||||
msgstr "{a_attr}>pretix</a>를 기반으로 {name}에 의해 작동합니다"
|
||||
msgstr "이 서비스는 {name}에 의해 제공되며 pretix(온라인 이벤트 티켓팅 및 등록 "
|
||||
"시스템) 시스템 기반으로 작동합니다"
|
||||
|
||||
#: pretix/base/context.py:48
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "powered by {name} based on <a {a_attr}>pretix</a>"
|
||||
msgid "<a {a_attr}>powered by {name} based on pretix</a>"
|
||||
msgstr "{a_attr}>pretix</a>를 기반으로 {name}에 의해 작동합니다"
|
||||
msgstr "이 서비스는 {name}에서 제공하며 pretix를 기반으로 합니다."
|
||||
|
||||
#: pretix/base/context.py:55
|
||||
#, python-format
|
||||
@@ -726,16 +729,16 @@ msgid "Incompatible SSO provider: \"{error}\"."
|
||||
msgstr "호환되지 않는 단일 로그인(Single Sign On) 제공자: \"{error}\"."
|
||||
|
||||
#: pretix/base/customersso/oidc.py:111
|
||||
#, fuzzy, python-brace-format
|
||||
#, python-brace-format
|
||||
msgid "You are not requesting \"{scope}\"."
|
||||
msgstr "\"범위\"를 요청하는 것이 아닙니다."
|
||||
msgstr "'{scope}' 요청이 포함되어 있지 않습니다."
|
||||
|
||||
#: pretix/base/customersso/oidc.py:117
|
||||
#, fuzzy, python-brace-format
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"You are requesting scope \"{scope}\" but provider only supports these: "
|
||||
"{scopes}."
|
||||
msgstr "범위 \"{scope}\"를 요청하고 있지만 제공자는 이를 지원합니다: {scope}"
|
||||
msgstr "범위 \"{scope}\"를 요청하고 있지만 제공자는 이를 지원합니다: {scopes}"
|
||||
|
||||
#: pretix/base/customersso/oidc.py:127
|
||||
#, python-brace-format
|
||||
@@ -2020,7 +2023,7 @@ msgstr "주문 지역 설정"
|
||||
#: pretix/base/exporters/orderlist.py:275
|
||||
#, fuzzy, python-brace-format
|
||||
msgid "Gross at {rate} % tax"
|
||||
msgstr "세율 {%}의 세금으로 총합"
|
||||
msgstr "세율{%}의 세금으로 총합"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:276
|
||||
#, python-brace-format
|
||||
@@ -2028,9 +2031,9 @@ msgid "Net at {rate} % tax"
|
||||
msgstr "순세율 %{rate} 세금"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:277
|
||||
#, fuzzy, python-brace-format
|
||||
#, python-brace-format
|
||||
msgid "Tax value at {rate} % tax"
|
||||
msgstr "세율 % 세금에서의 세금 가치"
|
||||
msgstr "{rate} % 세금에서의 세금 가치"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:280
|
||||
msgid "Invoice numbers"
|
||||
@@ -2070,7 +2073,7 @@ msgstr "외부고객 아이디"
|
||||
#: pretix/base/exporters/orderlist.py:293
|
||||
#, fuzzy, python-brace-format
|
||||
msgid "Paid by {method}"
|
||||
msgstr "{방법}으로 결제됨"
|
||||
msgstr "{방법}에 의해 결제됨"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:448
|
||||
#: pretix/base/exporters/orderlist.py:894
|
||||
@@ -3152,10 +3155,10 @@ msgid "Individual customer"
|
||||
msgstr "개별 고객"
|
||||
|
||||
#: pretix/base/invoice.py:138
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgctxt "invoice"
|
||||
msgid "Page %d of %d"
|
||||
msgstr "%d 페이지"
|
||||
msgstr "%d의%d 페이지"
|
||||
|
||||
#: pretix/base/invoice.py:375
|
||||
msgctxt "invoice"
|
||||
@@ -3303,7 +3306,7 @@ msgstr "단일 가격: {net_price} 순 / {gross_price} 총합"
|
||||
#, fuzzy, python-brace-format
|
||||
msgctxt "invoice"
|
||||
msgid "Single price: {price}"
|
||||
msgstr "단일 가격: {price}"
|
||||
msgstr "단일 가격: {가격}"
|
||||
|
||||
#: pretix/base/invoice.py:742 pretix/base/invoice.py:748
|
||||
msgctxt "invoice"
|
||||
@@ -3356,8 +3359,7 @@ msgctxt "invoice"
|
||||
msgid ""
|
||||
"Using the conversion rate of 1:{rate} as published by the {authority} on "
|
||||
"{date}, this corresponds to:"
|
||||
msgstr ""
|
||||
"{날짜}에 {당국}에서 발표한 1:{rate}의 변환율을 사용하면 다음과 같습니다:"
|
||||
msgstr "{날짜}에 {당국}에서 발표한 1:{세율}의 변환율을 사용하면 다음과 같습니다:"
|
||||
|
||||
#: pretix/base/invoice.py:909
|
||||
#, fuzzy, python-brace-format
|
||||
@@ -3365,8 +3367,7 @@ msgctxt "invoice"
|
||||
msgid ""
|
||||
"Using the conversion rate of 1:{rate} as published by the {authority} on "
|
||||
"{date}, the invoice total corresponds to {total}."
|
||||
msgstr ""
|
||||
"{날짜}에 {당국}에서 게시한 1:{rate}의 변환율을 사용하면 송장 총액이 {총합}에 "
|
||||
msgstr "{날짜}에 {당국}에서 게시한 1:{세율}의 변환율을 사용하면 송장 총액이 {총합}에 "
|
||||
"해당합니다."
|
||||
|
||||
#: pretix/base/invoice.py:923
|
||||
@@ -4210,11 +4211,9 @@ msgid "Available for dates starting from"
|
||||
msgstr "다음 날짜부터 사용 가능"
|
||||
|
||||
#: pretix/base/models/discount.py:182
|
||||
#, fuzzy
|
||||
#| msgid "Available until"
|
||||
msgctxt "subevent"
|
||||
msgid "Available for dates starting until"
|
||||
msgstr "다음까지 사용가능"
|
||||
msgstr "다음~까지 사용가능"
|
||||
|
||||
#: pretix/base/models/discount.py:214
|
||||
msgid ""
|
||||
@@ -5448,16 +5447,12 @@ msgid "Unknown country code."
|
||||
msgstr "알수 없는 국가 코드 입니다"
|
||||
|
||||
#: pretix/base/models/items.py:1921 pretix/base/models/items.py:1923
|
||||
#, fuzzy
|
||||
#| msgid "The maximum count needs to be greater than the minimum count."
|
||||
msgid "The maximum date must not be before the minimum value."
|
||||
msgstr "최대 카운트는 최소 카운트보다 커야 합니다."
|
||||
msgstr "종료일(최대 날짜)은 시작일(최소값)보다 앞서면 안됩니다."
|
||||
|
||||
#: pretix/base/models/items.py:1925
|
||||
#, fuzzy
|
||||
#| msgid "The maximum count needs to be greater than the minimum count."
|
||||
msgid "The maximum value must not be lower than the minimum value."
|
||||
msgstr "최대 카운트는 최소 카운트보다 커야 합니다."
|
||||
msgstr "최대 값은 최소 값보다 커야 합니다."
|
||||
|
||||
#: pretix/base/models/items.py:1942
|
||||
#: pretix/control/templates/pretixcontrol/items/question.html:90
|
||||
@@ -5913,10 +5908,8 @@ msgid "Cart ID (e.g. session key)"
|
||||
msgstr "카트 ID(예: 세션 키)"
|
||||
|
||||
#: pretix/base/models/orders.py:3102
|
||||
#, fuzzy
|
||||
#| msgid "Gift card: Expiration date"
|
||||
msgid "Limit for extending expiration date"
|
||||
msgstr "기프트 카드: 만료일"
|
||||
msgstr "만료일 연장 제한"
|
||||
|
||||
#: pretix/base/models/orders.py:3131
|
||||
msgid "Cart position"
|
||||
@@ -6080,9 +6073,9 @@ msgid "Teams"
|
||||
msgstr "팀들"
|
||||
|
||||
#: pretix/base/models/organizer.py:406
|
||||
#, fuzzy, python-brace-format
|
||||
#, python-brace-format
|
||||
msgid "Invite to team '{team}' for '{email}'"
|
||||
msgstr "'이메일'을 위해 '팀'에 초대하기"
|
||||
msgstr "'{email}'님을 '{team}' 팀에 초대하기"
|
||||
|
||||
#: pretix/base/models/organizer.py:538
|
||||
#: pretix/control/templates/pretixcontrol/organizers/channels.html:23
|
||||
@@ -6216,35 +6209,37 @@ msgstr ""
|
||||
|
||||
#: pretix/base/models/tax.py:205 pretix/base/models/tax.py:218
|
||||
#: pretix/base/models/tax.py:244
|
||||
#, fuzzy, python-brace-format
|
||||
#, python-brace-format
|
||||
msgctxt "tax_code"
|
||||
msgid ""
|
||||
"Exempt based on article {article}, section {section} ({letter}) of Council "
|
||||
"Directive 2006/112/EC"
|
||||
msgstr "위원회 지침 2006/112/EC의 {조문}, 섹션 {{편지}}에 따라 면제됩니다"
|
||||
msgstr ""
|
||||
"2006년 제정된 유럽연합 이사회 지침 2006/112/EC의 제{article}조 제{section}항 "
|
||||
"({letter}호)에 따라 면제됩니다"
|
||||
|
||||
#: pretix/base/models/tax.py:231
|
||||
#, fuzzy, python-brace-format
|
||||
#, python-brace-format
|
||||
msgctxt "tax_code"
|
||||
msgid ""
|
||||
"Exempt based on article {article}, section ({letter}) of Council Directive "
|
||||
"2006/112/EC"
|
||||
msgstr "2006/112/EC 이사회 지침 제{조문}, 섹션({편지})에 따라 면제됩니다"
|
||||
msgstr "2006년 제정된 유럽연합 이사회 지침 2006/112/EC의 제{article}조 ({letter}항)"
|
||||
"에 따라 면제됩니다"
|
||||
|
||||
#: pretix/base/models/tax.py:252
|
||||
msgctxt "tax_code"
|
||||
msgid "Exempt based on article 309 of Council Directive 2006/112/EC"
|
||||
msgstr ""
|
||||
"2006/112/EC 이사회 지침 제309조에 따른 면제( 세우타/멜리야 의 공급에 대한 명"
|
||||
"시적 부가가치세는 면세-EU VAT지역 밖이므로 해당지역으로의 공급은 수출로 간주"
|
||||
"함)"
|
||||
"2006년 제정된 유럽연합 이사회 지침 2006/112/EC의 제309조에 따라 면제됩니다( "
|
||||
"세우타/멜리야 의 공급에 대한 명시적 부가가치세는 면세-EU VAT지역 밖이므로 "
|
||||
"해당지역으로의 공급은 수출로 간주함)"
|
||||
|
||||
#: pretix/base/models/tax.py:254
|
||||
msgctxt "tax_code"
|
||||
msgid "Intra-Community acquisition from second hand means of transport"
|
||||
msgstr ""
|
||||
"중고 운송수단을 구매하면 ICA 적용한다 (EU 회원국간의 재화 이동에 서 발생하는 "
|
||||
"VAT 개념)"
|
||||
msgstr "중고 운송수단을 구매하면 EU 회원국 간 물품 취득(ICA)을 적용한다 (EU "
|
||||
"회원국간의 재화 이동에 서 발생하는 VAT 개념)"
|
||||
|
||||
#: pretix/base/models/tax.py:256
|
||||
msgctxt "tax_code"
|
||||
@@ -6255,7 +6250,7 @@ msgstr ""
|
||||
#: pretix/base/models/tax.py:258
|
||||
msgctxt "tax_code"
|
||||
msgid "Intra-Community acquisition of works of art"
|
||||
msgstr "예술작품은 ICA(EU 회원간의 재화이동에서 적용되는 VAT개념)을 적용한다"
|
||||
msgstr "예술작품은 ICA(EU 회원간의 재화이동에서 적용되는 VAT개념)을 적용한다"
|
||||
|
||||
#: pretix/base/models/tax.py:260
|
||||
msgctxt "tax_code"
|
||||
@@ -6268,8 +6263,8 @@ msgstr ""
|
||||
msgctxt "tax_code"
|
||||
msgid "France domestic VAT franchise in base"
|
||||
msgstr ""
|
||||
"프랑스 소규모 사업자를 위한 부가가치세 면세제도(\"TVA non applicable, "
|
||||
"article 293 B du CGI\" 기입)"
|
||||
"프랑스 소규모 사업자를 위한 부가가치세 면세제도("
|
||||
"\"TVA non applicable, article 293 B du CGI\" 기입)"
|
||||
|
||||
#: pretix/base/models/tax.py:264
|
||||
msgctxt "tax_code"
|
||||
@@ -6317,9 +6312,8 @@ msgstr "청구서 주소에 따라 세율이 변경되는 경우 총 금액을
|
||||
|
||||
#: pretix/base/models/tax.py:359
|
||||
msgid "Use EU reverse charge taxation rules"
|
||||
msgstr ""
|
||||
"EU 내 VAT 처리 방식 중 하나로 국경거래에서 VAT 납부 책임을 구매자에게 전가하"
|
||||
"는 과세 규칙 사용 (프랑스, 독일 등)"
|
||||
msgstr "EU 내 VAT 처리 방식(국경거래에서 VAT 납부 책임을 구매자에게 전가하는 과세 "
|
||||
"규칙)을 사용합니다 (프랑스, 독일 등)"
|
||||
|
||||
#: pretix/base/models/tax.py:363
|
||||
msgid ""
|
||||
@@ -6333,7 +6327,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/models/tax.py:365
|
||||
msgid "DEPRECATED"
|
||||
msgstr "사용중단 예정 (폐지 예정입니다)"
|
||||
msgstr "사용중단 예정 (폐지 예정)"
|
||||
|
||||
#: pretix/base/models/tax.py:366
|
||||
msgid ""
|
||||
@@ -6352,7 +6346,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/models/tax.py:374 pretix/plugins/stripe/payment.py:299
|
||||
msgid "Merchant country"
|
||||
msgstr "상인 국가"
|
||||
msgstr "상업국가"
|
||||
|
||||
#: pretix/base/models/tax.py:376
|
||||
msgid ""
|
||||
@@ -6370,12 +6364,12 @@ msgstr ""
|
||||
#: pretix/base/models/tax.py:416 pretix/control/forms/event.py:1560
|
||||
msgid ""
|
||||
"A combination of this tax code with a non-zero tax rate does not make sense."
|
||||
msgstr "이 세법을 0이 아닌 세율과 결합하는 것은 의미가 없습니다."
|
||||
msgstr "이 세법은 0이 아닌 세율과 결합하는 이 세율코드는 의미가 없습니다."
|
||||
|
||||
#: pretix/base/models/tax.py:421 pretix/control/forms/event.py:1564
|
||||
msgid ""
|
||||
"A combination of this tax code with a zero tax rate does not make sense."
|
||||
msgstr "이 세법을 제로 세율과 결합하는 것은 의미가 없습니다."
|
||||
msgstr "이 세법을 제로 세율과 결합하는 세율코드는 의미가 없습니다."
|
||||
|
||||
#: pretix/base/models/tax.py:426
|
||||
#, python-brace-format
|
||||
@@ -6400,9 +6394,9 @@ msgid ""
|
||||
"Reverse Charge: According to Article 194, 196 of Council Directive 2006/112/"
|
||||
"EEC, VAT liability rests with the service recipient."
|
||||
msgstr ""
|
||||
"EU 내 VAT 처리 방식중 하나로 국경거래에서 VAT 납부책임을 구매자에게 전가하는 "
|
||||
"과세 규칙: 2006/112/EEC 이사회 지침 제194조, 196조에 따르면 부가가치세 책임"
|
||||
"은 서비스 수혜자에게 있습니다."
|
||||
"(EU 내 VAT 처리 방식)국경거래에서 VAT 납부책임을 구매자에게 전가하는 과세 "
|
||||
"규칙: 2006/112/EEC 이사회 지침 제194조, 196조에 따르면 부가가치세 책임은 "
|
||||
"서비스 수혜자에게 있습니다."
|
||||
|
||||
#: pretix/base/models/tax.py:574
|
||||
msgctxt "invoice"
|
||||
@@ -6427,7 +6421,7 @@ msgstr "제품 가격 인하(%)"
|
||||
|
||||
#: pretix/base/models/vouchers.py:197
|
||||
msgid "Number of times this voucher can be redeemed."
|
||||
msgstr "이 바우처를 사용할 수 있는 횟수."
|
||||
msgstr "이 바우처가 사용될 수 있는 횟수입니다"
|
||||
|
||||
#: pretix/base/models/vouchers.py:201 pretix/control/views/vouchers.py:120
|
||||
msgid "Redeemed"
|
||||
@@ -6484,7 +6478,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/models/vouchers.py:265
|
||||
msgid "This variation of the product select above is being used."
|
||||
msgstr "위의 선택한 제품의 변형(옵션)이 사용되고 있습니다."
|
||||
msgstr "위의 선택한 제품의 변형(옵션)이 사용되고 있습니다."
|
||||
|
||||
#: pretix/base/models/vouchers.py:274
|
||||
msgid ""
|
||||
@@ -6545,7 +6539,7 @@ msgstr "이 변형은 이 제품에 속하지 않습니다."
|
||||
|
||||
#: pretix/base/models/vouchers.py:355
|
||||
msgid "It is currently not possible to create vouchers for add-on products."
|
||||
msgstr "현재 애드온 제품에 대한 바우처를 만드는 것은 불가능합니다."
|
||||
msgstr "현재 묶음 제품에 대한 바우처를 만드는 것은 불가능합니다."
|
||||
|
||||
#: pretix/base/models/vouchers.py:357 pretix/base/models/vouchers.py:469
|
||||
msgid ""
|
||||
@@ -6575,7 +6569,7 @@ msgstr "이 바우처가 할당량을 차단하려면 특정 날짜를 선택해
|
||||
|
||||
#: pretix/base/models/vouchers.py:384
|
||||
msgid "You can not select a subevent if your event is not an event series."
|
||||
msgstr "이벤트가 이벤트 시리즈가 아닌 경우 하위 이벤트를 선택할 수 없습니다."
|
||||
msgstr "당신의이벤트가 이벤트 시리즈가 아닌 경우 하위 이벤트를 선택할 수 없습니다."
|
||||
|
||||
#: pretix/base/models/vouchers.py:482
|
||||
msgid ""
|
||||
@@ -17957,20 +17951,13 @@ msgid ""
|
||||
"Your %(instance)s team\n"
|
||||
msgstr ""
|
||||
"안녕하세요.\n"
|
||||
"\n"
|
||||
"누군가 %(주소)를 %(instance)의 발신자 주소로 사용해 달라고 요청했습니다.\n"
|
||||
"\n"
|
||||
"이렇게 하면 이 이메일 주소에서 발신된 것으로 표시된 이메일을 보낼 수 "
|
||||
"있습니다.\n"
|
||||
"\n"
|
||||
"만약 당신이라면, 다음 확인 코드를 입력해 주세요:\n"
|
||||
"\n"
|
||||
"%(코드)\n"
|
||||
"\n"
|
||||
"이것이 당신의 요청이 아니라면, 이 이메일을 무시해도 괜찮습니다.\n"
|
||||
"\n"
|
||||
"감사해요.\n"
|
||||
"\n"
|
||||
"%(인스턴스) 팀\n"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/email/forgot.txt:1
|
||||
@@ -18018,23 +18005,14 @@ msgid ""
|
||||
"Your pretix team\n"
|
||||
msgstr ""
|
||||
"안녕하세요.\n"
|
||||
"\n"
|
||||
"이벤트를 수행할 플랫폼인 프리틱스의 팀에 초대되었습니다\n"
|
||||
"\n"
|
||||
"티켓 판매.\n"
|
||||
"\n"
|
||||
"주최자: %(주최자)\n"
|
||||
"\n"
|
||||
"팀: %(팀)\n"
|
||||
"\n"
|
||||
"해당 팀에 합류하려면 다음 링크를 클릭하세요:\n"
|
||||
"\n"
|
||||
"%(url)s\n"
|
||||
"\n"
|
||||
"가입을 원하지 않으시면 이 이메일을 무시하거나 삭제하셔도 됩니다.\n"
|
||||
"\n"
|
||||
"감사해요.\n"
|
||||
"\n"
|
||||
"프리픽스 팀\n"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/email/login_notice.txt:1
|
||||
@@ -18057,19 +18035,13 @@ msgid ""
|
||||
"Your %(instance)s team\n"
|
||||
msgstr ""
|
||||
"안녕하세요.\n"
|
||||
"\n"
|
||||
"비정상적이거나 새로운 위치에서 %(인스턴스) 계정에 로그인한 것이 "
|
||||
"감지되었습니다. 로그인은 %(국가)의 %(os)에서 %(에이전트)를 사용하여 "
|
||||
"수행되었습니다.\n"
|
||||
"\n"
|
||||
"만약 이 이메일이 당신이라면, 이 이메일을 무시해도 됩니다.\n"
|
||||
"\n"
|
||||
"본인이 아닌 경우 계정 설정에서 비밀번호를 변경하는 것이 좋습니다:\n"
|
||||
"\n"
|
||||
"%(url)s\n"
|
||||
"\n"
|
||||
"감사해요.\n"
|
||||
"\n"
|
||||
"%(인스턴스) 팀\n"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/email/security_notice.txt:1
|
||||
@@ -18640,29 +18612,30 @@ msgstr "티켓샵을 게시하려면 먼저 다음 문제를 해결해야 합니
|
||||
#: pretix/control/templates/pretixcontrol/event/live.html:51
|
||||
#: pretix/control/templates/pretixcontrol/event/live.html:65
|
||||
msgid "Go live"
|
||||
msgstr ""
|
||||
msgstr "실시간으로 전환하기"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/live.html:59
|
||||
msgid "If you want to, you can publish your ticket shop now."
|
||||
msgstr ""
|
||||
msgstr "원하신다면 지금 티켓 판매 페이지를 공개하실 수 있습니다"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/live.html:83
|
||||
msgid ""
|
||||
"Your shop is currently in test mode. All orders are not persistent and can "
|
||||
"be deleted at any point."
|
||||
msgstr ""
|
||||
msgstr "현재 상점이 테스트 모드로 설정되어 있습니다. 모든 주문은 실제로 저장되지 "
|
||||
"않으며, 언제든 삭제될 수 있습니다."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/live.html:88
|
||||
msgid "Permanently delete all orders created in test mode"
|
||||
msgstr ""
|
||||
msgstr "테스트 모드에서 생성된 모든 주문을 영구적으로 삭제합니다"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/live.html:93
|
||||
msgid "Disable test mode"
|
||||
msgstr ""
|
||||
msgstr "테스트 모드 해제"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/live.html:99
|
||||
msgid "Your shop is currently in production mode."
|
||||
msgstr ""
|
||||
msgstr "현재 상점이 운영 모드로 설정되어 있습니다"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/live.html:102
|
||||
msgid ""
|
||||
@@ -18670,6 +18643,9 @@ msgid ""
|
||||
"As long as the shop is in test mode, all orders that are created are marked "
|
||||
"as test orders and can be deleted again."
|
||||
msgstr ""
|
||||
"테스트 주문을 해보고 싶다면 상점의 테스트 모드를 켤 수 있습니다. 테스트 "
|
||||
"모드가 활성화된 동안 생성되는 모든 주문은 테스트 주문으로 처리되며, 언제든 "
|
||||
"삭제할 수 있습니다"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/live.html:104
|
||||
msgid ""
|
||||
@@ -18677,6 +18653,9 @@ msgid ""
|
||||
"vouchers and might perform actual payments. The only difference is that you "
|
||||
"can delete test orders. Use at your own risk!"
|
||||
msgstr ""
|
||||
"테스트 주문 역시 할당량에 포함되며, 실제 바우처 사용과 결제가 발생할 수 "
|
||||
"있습니다. 단지 테스트 주문은 삭제가 가능하다는 점만 다릅니다. 사용 시 이 "
|
||||
"점에 유의하시고, 본인의 책임 하에 이용해 주세요"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/live.html:108
|
||||
msgid ""
|
||||
@@ -18684,6 +18663,8 @@ msgid ""
|
||||
"sales channels such as the box office or resellers module are still created "
|
||||
"as production orders."
|
||||
msgstr ""
|
||||
"참고로, 테스트 모드는 메인 웹 상점에만 적용되며, 박스오피스나 리셀러 모듈 등 "
|
||||
"다른 판매 채널에서 발생하는 주문은 실제 운영 주문으로 생성됩니다"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/live.html:112
|
||||
msgid ""
|
||||
@@ -18691,23 +18672,26 @@ msgid ""
|
||||
"recommend enabling test mode if your customers already know your shop, as it "
|
||||
"will confuse them."
|
||||
msgstr ""
|
||||
"상점에 이미 실제 주문이 존재하는 것으로 확인됩니다. 고객들이 이미 상점을 "
|
||||
"이용하고 있다면, 테스트 모드 활성화 시 혼란이 발생할 수 있으므로 권장하지 "
|
||||
"않습니다"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/live.html:119
|
||||
msgid "Enable test mode"
|
||||
msgstr ""
|
||||
msgstr "테스트 모드 시작"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/logs.html:12
|
||||
#: pretix/control/templates/pretixcontrol/organizers/logs.html:12
|
||||
msgid "All actions"
|
||||
msgstr ""
|
||||
msgstr "모든 작업"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/logs.html:14
|
||||
msgid "Team actions"
|
||||
msgstr ""
|
||||
msgstr "팀 작업"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/logs.html:17
|
||||
msgid "Customer actions"
|
||||
msgstr ""
|
||||
msgstr "고객 작업"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/logs.html:49
|
||||
#: pretix/control/templates/pretixcontrol/event/logs_embed.html:10
|
||||
@@ -18715,14 +18699,14 @@ msgstr ""
|
||||
#: pretix/control/templates/pretixcontrol/organizers/device_logs.html:20
|
||||
#: pretix/control/templates/pretixcontrol/organizers/logs.html:35
|
||||
msgid "Personal data was cleared from this log entry."
|
||||
msgstr ""
|
||||
msgstr "이 로그 기록에서 개인정보가 삭제되었습니다"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/logs.html:58
|
||||
#: pretix/control/templates/pretixcontrol/event/logs_embed.html:19
|
||||
#: pretix/control/templates/pretixcontrol/includes/logs.html:12
|
||||
#: pretix/control/templates/pretixcontrol/organizers/logs.html:44
|
||||
msgid "This change was performed by a pretix administrator."
|
||||
msgstr ""
|
||||
msgstr "이 변경 작업은 pretix 관리자가 진행하였습니다"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/logs.html:86
|
||||
#: pretix/control/templates/pretixcontrol/event/logs_embed.html:47
|
||||
@@ -18734,7 +18718,7 @@ msgstr ""
|
||||
#: pretix/control/templates/pretixcontrol/organizers/logs.html:72
|
||||
#: pretix/control/templates/pretixcontrol/search/payments.html:147
|
||||
msgid "Inspect"
|
||||
msgstr ""
|
||||
msgstr "점검하기"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/logs.html:94
|
||||
#: pretix/control/templates/pretixcontrol/organizers/device_logs.html:50
|
||||
@@ -18745,30 +18729,30 @@ msgstr "결과 없음"
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:7
|
||||
#: pretix/control/templates/pretixcontrol/organizers/mail.html:11
|
||||
msgid "Email settings"
|
||||
msgstr ""
|
||||
msgstr "이메일 설정"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:21
|
||||
#: pretix/control/templates/pretixcontrol/organizers/mail.html:22
|
||||
msgid "Sending method"
|
||||
msgstr ""
|
||||
msgstr "전송방식"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:25
|
||||
#: pretix/control/templates/pretixcontrol/organizers/mail.html:26
|
||||
msgid "Custom SMTP server"
|
||||
msgstr ""
|
||||
msgstr "직접 설정하는 SMTP 서버"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:27
|
||||
#: pretix/control/templates/pretixcontrol/organizers/mail.html:28
|
||||
msgid "System-provided email server"
|
||||
msgstr ""
|
||||
msgstr "시스템 제공 이메일 서버"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:60
|
||||
msgid "Calendar invites"
|
||||
msgstr ""
|
||||
msgstr "일정 초대"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:66
|
||||
msgid "Email design"
|
||||
msgstr ""
|
||||
msgstr "이메일 디자인"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:79
|
||||
#: pretix/control/templates/pretixcontrol/event/mail_settings_fragment.html:29
|
||||
@@ -18777,63 +18761,63 @@ msgstr ""
|
||||
#: pretix/control/templates/pretixcontrol/vouchers/bulk.html:97
|
||||
#: pretix/control/templates/pretixcontrol/vouchers/bulk.html:120
|
||||
msgid "Preview"
|
||||
msgstr ""
|
||||
msgstr "미리보기"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:87
|
||||
#: pretix/control/templates/pretixcontrol/organizers/mail.html:58
|
||||
msgid "Email content"
|
||||
msgstr ""
|
||||
msgstr "메일 내용"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:90
|
||||
msgid "Placed order"
|
||||
msgstr ""
|
||||
msgstr "주문 완료"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:93
|
||||
msgid "Paid order"
|
||||
msgstr ""
|
||||
msgstr "결제 완료 주문"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:96
|
||||
msgid "Free order"
|
||||
msgstr ""
|
||||
msgstr "무료 주문"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:99
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:249
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:532
|
||||
msgid "Resend link"
|
||||
msgstr ""
|
||||
msgstr "링크 재전송"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:105
|
||||
msgid "Payment reminder"
|
||||
msgstr ""
|
||||
msgstr "결제 알림"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:108
|
||||
msgid "Payment failed"
|
||||
msgstr ""
|
||||
msgstr "지불 실패"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:111
|
||||
msgid "Waiting list notification"
|
||||
msgstr ""
|
||||
msgstr "대기자 명단 알림"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:117
|
||||
msgid "Order custom mail"
|
||||
msgstr ""
|
||||
msgstr "주문 맞춤 메일"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:120
|
||||
msgid "Reminder to download tickets"
|
||||
msgstr ""
|
||||
msgstr "티켓 다운로드 알림"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:123
|
||||
msgid "Order approval process"
|
||||
msgstr ""
|
||||
msgstr "주문 승인 절차"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/mail.html:126
|
||||
msgid "Attachments"
|
||||
msgstr ""
|
||||
msgstr "첨부파일"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/payment.html:6
|
||||
#: pretix/control/templates/pretixcontrol/event/payment_provider.html:5
|
||||
msgid "Payment settings"
|
||||
msgstr ""
|
||||
msgstr "결제 설정"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/payment.html:23
|
||||
#: pretix/control/templates/pretixcontrol/user/settings.html:48
|
||||
@@ -30279,10 +30263,8 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/forms/customer.py:90
|
||||
#, fuzzy
|
||||
#| msgid "Your current password"
|
||||
msgid "Forgot your password?"
|
||||
msgstr "현재 비밀번호"
|
||||
msgstr "현재 비밀번호를 잊었습니까?"
|
||||
|
||||
#: pretix/presale/forms/customer.py:146
|
||||
msgid ""
|
||||
@@ -32440,10 +32422,9 @@ msgid "Event overview"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/organizers/calendar.html:21
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Event timezone"
|
||||
#, python-format
|
||||
msgid "Events in %(month)s"
|
||||
msgstr "이벤트 시간대"
|
||||
msgstr "이벤트 시간대 %(month)s"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/organizers/calendar.html:91
|
||||
#: pretix/presale/templates/pretixpresale/organizers/calendar_day.html:104
|
||||
@@ -32989,7 +32970,7 @@ msgstr ""
|
||||
|
||||
#: pretix/settings.py:802
|
||||
msgid "Kosovo"
|
||||
msgstr ""
|
||||
msgstr "코소보"
|
||||
|
||||
#, fuzzy
|
||||
#~| msgctxt "refund_source"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-30 10:36+0000\n"
|
||||
"PO-Revision-Date: 2025-06-04 06:32+0000\n"
|
||||
"PO-Revision-Date: 2025-06-25 06:56+0000\n"
|
||||
"Last-Translator: 조정화 <junghwa.jo@om.org>\n"
|
||||
"Language-Team: Korean <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"ko/>\n"
|
||||
@@ -48,7 +48,7 @@ msgstr "이따우 (브라질 대형 민간 은행)"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:38
|
||||
msgid "PayPal Credit"
|
||||
msgstr "페이팔 신용 결제 서비스"
|
||||
msgstr "페이팔 신용 결제 서비스"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:39
|
||||
msgid "Credit Card"
|
||||
@@ -80,7 +80,7 @@ msgstr "조포르트 (PIN과 TAN 인증을 이용한 유럽 온라인 뱅킹 기
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:46
|
||||
msgid "eps"
|
||||
msgstr "이피에스 (오스트리아 실시간 은행 계좌 이체 결제 서비스)"
|
||||
msgstr "이피에스 (오스트리아 실시간 은행 계좌 이체 결제 서비스)"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:47
|
||||
msgid "MyBank"
|
||||
@@ -102,8 +102,7 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:51
|
||||
msgid "BLIK"
|
||||
msgstr ""
|
||||
"블릭 (폴란드 모바일 결제 시스템, 앱기반 OR코드/코드 입력 방식 결제 수단)"
|
||||
msgstr "블릭 (폴란드 모바일 결제 시스템, 앱기반 OR코드/코드 입력 방식 결제 수단)"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:52
|
||||
msgid "Trustly"
|
||||
@@ -178,7 +177,7 @@ msgstr "총 수익"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:15
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr "스트라이프(미국의 핀기업 온라인 결제 시스템) 에 문의하기"
|
||||
msgstr "스트라이프(미국의 핀기업 온라인 결제 시스템)에 문의하기"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:72
|
||||
msgid "Total"
|
||||
@@ -637,18 +636,14 @@ msgid "Unknown error."
|
||||
msgstr "알수 없는 에러입니다"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:292
|
||||
#, fuzzy
|
||||
#| msgid "Your color has great contrast and is very easy to read!"
|
||||
msgid "Your color has great contrast and will provide excellent accessibility."
|
||||
msgstr "당신의 색깔은 대비가 뛰어나고 읽기 매우 쉽습니다!"
|
||||
msgstr "당신의 색깔은 대비가 뛰어나고 매우 좋은 접근성을 제공합니다"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:296
|
||||
#, fuzzy
|
||||
#| msgid "Your color has decent contrast and is probably good-enough to read!"
|
||||
msgid ""
|
||||
"Your color has decent contrast and is sufficient for minimum accessibility "
|
||||
"requirements."
|
||||
msgstr "당신의 색깔은 대비가 적당하고 읽기에 충분할 것입니다!"
|
||||
msgstr "당신의 색깔은 적절한 대비가 가능하고 최소 접근성 요구사항을 충족합니다"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:300
|
||||
msgid ""
|
||||
@@ -745,22 +740,15 @@ msgstr[0] ""
|
||||
"카트에 있는 물품들은 {num}분 동안 예약되어 있습니다."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:86
|
||||
#, fuzzy
|
||||
#| msgid "Cart expired"
|
||||
msgid "Your cart has expired."
|
||||
msgstr "카트가 만료되었습니다"
|
||||
msgstr "장바구니의 유효 시간이 만료되었습니다"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:89
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "The items in your cart are no longer reserved for you. You can still "
|
||||
#| "complete your order as long as they’re available."
|
||||
msgid ""
|
||||
"The items in your cart are no longer reserved for you. You can still "
|
||||
"complete your order as long as they're available."
|
||||
msgstr ""
|
||||
"카트에 있는 상품은 더 이상 예약되지 않습니다. 주문이 가능한 한 주문을 완료할 "
|
||||
"수 있습니다."
|
||||
msgstr "장바구니에 있는 상품은 더 이상 예약되지 않습니다. 주문이 가능한 한 주문을 "
|
||||
"완료할 수 있습니다."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:90
|
||||
msgid "Do you want to renew the reservation period?"
|
||||
@@ -996,12 +984,9 @@ msgstr "매표소 열림"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
#: pretix/static/pretixpresale/js/widget/widget.v1.js:48
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "Resume checkout"
|
||||
msgctxt "widget"
|
||||
msgid "Checkout"
|
||||
msgstr "체크아웃 재개"
|
||||
msgstr "체크아웃"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
#: pretix/static/pretixpresale/js/widget/widget.v1.js:49
|
||||
@@ -1066,12 +1051,9 @@ msgid "Close"
|
||||
msgstr "종료"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "Resume checkout"
|
||||
msgctxt "widget"
|
||||
msgid "Close checkout"
|
||||
msgstr "체크아웃 재개"
|
||||
msgstr "체크아웃 종료"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
msgctxt "widget"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{% load compress %}
|
||||
{% load static %}
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixpresale/scss/widget.v1.scss" %}"/>
|
||||
{% endcompress %}
|
||||
@@ -38,10 +38,8 @@ from django.core.files.base import ContentFile, File
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db.models import Q
|
||||
from django.http import FileResponse, Http404, HttpResponse, JsonResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.template import Context, Engine
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import get_language, gettext, pgettext
|
||||
@@ -83,7 +81,7 @@ logger = logging.getLogger(__name__)
|
||||
# we never change static source without restart, so we can cache this thread-wise
|
||||
_source_cache_key = None
|
||||
|
||||
version_min = 1
|
||||
version_min = 2
|
||||
version_max = 2
|
||||
version_default = 2 # used for output in widget-embed-code
|
||||
|
||||
@@ -109,6 +107,8 @@ def indent(s):
|
||||
|
||||
|
||||
def widget_css_etag(request, version, **kwargs):
|
||||
if version < version_min:
|
||||
version = version_min
|
||||
# This makes sure a new version of the theme is loaded whenever settings or the source files have changed
|
||||
if hasattr(request, 'event'):
|
||||
return (f'{_get_source_cache_key(version)}-'
|
||||
@@ -130,11 +130,7 @@ def widget_css(request, version, **kwargs):
|
||||
if version > version_max:
|
||||
raise Http404()
|
||||
if version < version_min:
|
||||
return redirect(reverse('presale:event.widget.css' if hasattr(request, 'event') else 'organizer.widget.css', kwargs={
|
||||
'version': version_min,
|
||||
'organizer': request.organizer.slug,
|
||||
'event': request.event.slug if hasattr(request, 'event') else None,
|
||||
}))
|
||||
version = version_min
|
||||
o = getattr(request, 'event', request.organizer)
|
||||
|
||||
template_path = 'pretixpresale/widget_dummy.html' if version == version_max else 'pretixpresale/widget_dummy.v{}.html'.format(version)
|
||||
@@ -145,7 +141,7 @@ def widget_css(request, version, **kwargs):
|
||||
widget_css = f.read()
|
||||
|
||||
theme_css = get_theme_vars_css(o, widget=True)
|
||||
css = theme_css + widget_css
|
||||
css = f"/* v{version} */\n" + theme_css + widget_css
|
||||
|
||||
resp = FileResponse(css, content_type='text/css')
|
||||
resp._csp_ignore = True
|
||||
@@ -202,7 +198,7 @@ def generate_widget_js(version, lang):
|
||||
code.append('})({});\n')
|
||||
code = ''.join(code)
|
||||
code = rJSMinFilter(content=code).output()
|
||||
return code
|
||||
return f"/* v{version} */\n" + code
|
||||
|
||||
|
||||
@gzip_page
|
||||
@@ -212,10 +208,7 @@ def widget_js(request, version, lang, **kwargs):
|
||||
raise Http404()
|
||||
|
||||
if version < version_min:
|
||||
return redirect(reverse('presale:widget.js', kwargs={
|
||||
'version': version_min,
|
||||
'lang': lang,
|
||||
}))
|
||||
version = version_min
|
||||
|
||||
cached_js = cache.get('widget_js_data_v{}_{}'.format(version, lang))
|
||||
if cached_js and not settings.DEBUG:
|
||||
|
||||
@@ -1251,6 +1251,7 @@ var editor = {
|
||||
if (data.status === 'ok') {
|
||||
$("#editor-save").prop('disabled', false);
|
||||
editor.dirty = false;
|
||||
$("#preview-form").removeClass("dirty");
|
||||
editor.uploaded_file_id = null;
|
||||
editor._ever_saved = true;
|
||||
editor._update_save_button();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,6 @@ from django.utils.timezone import now
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from tests import assert_num_queries
|
||||
from tests.api.utils import _test_configurable_serializer
|
||||
from tests.const import SAMPLE_PNG
|
||||
|
||||
from pretix.base.models import (
|
||||
@@ -216,15 +215,6 @@ def test_event_list_filter(token_client, organizer, event):
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['count'] == 0
|
||||
|
||||
_test_configurable_serializer(
|
||||
token_client,
|
||||
"/api/v1/organizers/{}/events/".format(organizer.slug),
|
||||
[
|
||||
"slug", "live", "meta_data", "seating_plan", "item_meta_properties"
|
||||
],
|
||||
expands=[]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_event_list_name_filter(token_client, organizer, event):
|
||||
|
||||
@@ -42,7 +42,6 @@ from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import scopes_disabled
|
||||
from tests.api.utils import _test_configurable_serializer
|
||||
from tests.const import SAMPLE_PNG
|
||||
|
||||
from pretix.base.models import (
|
||||
@@ -360,17 +359,10 @@ TEST_ITEM_RES = {
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_item_list(token_client, organizer, event, team, item, taxrule):
|
||||
def test_item_list(token_client, organizer, event, team, item):
|
||||
cat = event.categories.create(name="foo")
|
||||
cat2 = event.categories.create(name="bar")
|
||||
item.category = cat2
|
||||
item.tax_rule = taxrule
|
||||
item.save()
|
||||
res = dict(TEST_ITEM_RES)
|
||||
res["id"] = item.pk
|
||||
res["category"] = cat2.pk
|
||||
res["tax_rule"] = taxrule.pk
|
||||
res["tax_rate"] = "19.00"
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/'.format(organizer.slug, event.slug))
|
||||
assert resp.status_code == 200
|
||||
assert [res] == resp.data['results']
|
||||
@@ -408,11 +400,11 @@ def test_item_list(token_client, organizer, event, team, item, taxrule):
|
||||
assert resp.status_code == 200
|
||||
assert [] == resp.data['results']
|
||||
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?tax_rate=19'.format(organizer.slug, event.slug))
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?tax_rate=0'.format(organizer.slug, event.slug))
|
||||
assert resp.status_code == 200
|
||||
assert [res] == resp.data['results']
|
||||
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?tax_rate=0'.format(organizer.slug, event.slug))
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?tax_rate=19'.format(organizer.slug, event.slug))
|
||||
assert resp.status_code == 200
|
||||
assert [] == resp.data['results']
|
||||
|
||||
@@ -427,15 +419,6 @@ def test_item_list(token_client, organizer, event, team, item, taxrule):
|
||||
assert resp.status_code == 200
|
||||
assert [] == resp.data['results']
|
||||
|
||||
_test_configurable_serializer(
|
||||
token_client,
|
||||
"/api/v1/organizers/{}/events/{}/items/".format(organizer.slug, event.slug),
|
||||
[
|
||||
"name", "free_price", "variations",
|
||||
],
|
||||
expands=["category", "tax_rule"],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_item_detail(token_client, organizer, event, team, item):
|
||||
@@ -2445,6 +2428,45 @@ def test_question_update(token_client, organizer, event, question):
|
||||
assert question.type == "N"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_question_update_type_changes(token_client, organizer, event, question):
|
||||
# Allowed because no answers exist
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/questions/{}/'.format(organizer.slug, event.slug, question.pk),
|
||||
{
|
||||
"type": "B",
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
with scopes_disabled():
|
||||
question.answers.create(answer="12")
|
||||
|
||||
# Allowed change
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/questions/{}/'.format(organizer.slug, event.slug, question.pk),
|
||||
{
|
||||
"type": "S",
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Forbidden change
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/questions/{}/'.format(organizer.slug, event.slug, question.pk),
|
||||
{
|
||||
"type": "B",
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == ('{"type":["The system already contains answers to this question that are not '
|
||||
'compatible with changing the type of question without data loss. Consider hiding '
|
||||
'this question and creating a new one instead."]}')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_question_update_circular_dependency(token_client, organizer, event, question):
|
||||
with scopes_disabled():
|
||||
|
||||
@@ -31,7 +31,6 @@ from django.utils.timezone import now
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import scopes_disabled
|
||||
from stripe import error
|
||||
from tests.api.utils import _test_configurable_serializer
|
||||
from tests.plugins.stripe.test_checkout import apple_domain_create
|
||||
from tests.plugins.stripe.test_provider import MockedCharge
|
||||
|
||||
@@ -401,18 +400,13 @@ def test_order_list_filter_subevent_date(token_client, device, organizer, event,
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_list(token_client, organizer, event, order, item, team, taxrule, question, device):
|
||||
def test_order_list(token_client, organizer, event, order, item, taxrule, question, device):
|
||||
res = dict(TEST_ORDER_RES)
|
||||
with scopes_disabled():
|
||||
voucher = event.vouchers.create(code="FOO")
|
||||
opos = order.positions.first()
|
||||
opos.voucher = voucher
|
||||
opos.save()
|
||||
res["positions"][0]["id"] = order.positions.first().pk
|
||||
res["fees"][0]["id"] = order.fees.first().pk
|
||||
res["positions"][0]["print_logs"][0]["id"] = order.positions.first().print_logs.first().pk
|
||||
res["positions"][0]["print_logs"][0]["device_id"] = device.device_id
|
||||
res["positions"][0]["voucher"] = voucher.pk
|
||||
res["positions"][0]["item"] = item.pk
|
||||
res["positions"][0]["answers"][0]["question"] = question.pk
|
||||
res["last_modified"] = order.last_modified.isoformat().replace('+00:00', 'Z')
|
||||
@@ -520,22 +514,6 @@ def test_order_list(token_client, organizer, event, order, item, team, taxrule,
|
||||
assert resp.status_code == 200
|
||||
assert len(resp.data['results'][0]['fees']) == 2
|
||||
|
||||
_test_configurable_serializer(
|
||||
token_client,
|
||||
"/api/v1/organizers/{}/events/{}/orders/".format(organizer.slug, event.slug),
|
||||
[
|
||||
"status", "invoice_address.company", "fees.value", "payments.state",
|
||||
"positions.print_logs.type", "positions.answers.answer"
|
||||
],
|
||||
expands=["positions.voucher"],
|
||||
)
|
||||
|
||||
team.can_view_vouchers = False
|
||||
team.save()
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?expand=positions.voucher'.format(organizer.slug, event.slug))
|
||||
assert resp.status_code == 403
|
||||
assert resp.data["detail"] == "No permission to expand field positions.voucher"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_detail(token_client, organizer, event, order, item, taxrule, question):
|
||||
@@ -543,7 +521,6 @@ def test_order_detail(token_client, organizer, event, order, item, taxrule, ques
|
||||
with scopes_disabled():
|
||||
res["positions"][0]["id"] = order.positions.first().pk
|
||||
res["positions"][0]["print_logs"][0]["id"] = order.positions.first().print_logs.first().pk
|
||||
res["positions"][0]["print_logs"][0]["device_id"] = order.positions.first().print_logs.first().device_id
|
||||
res["fees"][0]["id"] = order.fees.first().pk
|
||||
res["positions"][0]["item"] = item.pk
|
||||
res["fees"][0]["tax_rule"] = taxrule.pk
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
#
|
||||
import pytest
|
||||
from django.core.files.base import ContentFile
|
||||
from tests.api.utils import _test_configurable_serializer
|
||||
from tests.const import SAMPLE_PNG
|
||||
|
||||
TEST_ORGANIZER_RES = {
|
||||
@@ -37,15 +36,6 @@ def test_organizer_list(token_client, organizer):
|
||||
assert resp.status_code == 200
|
||||
assert TEST_ORGANIZER_RES in resp.data['results']
|
||||
|
||||
_test_configurable_serializer(
|
||||
token_client,
|
||||
"/api/v1/organizers/",
|
||||
[
|
||||
"name", "public_url"
|
||||
],
|
||||
expands=[],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_organizer_detail(token_client, organizer):
|
||||
|
||||
@@ -26,7 +26,6 @@ from unittest import mock
|
||||
import pytest
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import scopes_disabled
|
||||
from tests.api.utils import _test_configurable_serializer
|
||||
|
||||
from pretix.base.models import (
|
||||
InvoiceAddress, ItemVariation, Order, OrderPosition, SeatingPlan, SubEvent,
|
||||
@@ -158,15 +157,6 @@ def test_subevent_list(token_client, organizer, event, subevent):
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['results'][0]['best_availability_state'] is None
|
||||
|
||||
_test_configurable_serializer(
|
||||
token_client,
|
||||
"/api/v1/organizers/{}/events/{}/subevents/".format(organizer.slug, event.slug),
|
||||
[
|
||||
"name", "active", "item_price_overrides",
|
||||
],
|
||||
expands=[]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_subevent_list_filter(token_client, organizer, event, subevent):
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io 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 urllib.parse
|
||||
|
||||
|
||||
def _add_params(url, params):
|
||||
url_parts = list(urllib.parse.urlparse(url))
|
||||
query = urllib.parse.parse_qs(url_parts[4])
|
||||
query = [*query, *params]
|
||||
url_parts[4] = urllib.parse.urlencode(query)
|
||||
return urllib.parse.urlunparse(url_parts)
|
||||
|
||||
|
||||
def _find_field_names(d: dict, path):
|
||||
names = set()
|
||||
for k, v in d.items():
|
||||
names.add(".".join([*path, k]))
|
||||
if isinstance(v, dict):
|
||||
names |= _find_field_names(v, path=(*path, k))
|
||||
elif isinstance(v, list) and len(v) > 0 and isinstance(v[0], dict):
|
||||
names |= _find_field_names(v[0], path=(*path, k))
|
||||
return names
|
||||
|
||||
|
||||
def _test_configurable_serializer(client, url, field_name_samples, expands):
|
||||
# Test include
|
||||
resp = client.get(_add_params(url, [("include", f) for f in field_name_samples]))
|
||||
if "results" in resp.data:
|
||||
o = resp.data["results"][0]
|
||||
else:
|
||||
o = resp.data
|
||||
|
||||
found_field_names = _find_field_names(o, tuple())
|
||||
# Assert no unexpected fields
|
||||
for f in found_field_names:
|
||||
depth = f.count(".")
|
||||
assert (f in field_name_samples or
|
||||
any(f.rsplit(".", c)[0] in field_name_samples for c in range(depth + 1)) or
|
||||
any(fn.startswith(f + ".") for fn in field_name_samples))
|
||||
# Assert all fields are there
|
||||
for f in field_name_samples:
|
||||
assert f in found_field_names, f"{f} not in {found_field_names}"
|
||||
|
||||
# Test exclude
|
||||
resp = client.get(_add_params(url, [("exclude", f) for f in field_name_samples]))
|
||||
if "results" in resp.data:
|
||||
o = resp.data["results"][0]
|
||||
else:
|
||||
o = resp.data
|
||||
found_field_names = _find_field_names(o, [])
|
||||
# Assert all fields are not there
|
||||
for f in found_field_names:
|
||||
assert f not in field_name_samples
|
||||
|
||||
# Test expand
|
||||
if expands:
|
||||
resp = client.get(_add_params(url, [("expand", f) for f in expands]))
|
||||
if "results" in resp.data:
|
||||
o = resp.data["results"][0]
|
||||
else:
|
||||
o = resp.data
|
||||
for e in expands:
|
||||
path = e.split(".")
|
||||
obj = o
|
||||
while len(path) > 1:
|
||||
obj = o[path[0]]
|
||||
if isinstance(obj, list):
|
||||
obj = obj[0]
|
||||
path = path[1:]
|
||||
assert isinstance(obj[path[0]], dict), f"{e} is not a dictionary, but {type(obj[path[0]])}"
|
||||
@@ -542,7 +542,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
|
||||
@override_settings(COMPRESS_PRECOMPILERS=settings.COMPRESS_PRECOMPILERS_ORIGINAL)
|
||||
def test_css_customized(self):
|
||||
response = self.client.get('/%s/%s/widget/v1.css' % (self.orga.slug, self.event.slug))
|
||||
response = self.client.get('/%s/%s/widget/v2.css' % (self.orga.slug, self.event.slug))
|
||||
c = b"".join(response.streaming_content).decode()
|
||||
assert '#8E44B3' in c
|
||||
assert '#33c33c' not in c
|
||||
@@ -550,7 +550,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
|
||||
self.orga.settings.primary_color = "#33c33c"
|
||||
self.orga.cache.clear()
|
||||
response = self.client.get('/%s/%s/widget/v1.css' % (self.orga.slug, self.event.slug))
|
||||
response = self.client.get('/%s/%s/widget/v2.css' % (self.orga.slug, self.event.slug))
|
||||
c = b"".join(response.streaming_content).decode()
|
||||
assert '#8E44B3' not in c
|
||||
assert '#33c33c' in c
|
||||
@@ -558,18 +558,18 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
|
||||
self.event.settings.primary_color = "#34c34c"
|
||||
self.event.cache.clear()
|
||||
response = self.client.get('/%s/%s/widget/v1.css' % (self.orga.slug, self.event.slug))
|
||||
response = self.client.get('/%s/%s/widget/v2.css' % (self.orga.slug, self.event.slug))
|
||||
c = b"".join(response.streaming_content).decode()
|
||||
assert '#8E44B3' not in c
|
||||
assert '#33c33c' not in c
|
||||
assert '#34c34c' in c
|
||||
|
||||
def test_js_localized(self):
|
||||
response = self.client.get('/widget/v1.en.js')
|
||||
response = self.client.get('/widget/v2.en.js')
|
||||
c = response.content.decode()
|
||||
assert '%m/%d/%Y' in c
|
||||
assert '%d.%m.%Y' not in c
|
||||
response = self.client.get('/widget/v1.de.js')
|
||||
response = self.client.get('/widget/v2.de.js')
|
||||
c = response.content.decode()
|
||||
assert '%m/%d/%Y' not in c
|
||||
assert '%d.%m.%Y' in c
|
||||
|
||||
Reference in New Issue
Block a user