Compare commits

..

17 Commits

Author SHA1 Message Date
Raphael Michel
c835e04edd PDF editor: Fix interference with areyousure.js (allow page leave after save) 2025-06-25 18:09:32 +02:00
Raphael Michel
e796dc3a65 Webhooks: Fix typo in retry interval 2025-06-25 16:46:52 +02:00
Richard Schreiber
545625b732 Fix failing flake8 2025-06-25 11:24:11 +02:00
Richard Schreiber
9bf302e5ae Widget: deprecate v1 and deliver v2 instead (#5273)
* Widget: deprecate v1 and redirect to v2

* Make redirect permanent

* remove v1 files

* do not redirect, just serve version_min

* add version-comment to delivered css/js-file

* fix tests
2025-06-25 11:20:34 +02:00
dependabot[bot]
0c7c50cffc Update sentry-sdk requirement from ==2.30.* to ==2.31.* (#5271)
---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.31.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-25 11:19:13 +02:00
조정화
2c094f4c30 Translations: Update Korean
Currently translated at 52.3% (3088 of 5900 strings)

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

powered by weblate
2025-06-25 10:56:57 +02:00
조정화
e820424bdf Translations: Update Korean
Currently translated at 100.0% (252 of 252 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ko/

powered by weblate
2025-06-25 10:56:57 +02:00
Raphael Michel
cb3d88a923 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5900 of 5900 strings)

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

powered by weblate
2025-06-25 10:56:57 +02:00
Raphael Michel
530ce06155 Translations: Update German
Currently translated at 100.0% (5900 of 5900 strings)

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

powered by weblate
2025-06-25 10:56:57 +02:00
Raphael Michel
9017128513 Webhooks: Fix retry logic (Z#23197527) (#5250)
* Webhooks: Fix retry logic (Z#23197527)

* Add no-op migration
2025-06-25 08:56:46 +02:00
Raphael Michel
5d3fc62ba4 Questions: Validate type changes (Z#23197118) (#5259)
* Questions: Validate type changes (Z#23197118)

* Update src/pretix/base/forms/questions.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/base/forms/questions.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/base/forms/questions.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/base/models/items.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Fix failing test

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2025-06-24 17:54:28 +02:00
dependabot[bot]
243db008e1 Bump markdown from 3.8 to 3.8.2 (#5266)
Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.8 to 3.8.2.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.8...3.8.2)

---
updated-dependencies:
- dependency-name: markdown
  dependency-version: 3.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-24 16:18:55 +02:00
dependabot[bot]
5ea9f819e6 Update css-inline requirement from ==0.14.* to ==0.15.* (#5267)
Updates the requirements on [css-inline](https://github.com/Stranger6667/css-inline) to permit the latest version.
- [Release notes](https://github.com/Stranger6667/css-inline/releases)
- [Changelog](https://github.com/Stranger6667/css-inline/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/css-inline/compare/c-v0.14.0...c-v0.15.0)

---
updated-dependencies:
- dependency-name: css-inline
  dependency-version: 0.15.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-24 16:18:41 +02:00
dependabot[bot]
a5eb009e55 Update flake8 requirement from ==7.2.* to ==7.3.* (#5268)
Updates the requirements on [flake8](https://github.com/pycqa/flake8) to permit the latest version.
- [Commits](https://github.com/pycqa/flake8/compare/7.2.0...7.3.0)

---
updated-dependencies:
- dependency-name: flake8
  dependency-version: 7.3.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-24 16:18:30 +02:00
dependabot[bot]
5129ed3846 Update webauthn requirement from ==2.5.* to ==2.6.* (#5269)
Updates the requirements on [webauthn](https://github.com/duo-labs/py_webauthn) to permit the latest version.
- [Release notes](https://github.com/duo-labs/py_webauthn/releases)
- [Changelog](https://github.com/duo-labs/py_webauthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/duo-labs/py_webauthn/compare/v2.5.0...v2.6.0)

---
updated-dependencies:
- dependency-name: webauthn
  dependency-version: 2.6.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-24 16:18:17 +02:00
Raphael Michel
f51906338f Order detail: Set correct language for invoice email (Z#23197863) (#5260) 2025-06-24 16:14:33 +02:00
Raphael Michel
d67e1116f4 Address forms: Add "federal entity" of Mexico to state list 2025-06-24 10:05:36 +02:00
38 changed files with 447 additions and 4319 deletions

View File

@@ -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
-----------

View File

@@ -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.

View File

@@ -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.

View File

@@ -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``.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.*",

View File

@@ -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(),
),
]

View File

@@ -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)

View File

@@ -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('.'))

View File

@@ -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']

View File

@@ -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)

View File

@@ -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):

View File

@@ -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))

View File

@@ -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):

View File

@@ -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):

View File

@@ -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(

View File

@@ -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,

View File

@@ -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)

View File

@@ -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'),

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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 theyre 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"

View File

@@ -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 %}

View File

@@ -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:

View File

@@ -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

View File

@@ -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):

View File

@@ -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():

View File

@@ -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

View File

@@ -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):

View File

@@ -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):

View File

@@ -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]])}"

View File

@@ -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