Compare commits

...
Author SHA1 Message Date
Raphael Michel b5ad4680c8 Devices: Allow admins to inspect raw info data 2026-07-16 16:39:33 +02:00
Raphael Michel 2660dfe1bc CSP: Hotfix connect-src to allow static file access for PDF editor 2026-07-16 10:23:44 +02:00
Raphael MichelandGitHub 08c376a459 API: Add raw_source_type to failed checkins update (#6272) 2026-07-15 18:06:58 +02:00
Gerasimos ChourdakisandGitHub 6901ed8b26 Event settings: Improve wording of help message for is_remote (#6386) 2026-07-15 17:46:15 +02:00
Llewelyn BarkhuizenandRaphael Michel 62f1cbadaf Translations: Update Afrikaans
Currently translated at 0.1% (3 of 6362 strings)

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

powered by weblate
2026-07-15 17:44:34 +02:00
szurofkamarciidfbe08444ef04788andRaphael Michel 1386b40e6f Translations: Update Hungarian
Currently translated at 100.0% (260 of 260 strings)

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

powered by weblate
2026-07-15 17:44:34 +02:00
szurofkamarciidfbe08444ef04788andRaphael Michel bbcd1b86b7 Translations: Update Hungarian
Currently translated at 17.8% (1138 of 6362 strings)

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

powered by weblate
2026-07-15 17:44:34 +02:00
Jochen SiebertandRaphael Michel 6894070239 Translations: Update German
Currently translated at 100.0% (6362 of 6362 strings)

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

powered by weblate
2026-07-15 17:44:34 +02:00
Llewelyn BarkhuizenandRaphael Michel 7470a7198b Translations: Add Afrikaans 2026-07-15 17:44:34 +02:00
Llewelyn BarkhuizenandRaphael Michel ddda634ef3 Translations: Add Afrikaans 2026-07-15 17:44:34 +02:00
szurofkamarciidfbe08444ef04788andRaphael Michel 3f7064f12b Translations: Update Hungarian
Currently translated at 100.0% (260 of 260 strings)

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

powered by weblate
2026-07-15 17:44:34 +02:00
szurofkamarciidfbe08444ef04788andRaphael Michel 6386c99175 Translations: Update Hungarian
Currently translated at 17.0% (1084 of 6362 strings)

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

powered by weblate
2026-07-15 17:44:34 +02:00
eb29101d09 Allow more decimal places for tax rates (#6092)
* Allow more decimal places for tax rates

* Fix normalization

* More tax rate handling

* Add uncommitted tests

* Rebase migration

* Update src/pretix/base/migrations/0304_tax_rate_decimals.py

Co-authored-by: robbi5 <richt@rami.io>

---------

Co-authored-by: robbi5 <richt@rami.io>
2026-07-15 16:20:45 +02:00
szurofkamarciidfbe08444ef04788andRaphael Michel d007fb9566 Translations: Update Hungarian
Currently translated at 100.0% (260 of 260 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
szurofkamarciidfbe08444ef04788andRaphael Michel 93cbbea193 Translations: Update Hungarian
Currently translated at 16.7% (1064 of 6362 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
szurofkamarciidfbe08444ef04788andRaphael Michel eacce6d1f8 Translations: Update Hungarian
Currently translated at 100.0% (260 of 260 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
szurofkamarciidfbe08444ef04788andRaphael Michel cdd7412fbb Translations: Update Hungarian
Currently translated at 13.2% (844 of 6362 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
szurofkamarciidfbe08444ef04788andRaphael Michel a6d462c14e Translations: Update Hungarian
Currently translated at 11.2% (717 of 6362 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
Hijiri UmemotoandRaphael Michel d5d928cdce Translations: Update Japanese
Currently translated at 100.0% (6362 of 6362 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
szurofkamarciidfbe08444ef04788andRaphael Michel e0a8b81290 Translations: Update Hungarian
Currently translated at 11.1% (711 of 6362 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
szurofkamarciidfbe08444ef04788andRaphael Michel 6b0419e488 Translations: Update Hungarian
Currently translated at 100.0% (260 of 260 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
szurofkamarciidfbe08444ef04788andRaphael Michel 48104e0c9f Translations: Update Hungarian
Currently translated at 10.8% (690 of 6362 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
ahmed badrandRaphael Michel 7eefb18b67 Translations: Update Arabic
Currently translated at 99.7% (6348 of 6362 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
ahmed badrandRaphael Michel f9022e5a5e Translations: Update Arabic
Currently translated at 100.0% (260 of 260 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
ahmed shabbatandRaphael Michel 08c5963d7b Translations: Update Arabic
Currently translated at 99.7% (6348 of 6362 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
ahmed badrandRaphael Michel e8274527cb Translations: Update Arabic
Currently translated at 99.7% (6348 of 6362 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
Raphael MichelandRaphael Michel f77aad0eeb Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (260 of 260 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
ahmed badrandRaphael Michel d5b0059c34 Translations: Update Arabic
Currently translated at 100.0% (260 of 260 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
ahmed badrandRaphael Michel 47a170ab62 Translations: Update Arabic
Currently translated at 78.6% (5005 of 6362 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
ahmed shabbatandRaphael Michel 93aebffe2c Translations: Update Arabic
Currently translated at 65.8% (4191 of 6362 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
ahmed badrandRaphael Michel 7795c711be Translations: Update Arabic
Currently translated at 65.8% (4191 of 6362 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
ahmed badrandRaphael Michel 1eb685d4ee Translations: Update Arabic
Currently translated at 59.8% (3808 of 6362 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
CVZ-esandRaphael Michel b8c523cfac Translations: Update Spanish
Currently translated at 100.0% (6362 of 6362 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
Joram SchwartzmannandRaphael Michel fae9fff98e Translations: Update French
Currently translated at 100.0% (260 of 260 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
CVZ-esandRaphael Michel 92cfbd54b9 Translations: Update French
Currently translated at 100.0% (6362 of 6362 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
Joram SchwartzmannandRaphael Michel 18203e0804 Translations: Update French
Currently translated at 100.0% (6362 of 6362 strings)

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

powered by weblate
2026-07-15 11:39:34 +02:00
Raphael MichelandGitHub 52ed8eeb50 Mail setup: Add DKIM + DMARC validation (#6339)
* Mail setup: Add DKIM + DMARC validation

* Tests

* Test fixes
2026-07-15 11:36:35 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1e4888bad0 Bump django-filter from 25.1 to 26.1 (#6377)
Bumps [django-filter](https://github.com/carltongibson/django-filter) from 25.1 to 26.1.
- [Release notes](https://github.com/carltongibson/django-filter/releases)
- [Changelog](https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst)
- [Commits](https://github.com/carltongibson/django-filter/compare/25.1...26.1)

---
updated-dependencies:
- dependency-name: django-filter
  dependency-version: '26.1'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-15 09:41:35 +02:00
dependabot[bot]andRaphael Michel c3056a8aae Update sentry-sdk requirement from ==2.64.* to ==2.65.*
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.64.0...2.65.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.65.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-15 09:41:18 +02:00
Raphael MichelandGitHub 4a4cff3c41 Drop all csp_ignore statements from core (except stripe, for now) (#6337)
* Add proper CSP policy for HTML email preview

* Handle safe types centrally

* Precompile schema validation function to get rid of ajv2020

* Fix tests

* adjust more tests
2026-07-15 09:39:42 +02:00
Raphael MichelandGitHub ffca102a8a Question form: Do not store invalid False values for required boolean questions (Z#23239887) (#6366) 2026-07-10 16:42:10 +02:00
luelistaandGitHub 78f8830f90 Hotfix: fix order_position_buttons receiver 2026-07-10 15:08:10 +02:00
Raphael Michel eb594266a7 Hotfix: Add missing mark_safe after strip 2026-07-10 14:56:48 +02:00
76e6803eac Fix customer views using wrong csrf middleware (#6027)
* Fix customer views using wrong csrf middleware

This lead to persistent csrf validation errors if the token from the cookie expired, which could only be solved by clearing cookies.

* Remove unneccesary csrf_protect decorators

* Fix typo

Co-authored-by: Raphael Michel <michel@pretix.eu>

---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-07-10 13:25:40 +02:00
Richard Schreiberandpajowu e19908571c Fix delete_cookie for partitioned legacy CSRF cookie 2026-07-10 13:25:40 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
779ab360df Bump django-formtools from 2.6.1 to 2.7 (#6371)
Bumps [django-formtools](https://github.com/jazzband/django-formtools) from 2.6.1 to 2.7.
- [Changelog](https://github.com/jazzband/django-formtools/blob/master/docs/changelog.rst)
- [Commits](https://github.com/jazzband/django-formtools/compare/2.6.1...2.7)

---
updated-dependencies:
- dependency-name: django-formtools
  dependency-version: '2.7'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 12:17:12 +02:00
Raphael MichelandGitHub 723c63008d Team invite: Improve error message (Z#23239860) (#6368) 2026-07-10 12:13:44 +02:00
15c194c0a3 Add base class for historical payment providers (#6336)
* Add base class for historical payment providers

* Update src/pretix/base/payment.py

Co-authored-by: pajowu <engelhardt@pretix.eu>

---------

Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-07-10 11:10:32 +02:00
pajowuandGitHub 7f847c3dd2 Fix TypeError if task.request.headers is None (#6367) 2026-07-09 18:52:49 +02:00
luelistaandGitHub 430c6dd269 Use SafeStrings for plugin signals returning HTML that should be rendered (#6343)
As a security precaution, we change the contract of some signals such that a 
SafeString needs to be returned if HTML should be rendered without further 
escaping.

Before, the `{% signal ... %}` and `{% eventsignal ... %}` template tags 
called mark_safe themselves on all strings returned from signals. That could
lead to unsafe coding practices, where untrusted values are interpolated into
HTML format strings. However, such interpolations should usually be performed 
using helpers such Django's format_html, which automatically escapes inputs
and returns a SafeString.
Now, we call conditional_escape on signal results, so that any HTML not explicitly
marked as safe gets escaped.

Most plugins are not affected by this change as they return a SafeString as a 
result of Template.render already.
2026-07-09 17:50:26 +02:00
Raphael Michel 6411648457 Revert "Update redis requirement from ==7.4.* to ==8.0.* (#6226)"
This reverts commit 005b3864b1.
2026-07-09 00:45:32 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Raphael MichelRaphael Michelluelista
005b3864b1 Update redis requirement from ==7.4.* to ==8.0.* (#6226)
* Update redis requirement from ==7.4.* to ==8.0.*

Updates the requirements on [redis](https://github.com/redis/redis-py) to permit the latest version.
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/redis/redis-py/compare/v7.4.0...v8.0.0)

---
updated-dependencies:
- dependency-name: redis
  dependency-version: 8.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Remove setex calls

* Update src/pretix/presale/views/user.py

Co-authored-by: luelista <weller@rami.io>

* Fix tests

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Raphael Michel <michel@pretix.eu>
Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: luelista <weller@rami.io>
2026-07-08 12:07:54 +02:00
Raphael MichelandGitHub 1ee1c604cf Tax rules: Correctly write log when changing custom rules (#6358) 2026-07-08 11:33:06 +02:00
luelistaandGitHub e425105dbf Fix link from voucher tag to filtered voucher list (#6362) 2026-07-08 11:26:37 +02:00
Martin Gross 738b375042 DevDocs: Bump nodejs to 24 2026-07-08 10:53:14 +02:00
sweenuandGitHub d09572d999 Remove leftovers from contact url in quick setup (#6359) 2026-07-07 19:23:48 +02:00
Lukas BockstallerandGitHub 634754aacb repair sendmail rules so we don't send emails to all positions in order independent of checkin status (Z#23235255) (#6340)
* add repro for Z#23235255

* add testcase for fallback to order.email

* only send to orderpositions included in op_qs

* codestyle
2026-07-07 17:46:52 +02:00
14c3baa2aa Drop nullability on Order.organizer and OrderPosition.organizer (#4278)
* Drop nullability on Order.organizer and OrderPosition.organizer

* Rebase migration and add autoclean

* Utilize new relationship for scopes

* Declare reverse noop

* Update src/pretix/base/migrations/0302_resolve_duplicate_codes_and_secrets.py

Co-authored-by: Martin Gross <gross@rami.io>

* Update src/pretix/base/migrations/0302_resolve_duplicate_codes_and_secrets.py

Co-authored-by: Martin Gross <gross@rami.io>

---------

Co-authored-by: Martin Gross <gross@rami.io>
2026-07-07 17:44:19 +02:00
77 changed files with 40350 additions and 8222 deletions
+2 -1
View File
@@ -351,7 +351,8 @@ Endpoints
:<json boolean error_reason: One of ``canceled``, ``invalid``, ``unpaid``, ``product``, ``rules``, ``revoked``,
``incomplete``, ``already_redeemed``, ``blocked``, ``invalid_time``, or ``error``. Required.
:<json raw_barcode: The raw barcode you scanned. Required.
:<json raw_barcode: The raw barcode or identifier you scanned. Required.
:<json raw_source_type: The type of medium you scanned, defaults to ``barcode``. Optional.
:<json datetime: Date and time of the scan. Optional.
:<json type: Type of scan, defaults to ``"entry"``.
:<json position: Internal ID of an order position you matched. Optional.
+1 -1
View File
@@ -53,7 +53,7 @@ Working with the code
---------------------
If you do not have a recent installation of ``nodejs``, install it now::
curl -sL https://deb.nodesource.com/setup_17.x | sudo -E bash -
curl -sL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install nodejs
To make sure it is on your path variable, close and reopen your terminal. Now, install the Python-level dependencies of pretix::
+3 -3
View File
@@ -41,9 +41,9 @@ dependencies = [
"django-bootstrap3==26.1",
"django-compressor==4.6.0",
"django-countries==8.2.*",
"django-filter==25.1",
"django-filter==26.1",
"django-formset-js-improved==0.5.0.5",
"django-formtools==2.6.1",
"django-formtools==2.7",
"django-hierarkey==2.0.*,>=2.0.1",
"django-hijack==3.7.*",
"django-i18nfield==1.11.*",
@@ -94,7 +94,7 @@ dependencies = [
"redis==7.4.*",
"reportlab==5.0.*",
"requests==2.34.*",
"sentry-sdk==2.64.*",
"sentry-sdk==2.65.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
+28
View File
@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
import json
import re
from django.db.models import prefetch_related_objects
from rest_framework import serializers
@@ -135,3 +136,30 @@ class SalesChannelMigrationMixin:
else:
value["sales_channels"] = value["limit_sales_channels"]
return value
class CompatDecimalField(serializers.DecimalField):
"""
Historically, pretix recorded tax rates as decimals with two places. Today, pretix supports tax rates with up to
four places. Since our API outputs decimals with the stored precision, this would have changed the API output from
"19.00" to "19.0000" without warning. While this is semantically the same thing, we need to assume some pretix API
users might run into trouble, either because they treat the value as a string and then map something
(e.g. ``if tax_rate == "19.00"``) or process it with a language where this is a significant difference. For example,
while in Python ``Decimal("19.00") == Decimal("19.0000")`` is true, in Java
``(new BigDecimal("19.00")).equals(new BigDecimal("19.0000"))`` is false and only
``(new BigDecimal("19.00")).compareTo(new BigDecimal("19.0000")) == 0`` is true.
Therefore, we stay backwards compatible by outputting two decimal places *as long as the trailing digits are zero-valued.
"""
regex = re.compile(r"^([0-9]+\.[0-9]{2})0+$")
def to_representation(self, value):
if self.localize:
raise ValueError("localization not supported")
value = super().to_representation(value)
if value and "." not in value:
return f"{value}.00"
if m := self.regex.match(value):
return m.group(1)
return value
+2 -1
View File
@@ -48,7 +48,7 @@ from rest_framework.fields import ChoiceField, Field
from rest_framework.relations import SlugRelatedField
from pretix.api.serializers import (
CompatibleJSONField, SalesChannelMigrationMixin,
CompatDecimalField, CompatibleJSONField, SalesChannelMigrationMixin,
)
from pretix.api.serializers.fields import PluginsField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
@@ -681,6 +681,7 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
required=False,
allow_null=True,
)
rate = CompatDecimalField(max_digits=7, decimal_places=4)
class Meta:
model = TaxRule
+6 -4
View File
@@ -42,7 +42,9 @@ 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 SalesChannelMigrationMixin
from pretix.api.serializers import (
CompatDecimalField, SalesChannelMigrationMixin,
)
from pretix.api.serializers.event import MetaDataField
from pretix.api.serializers.fields import UploadedFileField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
@@ -276,10 +278,10 @@ class ItemAddOnSerializer(serializers.ModelSerializer):
return value
class ItemTaxRateField(serializers.Field):
class ItemTaxRateField(CompatDecimalField):
def to_representation(self, i):
if i.tax_rule:
return str(Decimal(i.tax_rule.rate))
return super().to_representation(Decimal(i.tax_rule.rate))
else:
return str(Decimal('0.00'))
@@ -289,7 +291,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
bundles = InlineItemBundleSerializer(many=True, required=False)
variations = InlineItemVariationSerializer(many=True, required=False)
program_times = InlineItemProgramTimeSerializer(many=True, required=False)
tax_rate = ItemTaxRateField(source='*', read_only=True)
tax_rate = ItemTaxRateField(source='*', read_only=True, max_digits=7, decimal_places=4)
meta_data = MetaDataField(required=False, source='*')
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
'image/png', 'image/jpeg', 'image/gif'
+9 -2
View File
@@ -41,7 +41,7 @@ from rest_framework.exceptions import ValidationError
from rest_framework.relations import SlugRelatedField
from rest_framework.reverse import reverse
from pretix.api.serializers import CompatibleJSONField
from pretix.api.serializers import CompatDecimalField, CompatibleJSONField
from pretix.api.serializers.event import SubEventSerializer
from pretix.api.serializers.forms import form_field_to_serializer_field
from pretix.api.serializers.i18n import I18nAwareModelSerializer
@@ -52,6 +52,7 @@ from pretix.api.signals import order_api_details, orderposition_api_details
from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.invoicing.transmission import get_transmission_types
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import (
CachedFile, Checkin, Customer, Device, GiftCard, Invoice, InvoiceAddress,
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
@@ -381,6 +382,7 @@ class PrintLogSerializer(serializers.ModelSerializer):
class FailedCheckinSerializer(I18nAwareModelSerializer):
error_reason = serializers.ChoiceField(choices=Checkin.REASONS, required=True, allow_null=False)
raw_barcode = serializers.CharField(required=True, allow_null=False)
raw_source_type = serializers.ChoiceField(choices=[(k, v) for k, v in MEDIA_TYPES.items()], default='barcode')
position = serializers.PrimaryKeyRelatedField(queryset=OrderPosition.all.none(), required=False, allow_null=True)
raw_item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
raw_variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
@@ -390,7 +392,7 @@ class FailedCheckinSerializer(I18nAwareModelSerializer):
class Meta:
model = Checkin
fields = ('error_reason', 'error_explanation', 'raw_barcode', 'raw_item', 'raw_variation',
'raw_subevent', 'nonce', 'datetime', 'type', 'position')
'raw_subevent', 'raw_source_type', 'nonce', 'datetime', 'type', 'position')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -591,6 +593,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
country = CompatibleCountryField(source='*')
attendee_name = serializers.CharField(required=False)
plugin_data = OrderPositionPluginDataField(source='*', allow_null=True, read_only=True)
tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
class Meta:
list_serializer_class = OrderPositionListSerializer
@@ -747,6 +750,8 @@ class OrderPaymentDateField(serializers.DateField):
class OrderFeeSerializer(I18nAwareModelSerializer):
tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
class Meta:
model = OrderFee
fields = ('id', 'fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule',
@@ -1911,6 +1916,7 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
position = LinePositionField(read_only=True)
event_date_from = serializers.DateTimeField(read_only=True, source="period_start")
event_date_to = serializers.DateTimeField(read_only=True, source="period_end")
tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
class Meta:
model = InvoiceLine
@@ -1994,6 +2000,7 @@ class BlockedTicketSecretSerializer(I18nAwareModelSerializer):
class TransactionSerializer(I18nAwareModelSerializer):
order = serializers.SlugRelatedField(slug_field="code", read_only=True)
tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
class Meta:
model = Transaction
+7
View File
@@ -1116,6 +1116,13 @@ class BaseQuestionsForm(forms.Form):
if q.dependency_question_id and not question_is_visible(q.dependency_question_id, q.dependency_values) and answer is not None:
d['question_%d' % q.pk] = None
# Strip False answers to required yes/no questions even if all_optional is set, as our data model assumes that
# required yes/no questions can only be answered with yes
for q in question_cache.values():
if q.required and q.type == Question.TYPE_BOOLEAN:
if 'question_%d' % q.pk in d and d['question_%d' % q.pk] is False:
del d['question_%d' % q.pk]
return d
+22 -1
View File
@@ -266,6 +266,22 @@ def _merge_csp(a, b):
class SecurityMiddleware(MiddlewareMixin):
SAFE_TYPES = (
# CSP policies are only used for:
# - HTML and SVG in top-level contexts
# - SVG or JS Workers delivered in embedded contexts
# See: https://www.w3.org/TR/CSP2/#which-policy-applies
# Therefore, we can save bandwidth on not including our (sometimes huge) policy
# on API responses or CSS. We do however include it with other types as a precaution
# (whitelist instead of blacklist) and we also do not whitelist JavaScript in
# we ever add service workers to not break the protection of this feature:
# https://www.w3.org/TR/CSP2/#sandboxing-and-workers
'application/json',
'text/css',
# We used to skip CSP for PDF since it was necessary for inline previews in Safari,
# but at the moment it does not seem to be an issue to just send it.
)
def process_response(self, request, resp):
if settings.DEBUG and resp.status_code >= 400:
# Don't use CSP on debug error page as it breaks of Django's fancy error
@@ -277,6 +293,11 @@ class SecurityMiddleware(MiddlewareMixin):
# https://github.com/pretix/pretix/issues/765
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
if "Content-Type" in resp and resp["Content-Type"].split(";")[0] in self.SAFE_TYPES:
if 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']
return resp
if not getattr(resp, '_csp_ignore', False):
resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp))
elif 'Content-Security-Policy' in resp:
@@ -293,7 +314,7 @@ class SecurityMiddleware(MiddlewareMixin):
'object-src': ["'none'"],
'frame-src': ['{static}'],
'style-src': ["{static}", "{media}"],
'connect-src': ["{dynamic}", "{media}"],
'connect-src': ["{static}", "{dynamic}", "{media}"],
'img-src': ["{static}", "{media}", "data:"],
'font-src': ["{static}"],
'media-src': ["{static}", "data:"],
@@ -0,0 +1,58 @@
# Generated by Django 4.2.8 on 2024-07-01 09:27
import logging
from django.db import migrations
from django.db.models import Count
logger = logging.getLogger(__name__)
def clean_duplicate_secrets(apps, schema_editor):
# This will autofix all possible duplicate Order.code and OrderPosition.secret values,
# unless Order.code is already too long to append something. This would need to be fixed by
# sysadmins manually.
OrderPosition = apps.get_model("pretixbase", "OrderPosition")
Order = apps.get_model("pretixbase", "Order")
qs = OrderPosition.all.values("secret", "order__event__organizer_id").order_by().annotate(c=Count("*")).filter(c__gt=1)
for row in qs:
affected = OrderPosition.all.filter(
**{k: v for k, v in row.items() if k != "c"}
).order_by("pk")
logger.error(f"Found {row['c']} tickets with with the same secret \"{row['secret']}\" in organizer {row['order__event__organizer_id']}, all except one will be changed")
for i, a in enumerate(affected):
if i > 0:
a.secret = a.secret + "__dupl__" + str(a.pk)
logger.info(
f"Ticket {a.pk} has new secret {a.secret}"
)
a.save(update_fields=["organizer_id", "secret"])
qs = Order.objects.values("code", "event__organizer_id").order_by().annotate(c=Count("*")).filter(c__gt=1)
for row in qs:
affected = Order.objects.filter(
**{k: v for k, v in row.items() if k != "c"}
).order_by("pk")
logger.error(f"Found {row['c']} orders with with the same code \"{row['code']}\" in organizer {row['event__organizer_id']}, all except one will be changed")
for i, a in enumerate(affected):
if i > 0:
if len(a.code) > 16 - len(str(a.pk)):
raise ValueError(f"Cannot auto-fix order with duplicate code {a.code}, order code is too long already")
a.code = a.code + str(a.pk).zfill(16 - len(a.code))
logger.info(
f"Order {a.pk} has new code {a.code}"
)
a.save(update_fields=["organizer_id", "code"])
class Migration(migrations.Migration):
dependencies = [
(
"pretixbase",
"0301_reusablemedium_remove_orderposition",
),
]
operations = [
migrations.RunPython(clean_duplicate_secrets, migrations.RunPython.noop),
]
@@ -0,0 +1,46 @@
# Generated by Django 4.2.8 on 2024-07-01 09:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"pretixbase",
"0302_resolve_duplicate_codes_and_secrets",
),
]
operations = [
migrations.RunSQL(
"UPDATE pretixbase_order "
"SET organizer_id = (SELECT e.organizer_id FROM pretixbase_event e WHERE e.id = pretixbase_order.event_id) "
"WHERE pretixbase_order.organizer_id IS NULL;",
migrations.RunSQL.noop,
),
migrations.RunSQL(
"UPDATE pretixbase_orderposition "
"SET organizer_id = (SELECT e.organizer_id FROM pretixbase_order o LEFT JOIN pretixbase_event e ON e.id = o.event_id WHERE o.id = pretixbase_orderposition.order_id) "
"WHERE pretixbase_orderposition.organizer_id IS NULL;",
migrations.RunSQL.noop,
),
migrations.AlterField(
model_name="order",
name="organizer",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="orders",
to="pretixbase.organizer",
),
),
migrations.AlterField(
model_name="orderposition",
name="organizer",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="order_positions",
to="pretixbase.organizer",
),
),
]
@@ -0,0 +1,48 @@
# Generated by Django 5.2.12 on 2026-04-15 20:10
from decimal import Decimal
from django.db import migrations, models
import pretix.helpers.models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0303_alter_order_organizer_alter_orderposition_organizer'),
]
operations = [
migrations.AlterField(
model_name='cartposition',
name='tax_rate',
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, default=Decimal('0'), max_digits=7),
),
migrations.AlterField(
model_name='invoiceline',
name='tax_rate',
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, default=Decimal('0'), max_digits=7),
),
migrations.AlterField(
model_name='orderfee',
name='tax_rate',
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
),
migrations.AlterField(
model_name='orderposition',
name='tax_rate',
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
),
migrations.AlterField(
model_name='transaction',
name='tax_rate',
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
),
migrations.AlterField(
model_name='taxrule',
name='rate',
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
),
]
+7 -10
View File
@@ -649,7 +649,7 @@ class Event(EventMixin, LoggedModel):
is_remote = models.BooleanField(
default=False,
verbose_name=_("This event is remote or partially remote."),
help_text=_("This will be used to let users know if the event is in a different timezone and lets us calculate users local times."),
help_text=_("This will be used to let users know if the event is in a different timezone, and to let us calculate the local time of a user."),
)
geo_lat = models.FloatField(
verbose_name=_("Latitude"),
@@ -1403,15 +1403,12 @@ class Event(EventMixin, LoggedModel):
for mp in self.organizer.meta_properties.all():
if mp.required and not self.meta_data.get(mp.name):
issues.append(
('<a {a_attr}>' + gettext('You need to fill the meta parameter "{property}".') + '</a>').format(
property=mp.name,
a_attr='href="%s#id_prop-%d-value"' % (
reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}),
mp.pk
)
)
)
issues.append(format_html(
'<a href="{href}{href_hash}">{text}</a>',
text=gettext('You need to fill the meta parameter "{property}".').format(property=mp.name),
href=reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}),
href_hash=f'#id_prop-{mp.pk}-value',
))
responses = event_live_issues.send(self)
for receiver, response in sorted(responses, key=lambda r: str(r[0])):
+2 -1
View File
@@ -49,6 +49,7 @@ from django_scopes import ScopedManager
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
from pretix.helpers.countries import FastCountryField
from pretix.helpers.models import NormalizedDecimalField
def invoice_filename(instance, filename: str) -> str:
@@ -450,7 +451,7 @@ class InvoiceLine(models.Model):
description = models.TextField()
gross_value = models.DecimalField(max_digits=13, decimal_places=2)
tax_value = models.DecimalField(max_digits=13, decimal_places=2, default=Decimal('0.00'))
tax_rate = models.DecimalField(max_digits=7, decimal_places=2, default=Decimal('0.00'))
tax_rate = NormalizedDecimalField(max_digits=7, decimal_places=4, default=Decimal('0'))
tax_name = models.CharField(max_length=190)
tax_code = models.CharField(max_length=190, null=True, blank=True)
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
+11 -14
View File
@@ -87,6 +87,7 @@ from pretix.base.timemachine import time_machine_now
from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.models import NormalizedDecimalField
from ...helpers.names import build_name
from ...testutils.middleware import debugflags_var
from ._transactions import (
@@ -224,8 +225,6 @@ class Order(LockModel, LoggedModel):
"Organizer",
related_name="orders",
on_delete=models.CASCADE,
null=True,
blank=True,
)
event = models.ForeignKey(
Event,
@@ -329,7 +328,7 @@ class Order(LockModel, LoggedModel):
default="line",
)
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='event__organizer')
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='organizer')
class Meta:
verbose_name = _("Order")
@@ -2356,8 +2355,8 @@ class OrderFee(RoundingCorrectionMixin, models.Model):
)
description = models.CharField(max_length=190, blank=True)
internal_type = models.CharField(max_length=255, blank=True)
tax_rate = models.DecimalField(
max_digits=7, decimal_places=2,
tax_rate = NormalizedDecimalField(
max_digits=7, decimal_places=4,
verbose_name=_('Tax rate')
)
tax_rule = models.ForeignKey(
@@ -2541,8 +2540,6 @@ class OrderPosition(AbstractPosition):
"Organizer",
related_name="order_positions",
on_delete=models.CASCADE,
null=True,
blank=True,
)
order = models.ForeignKey(
Order,
@@ -2555,8 +2552,8 @@ class OrderPosition(AbstractPosition):
max_digits=13, decimal_places=2, null=True, blank=True,
)
tax_rate = models.DecimalField(
max_digits=7, decimal_places=2,
tax_rate = NormalizedDecimalField(
max_digits=7, decimal_places=4,
verbose_name=_('Tax rate')
)
tax_rule = models.ForeignKey(
@@ -2599,7 +2596,7 @@ class OrderPosition(AbstractPosition):
blank=True,
)
all = ScopedManager(organizer='order__event__organizer')
all = ScopedManager(organizer='organizer')
objects = ActivePositionManager()
def __init__(self, *args, **kwargs):
@@ -3074,8 +3071,8 @@ class Transaction(models.Model):
price_includes_rounding_correction = models.DecimalField(
max_digits=13, decimal_places=2, default=Decimal("0.00")
)
tax_rate = models.DecimalField(
max_digits=7, decimal_places=2,
tax_rate = NormalizedDecimalField(
max_digits=7, decimal_places=4,
verbose_name=_('Tax rate')
)
tax_rule = models.ForeignKey(
@@ -3190,8 +3187,8 @@ class CartPosition(AbstractPosition):
verbose_name=_("Limit for extending expiration date"),
null=True
)
tax_rate = models.DecimalField(
max_digits=7, decimal_places=2, default=Decimal('0.00'),
tax_rate = NormalizedDecimalField(
max_digits=7, decimal_places=4, default=Decimal('0'),
verbose_name=_('Tax rate')
)
tax_code = models.CharField(
+4 -3
View File
@@ -40,6 +40,7 @@ from pretix.base.decimal import round_decimal
from pretix.base.models.base import LoggedModel
from pretix.base.templatetags.money import money_filter
from pretix.helpers.countries import FastCountryField
from pretix.helpers.models import NormalizedDecimalField
class TaxedPrice:
@@ -335,9 +336,9 @@ class TaxRule(LoggedModel):
max_length=190,
choices=TAX_CODE_LISTS,
)
rate = models.DecimalField(
max_digits=10,
decimal_places=2,
rate = NormalizedDecimalField(
max_digits=7,
decimal_places=4,
validators=[
MaxValueValidator(
limit_value=Decimal("100.00"),
+52
View File
@@ -1706,6 +1706,58 @@ class GiftCardPayment(BasePaymentProvider):
)
class BaseHistoricalPaymentProvider(BasePaymentProvider):
"""
Base class for payment providers that no longer exist but can't be deleted to make sure historical
payments are shown correctly.
Subclasses are recommended to only implement:
- identifier
- verbose_name
- public_name
- payment_control_render
- payment_control_render_short
- refund_control_render
- refund_control_render_short
- render_invoice_text
- render_invoice_stamp
- api_payment_details
- api_refund_details
- shred_payment_info
- matching_id
- refund_matching_id
"""
@property
def is_enabled(self) -> bool:
return False
@property
def settings_form_fields(self) -> dict:
return {}
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
return False
def payment_is_valid_session(self, request: HttpRequest, payment: OrderPayment):
return False
def order_change_allowed(self, order: Order, request: HttpRequest=None) -> bool:
return False
def payment_refund_supported(self, payment: OrderPayment) -> bool:
return False
def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
return False
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
raise PaymentException(_("This payment provider exists for historical purposes only and is no longer usable."))
def execute_refund(self, refund: OrderRefund):
raise PaymentException(_("This payment provider exists for historical purposes only and is no longer usable."))
@receiver(register_payment_providers, dispatch_uid="payment_free")
def register_payment_provider(sender, **kwargs):
return [FreeOrderProvider, BoxOfficeProvider, OffsettingProvider, ManualPayment, GiftCardPayment]
+3 -2
View File
@@ -535,8 +535,9 @@ EventPluginRegistry = PluginAwareRegistry # for backwards compatibility
event_live_issues = EventPluginSignal()
"""
This signal is sent out to determine whether an event can be taken live. If you want to
prevent the event from going live, return a string that will be displayed to the user
as the error message. If you don't, your receiver should return ``None``.
prevent the event from going live, return an error message to display to the user (either
as a SafeString containing HTML, or a string that will be HTML-escaped). If you don't,
your receiver should return ``None``.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
+3 -2
View File
@@ -22,6 +22,7 @@
import importlib
from django import template
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
from pretix.base.models import Event
@@ -44,7 +45,7 @@ def eventsignal(event: Event, signame: str, **kwargs):
_html = []
for receiver, response in signal.send(event, **kwargs):
if response:
_html.append(response)
_html.append(conditional_escape(response))
return mark_safe("".join(_html))
@@ -63,5 +64,5 @@ def signal(signame: str, request, **kwargs):
_html = []
for receiver, response in signal.send(request, **kwargs):
if response:
_html.append(response)
_html.append(conditional_escape(response))
return mark_safe("".join(_html))
+18
View File
@@ -26,6 +26,8 @@ from babel.numbers import format_currency
from django import template
from django.conf import settings
from django.template.defaultfilters import floatformat
from django.utils import formats
from django.utils.safestring import mark_safe
from pretix.base.i18n import get_babel_locale
@@ -82,3 +84,19 @@ def money_numberfield_filter(value: Decimal, arg=''):
places = settings.CURRENCY_PLACES.get(arg, 2)
return str(value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP))
@register.filter(is_safe=True)
def tax_rate_format(number):
"""
Display a Decimal to its significant decimal places, used for tax rates.
"""
assert isinstance(number, Decimal)
return mark_safe(
formats.number_format(
number.normalize(),
-number.as_tuple().exponent,
use_l10n=True,
force_grouping=False,
)
)
+1 -1
View File
@@ -27,11 +27,11 @@ from django.template import TemplateDoesNotExist, loader
from django.template.loader import get_template
from django.utils.functional import Promise
from django.utils.translation import gettext as _
from django.views.decorators.csrf import requires_csrf_token
from sentry_sdk import last_event_id
from pretix.base.i18n import language
from pretix.base.middleware import get_language_from_request
from pretix.multidomain.middlewares import requires_csrf_token
def csrf_failure(request, reason=""):
+1 -1
View File
@@ -59,7 +59,7 @@ def on_task_prerun(sender, task_id, task, **kwargs):
from pretix.helpers.logs import local
local.request_id = task_id
if "X-Pretix-Trace" in task.request.headers:
if task.request.headers and "X-Pretix-Trace" in task.request.headers:
local.trace = task.request.headers["X-Pretix-Trace"].split(" ")
else:
local.trace = []
-6
View File
@@ -1905,12 +1905,6 @@ class QuickSetupForm(I18nForm):
required=False,
help_text=_("We'll show this publicly to allow attendees to contact you.")
)
contact_url = forms.URLField(
label=_("Contact URL"),
required=False,
help_text=_("If you set this, the footer contact link will point here instead of using the email address above. "
"Please note that you still need to add a contact email address that will be shared with all emails you send.")
)
total_quota = forms.IntegerField(
label=_("Total capacity"),
min_value=0,
+12 -4
View File
@@ -39,7 +39,8 @@ from pretix.base.signals import (
html_page_start = GlobalSignal()
"""
This signal allows you to put code in the beginning of the main page for every
page in the backend. You are expected to return HTML.
page in the backend. You are expected to return a SafeString containing HTML, or
a string that will be HTML-escaped.
The ``sender`` keyword argument will contain the request.
"""
@@ -129,7 +130,7 @@ event_dashboard_top = EventPluginSignal()
Arguments: 'request'
This signal is sent out to include custom HTML in the top part of the the event dashboard.
Receivers should return HTML.
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
An additional keyword argument ``subevent`` *can* contain a sub-event.
@@ -172,6 +173,7 @@ Arguments: 'form'
This signal allows you to add additional HTML to the form that is used for modifying vouchers.
You receive the form object in the ``form`` keyword argument.
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -209,6 +211,7 @@ Arguments: 'quota'
This signal allows you to append HTML to a Quota's detail view. You receive the
quota as argument in the ``quota`` keyword argument.
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -219,6 +222,7 @@ Arguments: 'subevent'
This signal allows you to append HTML to a SubEvent's detail view. You receive the
subevent as argument in the ``subevent`` keyword argument.
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -265,7 +269,8 @@ order_info = EventPluginSignal()
"""
Arguments: ``order``, ``request``
This signal is sent out to display additional information on the order detail page
This signal is sent out to display additional information on the order detail page.
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
Additionally, the argument ``order`` and ``request`` are available.
@@ -275,7 +280,8 @@ order_approve_info = EventPluginSignal()
"""
Arguments: ``order``, ``request``
This signal is sent out to display additional information on the order approve page
This signal is sent out to display additional information on the order approve page.
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
Additionally, the argument ``order`` and ``request`` are available.
@@ -286,6 +292,7 @@ order_position_buttons = EventPluginSignal()
Arguments: ``order``, ``position``, ``request``
This signal is sent out to display additional buttons for a single position of an order.
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
Additionally, the argument ``order`` and ``request`` are available.
@@ -315,6 +322,7 @@ Arguments: 'request'
This signal is sent out to include template snippets on the settings page of an event
that allows generating a pretix Widget code.
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
A second keyword argument ``request`` will contain the request object.
@@ -50,6 +50,46 @@
{% endblocktrans %}
</div>
{% endif %}
{% if dkim_warning %}
<div class="alert alert-danger">
<p>
{{ dkim_warning }}
</p>
<p>
{% trans "Your new DKIM record should be set up as a CNAME record like this:" %}
</p>
<pre><code>{{ dkim_hostname }} CNAME {{ dkim_cname }}</code></pre>
<p>
{% trans "Please keep in mind that updates to DNS might require multiple hours to take effect." %}
</p>
</div>
{% elif dkim_cname %}
<div class="alert alert-success">
{% blocktrans trimmed %}
We found a DKIM record on your domain for this system. Great!
{% endblocktrans %}
</div>
{% endif %}
{% if dmarc_warning %}
<div class="alert alert-danger">
<p>
{{ dmarc_warning }}
</p>
<p>
{% trans "Your new DMARC record could look like this:" %}
</p>
<pre><code>_dmarc.{{ hostname }} TXT "v=DMARC1; p=quarantine; sp=none; adkim=r; aspf=r;"</code></pre>
<p>
{% trans "Please keep in mind that updates to DNS might require multiple hours to take effect." %}
</p>
</div>
{% elif dkim_cname %}
<div class="alert alert-success">
{% blocktrans trimmed %}
We found a DMARC record on your domain for this system. Great!
{% endblocktrans %}
</div>
{% endif %}
{% if verification %}
<h3>{% trans "Verification" %}</h3>
<p>
@@ -70,7 +110,7 @@
</div>
</div>
{% if spf_warning %}
{% if spf_warning or dkim_warning or dmarc_warning %}
<div class="form-group submit-group">
<a href="" class="btn btn-default btn-save">
{% trans "Cancel" %}
@@ -19,7 +19,7 @@
</p>
<ul>
{% for issue in issues %}
<li>{{ issue|safe }}</li>
<li>{{ issue }}</li>
{% endfor %}
</ul>
</div>
@@ -42,7 +42,7 @@
</p>
<ul>
{% for issue in issues %}
<li>{{ issue|safe }}</li>
<li>{{ issue }}</li>
{% endfor %}
</ul>
</div>
@@ -193,7 +193,6 @@
{% endblocktrans %}
</p>
{% bootstrap_field form.contact_mail layout="control" %}
{% bootstrap_field form.contact_url layout="control" %}
{% bootstrap_field form.imprint_url layout="control" %}
</div>
</fieldset>
@@ -156,11 +156,11 @@
<br/>
<small class="text-muted">
{% if not i.tax_rule.price_includes_tax %}
{% blocktrans trimmed with rate=i.tax_rule.rate|floatformat:-2 taxname=i.tax_rule.name %}
{% blocktrans trimmed with rate=i.tax_rule.rate|tax_rate_format taxname=i.tax_rule.name %}
<strong>plus</strong> {{ rate }}% {{ taxname }}
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with rate=i.tax_rule.rate|floatformat:-2 taxname=i.tax_rule.name|default:s_taxes %}
{% blocktrans trimmed with rate=i.tax_rule.rate|tax_rate_format taxname=i.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
{% endif %}
@@ -705,7 +705,7 @@
{% if line.tax_rate %}
<br/>
<small>
{% blocktrans trimmed with rate=line.tax_rate|floatformat:-2 taxname=line.tax_rule.name|default:s_taxes %}
{% blocktrans trimmed with rate=line.tax_rate|tax_rate_format taxname=line.tax_rule.name|default:s_taxes %}
<strong>plus</strong> {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
@@ -715,7 +715,7 @@
{% if line.tax_rate and line.price %}
<br/>
<small>
{% blocktrans trimmed with rate=line.tax_rate|floatformat:-2 taxname=line.tax_rule.name|default:s_taxes %}
{% blocktrans trimmed with rate=line.tax_rate|tax_rate_format taxname=line.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
@@ -755,7 +755,7 @@
{% if fee.tax_rate %}
<br/>
<small>
{% blocktrans trimmed with rate=fee.tax_rate|floatformat:-2 taxname=fee.tax_rule.name|default:s_taxes %}
{% blocktrans trimmed with rate=fee.tax_rate|tax_rate_format taxname=fee.tax_rule.name|default:s_taxes %}
<strong>plus</strong> {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
@@ -765,7 +765,7 @@
{% if fee.tax_rate %}
<br/>
<small>
{% blocktrans trimmed with rate=fee.tax_rate|floatformat:-2 taxname=fee.tax_rule.name|default:s_taxes %}
{% blocktrans trimmed with rate=fee.tax_rate|tax_rate_format taxname=fee.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
@@ -137,6 +137,15 @@
</td>
<td>
{{ d.software_brand|default_if_none:"" }} {{ d.software_version|default_if_none:"" }}
{% if staff_session %}
<details class="admin-only">
<summary>
<i class="fa fa-angle-down collapse-indicator"></i>
{% trans "Details" %}
</summary>
<pre class="admin-only">{{ d.info|pprint }}</pre>
</details>
{% endif %}
</td>
<td>
{% if d.initialized %}
@@ -560,11 +560,10 @@
</div>
</div>
</div>
<script type="text/plain" id="schema-url">{% static "schema/pdf-layout.schema.json" %}</script>
<script type="text/javascript" src="{% static "pdfjs/pdf.js" %}"></script>
<script type="text/javascript" src="{% static "ajv/ajv2020.bundle.min.js" %}"></script>
<script type="text/javascript" src="{% static "fabric/fabric.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/editor.js" %}"></script>
<script type="text/javascript" src="{% static "schema/pdf-layout.validate.js" %}"></script>
<img src="{% static 'pretixpresale/pdf/powered_by_pretix_dark.png' %}" id="poweredby-dark" class="sr-only">
<img src="{% static 'pretixpresale/pdf/powered_by_pretix_white.png' %}" id="poweredby-white" class="sr-only">
{% for family, styles in fonts.items %}
@@ -49,11 +49,11 @@
<td>
<strong>
{% if t.tag %}
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?tag={{ '"'|add:t.tag|add:'"'|urlencode }}">
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?filter-tag={{ '"'|add:t.tag|add:'"'|urlencode }}">
{{ t.tag }}
</a>
{% else %}
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?tag={{ '<>'|urlencode }}">
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?filter-tag={{ '<>'|urlencode }}">
{% trans "Empty tag" %}
</a>
{% endif %}
+2 -1
View File
@@ -243,7 +243,8 @@ def invite(request, token):
if request.user.is_authenticated:
if inv.team.members.filter(pk=request.user.pk).exists():
messages.error(request, _('You cannot accept the invitation for "{}" as you already are part of '
'this team.').format(inv.team.name))
'this team. If you want to add a different user or create a new account, '
'log out and click the invitation link again.').format(inv.team.name))
return redirect('control:index')
else:
with transaction.atomic():
+15 -5
View File
@@ -952,7 +952,12 @@ class MailSettingsRendererPreview(MailSettingsPreview):
context=context,
)
r = HttpResponse(v, content_type='text/html')
r._csp_ignore = True
r['Content-Security-Policy'] = (
# Plugin-provided email templates will contain inline styles or remote images
# but emails should not contain JS
"style-src 'unsafe-inline'; "
"img-src https: data:"
)
return r
else:
raise Http404(_('Unknown email renderer.'))
@@ -1428,11 +1433,16 @@ class TaxUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, UpdateView
form.instance.custom_rules = json.dumps([
f.cleaned_data for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
], cls=I18nJSONEncoder)
if form.has_changed():
if form.has_changed() or self.formset.has_changed():
change_data = {
k: form.cleaned_data.get(k) for k in form.changed_data
}
if self.formset.has_changed():
change_data["custom_rules"] = [
f.cleaned_data for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
]
self.object.log_action(
'pretix.event.taxrule.changed', user=self.request.user, data={
k: form.cleaned_data.get(k) for k in form.changed_data
}
'pretix.event.taxrule.changed', user=self.request.user, data=change_data
)
return super().form_valid(form)
+75 -4
View File
@@ -41,6 +41,30 @@ from pretix.control.forms.mailsetup import SimpleMailForm, SMTPMailForm
logger = logging.getLogger(__name__)
def get_cname_record(hostname):
try:
r = dns.resolver.Resolver()
answers = r.resolve(hostname, 'CNAME')
answers = list(answers)
if len(answers) != 1:
logger.exception('Found multiple CNAME records for {}'.format(hostname))
return
return str(answers[0].target).lower()
except:
logger.exception('Could not fetch CNAME record for {}'.format(hostname))
def get_dmarc_record(hostname):
try:
r = dns.resolver.Resolver()
for resp in r.resolve("_dmarc." + hostname, 'TXT'):
data = b''.join(resp.strings).decode()
if 'DMARC1' in data.strip():
return data
except:
logger.exception("Could not fetch DMARC record for {}".format(hostname))
def get_spf_record(hostname):
try:
r = dns.resolver.Resolver()
@@ -49,7 +73,7 @@ def get_spf_record(hostname):
if data.lower().strip().startswith('v=spf1 '): # RFC7208, section 4.5
return data
except:
logger.exception("Could not fetch SPF record")
logger.exception("Could not fetch SPF record for {}".format(hostname))
def _check_spf_record(not_found_lookup_parts, spf_record, depth):
@@ -168,10 +192,15 @@ class MailSettingsSetupView(TemplateView):
return super().get(request, *args, **kwargs)
session_key = f'sender_mail_verification_code_{self.request.path}_{self.simple_form.cleaned_data.get("mail_from")}'
verify_dns = (
settings.MAIL_CUSTOM_SENDER_SPF_STRING or
(settings.MAIL_CUSTOM_SENDER_DKIM_CNAME and settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR) or
settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED
)
allow_save = (
(not settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED or
('verification' in self.request.POST and self.request.POST.get('verification', '') == self.request.session.get(session_key, None))) and
(not settings.MAIL_CUSTOM_SENDER_SPF_STRING or self.request.POST.get('state') == 'save')
(not verify_dns or self.request.POST.get('state') == 'save')
)
if allow_save:
@@ -192,8 +221,8 @@ class MailSettingsSetupView(TemplateView):
spf_warning = None
spf_record = None
hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1]
if settings.MAIL_CUSTOM_SENDER_SPF_STRING:
hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1]
spf_record = get_spf_record(hostname)
if not spf_record:
spf_warning = _(
@@ -210,7 +239,43 @@ class MailSettingsSetupView(TemplateView):
'this system in the SPF record.'
)
verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning
dkim_warning = None
dkim_hostname = None
dkim_cname = None
if settings.MAIL_CUSTOM_SENDER_DKIM_CNAME and settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR:
dkim_hostname = settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR + '._domainkey.' + hostname
cname_target = get_cname_record(dkim_hostname)
dkim_cname = settings.MAIL_CUSTOM_SENDER_DKIM_CNAME
if not dkim_cname.endswith("."):
dkim_cname += "."
if "%s" in dkim_cname:
dkim_cname = dkim_cname.replace("%s", hostname.replace(".", "-").lower())
if not cname_target:
dkim_warning = _(
'We could not find a CNAME record pointing to our DKIM key for domain you are trying to use. '
'This means that there is a very high change most of the emails will be rejected or marked as '
'spam. We strongly recommend setting up DKIM through a CNAME record. You can do so through the '
'DNS settings at the provider you registered your domain with.'
)
elif cname_target != dkim_cname:
dkim_warning = _(
'We found a CNAME record for a DKIM key, but it is not pointing to the right location. '
'This means that there is a very high chance most of the emails will be rejected or marked as '
'spam. You should update the DNS settings of your domain.'
)
dmarc_warning = None
dmarc_record = None
if settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED:
dmarc_record = get_dmarc_record(hostname)
if not dmarc_record:
spf_warning = _(
'We did not find DMARC record for your domain. This means that there is a very high chance '
'most of the emails will be rejected or marked as spam. You should update the DNS settings '
'of your domain.'
)
verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning and not dkim_warning and not dmarc_warning
if verification:
if 'verification' in self.request.POST:
messages.error(request, _('The verification code was incorrect, please try again.'))
@@ -241,6 +306,12 @@ class MailSettingsSetupView(TemplateView):
'spf_warning': spf_warning,
'spf_record': spf_record,
'spf_key': settings.MAIL_CUSTOM_SENDER_SPF_STRING,
'dkim_warning': dkim_warning,
'dkim_hostname': dkim_hostname,
'dkim_cname': dkim_cname,
'dmarc_warning': dmarc_warning,
'dmarc_record': dmarc_record,
'hostname': hostname,
'recp': self.simple_form.cleaned_data.get('mail_from')
},
using=self.template_engine,
-1
View File
@@ -70,7 +70,6 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
if 'placeholders' in request.GET:
return self.get_placeholders_help(request)
resp = super().get(request, *args, **kwargs)
resp._csp_ignore = True
return resp
def get_placeholders_help(self, request):
+37
View File
@@ -19,7 +19,9 @@
# 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 itertools
import re
from http.cookies import Morsel
from django.conf import settings
@@ -48,6 +50,41 @@ def set_cookie_without_samesite(request, response, key, *args, **kwargs):
response.cookies[key]['Partitioned'] = True
def thoroughly_delete_cookie(response, cookie_name, **kwargs):
""" Deletes different possible versions of a cookie (SameSite, Partitioned) """
properties = {"SameSite": ["", 'None'], "Partitioned": ["", True]}
for i, values in enumerate(itertools.product(*properties.values())):
m = Morsel()
m.set(cookie_name, '', '')
m.update(kwargs)
m.update(zip(properties.keys(), values))
m['expires'] = "Thu, 01 Jan 1970 00:00:00 GMT"
response.cookies[f'___DELETECOOKIE__{i}___{cookie_name}'] = m
# Make sure settings a cookie afterwards will add a new item in the dictionary, placing
# it below our deletion headers.
response.cookies.pop(cookie_name, None)
def get_all_values_of_cookie(cookie_header, cookie_name):
# like django.http.cookie.parse_cookie, but returns all values of duplicated cookies instead of only the last
values = list()
if not cookie_header:
return values
for chunk in cookie_header.split(";"):
if "=" in chunk:
key, val = chunk.split("=", 1)
else:
# Assume an empty name per
# https://bugzilla.mozilla.org/show_bug.cgi?id=169091
key, val = "", chunk
key, val = key.strip(), val.strip()
if key == cookie_name:
values.append(val)
return values
# Based on https://www.chromium.org/updates/same-site/incompatible-clients
# Copyright 2019 Google LLC.
# SPDX-License-Identifier: Apache-2.0
+32
View File
@@ -20,9 +20,11 @@
# <https://www.gnu.org/licenses/>.
#
import copy
from decimal import Decimal
from django.core.files import File
from django.db import models
from django.db.models.fields import DecimalField
class Thumbnail(models.Model):
@@ -54,3 +56,33 @@ def flatten_choices(choices):
yield from label_or_nested
else:
yield value_or_group, label_or_nested
def _normalize_decimal(d: Decimal) -> Decimal:
"""
Strips trailing zeros, e.g.
20.000 20
20.010 20.01
20.100 20.1
But unlike of Decimal.normalize(), 20.000 will not become 2e+1. Very small decimals might still be represented
in scientific notation when printed.
"""
normalized = d.normalize()
sign, digit, exponent = normalized.as_tuple()
if exponent > 0:
return normalized.quantize(1)
return normalized
class NormalizedDecimalField(DecimalField):
"""
Variant of DecimalField that never outputs the trailing zeros, so we always have normalized decimals internally.
Use this only for fields where the trailing zeros are pointless (e.g. percentages), not for monetary amounts.
"""
def from_db_value(self, value, expression, connection):
if value is not None:
value = _normalize_decimal(value)
return value
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+132 -183
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
"PO-Revision-Date: 2026-07-13 11:28+0000\n"
"Last-Translator: ahmed badr <ahmedbadr@oma.com.eg>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
"ar/>\n"
"Language: ar\n"
@@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
"X-Generator: Weblate 4.8\n"
"X-Generator: Weblate 2026.7.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
msgid "Marked as paid"
@@ -30,112 +30,110 @@ msgstr "تعليق:"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "PayPal"
msgstr ""
msgstr "باي بال"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Venmo"
msgstr ""
msgstr "Venmo"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
#: pretix/static/pretixpresale/js/walletdetection.js
msgid "Apple Pay"
msgstr ""
msgstr "Apple Pay"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Itaú"
msgstr ""
msgstr "إيتاو"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "PayPal Credit"
msgstr ""
msgstr "الائتمان من PayPal"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Credit Card"
msgstr ""
msgstr "بطاقة الائتمان"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "PayPal Pay Later"
msgstr ""
msgstr "الدفع لاحقًا عبر PayPal"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "iDEAL | Wero"
msgstr ""
msgstr "iDEAL | التحدي"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "SEPA Direct Debit"
msgstr ""
msgstr "الخصم المباشر في منطقة الدفع الموحدة (SEPA)"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Bancontact"
msgstr ""
msgstr "بانكونتاكت"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "giropay"
msgstr ""
msgstr "giropay"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "SOFORT"
msgstr ""
msgstr "فوراً"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
#, fuzzy
#| msgid "Yes"
msgid "eps"
msgstr عم"
msgstr قاط إضافية"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "MyBank"
msgstr ""
msgstr "بنكي"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Przelewy24"
msgstr ""
msgstr "Przelewy24"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Verkkopankki"
msgstr ""
msgstr "الخدمات المصرفية عبر الإنترنت"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "PayU"
msgstr ""
msgstr "الدفع لك"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "BLIK"
msgstr ""
msgstr "BLIK"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Trustly"
msgstr ""
msgstr "Trustly"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Zimpler"
msgstr ""
msgstr "Zimpler"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Maxima"
msgstr ""
msgstr "الأعظم"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "OXXO"
msgstr ""
msgstr "OXXO"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Boleto"
msgstr ""
msgstr "تذكرة"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "WeChat Pay"
msgstr ""
msgstr "WeChat Pay"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Mercado Pago"
msgstr ""
msgstr "Mercado Pago"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
#: pretix/static/pretixpresale/js/ui/cart.js
msgid "Continue"
msgstr "المتابعة"
msgstr "تابع"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js
@@ -144,7 +142,7 @@ msgstr "جاري تأكيد الدفع الخاص بك …"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Payment method unavailable"
msgstr ""
msgstr "طريقة الدفع غير متاحة"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Placed orders"
@@ -156,11 +154,11 @@ msgstr "الطلبات المدفوعة"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Attendees (ordered)"
msgstr ""
msgstr "الحاضرون (حسب الترتيب)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Attendees (paid)"
msgstr ""
msgstr "الحاضرون (الذين دفعوا رسوم الاشتراك)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Total revenue"
@@ -192,7 +190,7 @@ msgstr "تبديل قائمة الدخول"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Search results"
msgstr "البحث في النتائج"
msgstr "نتائج البحث"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "No tickets found"
@@ -236,15 +234,15 @@ msgstr "غير مدفوع"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Canceled"
msgstr "ملغاة"
msgstr "تم إلغاؤه"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Confirmed"
msgstr ""
msgstr "تم التأكيد"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Approval pending"
msgstr ""
msgstr "في انتظار الموافقة"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Redeemed"
@@ -252,11 +250,11 @@ msgstr "مستخدم"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Cancel"
msgstr "قم بالإلغاء"
msgstr "إلغاء"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Ticket not paid"
msgstr "لم يتم دفع قيمة التذكرة"
msgstr "التذكرة لم يتم دفع ثمنها"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "This ticket is not yet paid. Do you want to continue anyways?"
@@ -276,23 +274,19 @@ msgstr "تم تسجيل الخروج"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Ticket already used"
msgstr "تم استخدام التذكرة مسبقا"
msgstr "تم استخدام التذكرة بالفعل"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Information required"
msgstr "معلومات مطلوبة"
msgstr "المعلومات المطلوبة"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
#, fuzzy
#| msgid "Unknown error."
msgid "Unknown ticket"
msgstr "خطأ غير معروف."
msgstr "تذكرة غير معروفة"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
#, fuzzy
#| msgid "Entry not allowed"
msgid "Ticket type not allowed here"
msgstr "إدخال غير مسموح"
msgstr "نوع تذكرة الدخول غير مسموح به هنا"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Entry not allowed"
@@ -300,17 +294,13 @@ msgstr "إدخال غير مسموح"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Ticket code revoked/changed"
msgstr "تم إلغاء رمز التذكرة أو تبديله"
msgstr "تم إلغاء/تغيير رمز التذكرة"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
#, fuzzy
#| msgid "Ticket not paid"
msgid "Ticket blocked"
msgstr "لم يتم دفع قيمة التذكرة"
msgstr "التذكرة غير محجوبة"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
#, fuzzy
#| msgid "Ticket not paid"
msgid "Ticket not valid at this time"
msgstr "لم يتم دفع قيمة التذكرة"
@@ -320,11 +310,11 @@ msgstr "تم إلغاء الطلب"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Ticket code is ambiguous on list"
msgstr ""
msgstr "رمز التذكرة غير واضح في القائمة"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Order not approved"
msgstr ""
msgstr "لم تتم الموافقة على الطلب"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Checked-in Tickets"
@@ -348,12 +338,9 @@ msgstr "نعم"
#: pretix/static/pretixcontrol/js/ui/question.js
#: pretix/static/pretixpresale/js/ui/questions.js
msgid "No"
msgstr "لا"
msgstr "من"
#: pretix/static/lightbox/js/lightbox.js
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"
msgid "close"
msgstr "إغلاق"
@@ -408,7 +395,7 @@ msgstr ""
#: pretix/static/pretixbase/js/asynctask.js
msgid "We are processing your request …"
msgstr "جاري معالجة طلبك …"
msgstr "نحن نقوم بمعالجة طلبك …"
#: pretix/static/pretixbase/js/asynctask.js
msgid ""
@@ -421,7 +408,7 @@ msgstr ""
#: pretix/static/pretixbase/js/asynctask.js
msgid "If this takes longer than a few minutes, please contact us."
msgstr ""
msgstr "إذا استغرق ذلك أكثر من بضع دقائق، يرجى الاتصال بنا."
#: pretix/static/pretixbase/js/asynctask.js
msgid "Close message"
@@ -437,11 +424,11 @@ msgstr "للنسخ اضغط Ctrl + C!"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid "Edit"
msgstr ""
msgstr "تحرير"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid "Visualize"
msgstr ""
msgstr "تصور"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid ""
@@ -449,10 +436,13 @@ msgid ""
"or variations are not contained in any of your rule parts so people with "
"these tickets will not get in:"
msgstr ""
"تقوم القاعدة الخاصة بك دائمًا بالتصفية حسب المنتج أو النوع، لكن المنتجات أو "
"الأنواع التالية غير مدرجة في أي من أجزاء القاعدة الخاصة بك، لذا لن يتم قبول "
"الأشخاص الذين يحملون هذه التذاكر:"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid "Please double-check if this was intentional."
msgstr ""
msgstr "يرجى التأكد مرة أخرى مما إذا كان ذلك مقصودًا أم لا."
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "All of the conditions below (AND)"
@@ -492,21 +482,21 @@ msgstr "أضف شرطا"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "minutes"
msgstr "الدقائق"
msgstr "دقائق"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Duplicate"
msgstr ""
msgstr "مكرر"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgctxt "entry_status"
msgid "present"
msgstr ""
msgstr "حاضر"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgctxt "entry_status"
msgid "absent"
msgstr ""
msgstr "غائب"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "is one of"
@@ -522,11 +512,11 @@ msgstr "بعد"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "="
msgstr ""
msgstr "="
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Product"
msgstr "منتج"
msgstr "المنتج"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Product variation"
@@ -534,7 +524,7 @@ msgstr "نوع المنتج"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Gate"
msgstr ""
msgstr "الشارع"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Current date and time"
@@ -542,11 +532,11 @@ msgstr "التاريخ والوقت الحالي"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Current day of the week (1 = Monday, 7 = Sunday)"
msgstr ""
msgstr "اليوم الحالي من الأسبوع (1 = الاثنين، 7 = الأحد)"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Current entry status"
msgstr ""
msgstr "الحالة الحالية للطلب"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Number of previous entries"
@@ -557,48 +547,40 @@ msgid "Number of previous entries since midnight"
msgstr "عدد المدخلات السابقة قبل منتصف الليل"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
#, fuzzy
#| msgid "Number of previous entries"
msgid "Number of previous entries since"
msgstr "عدد المدخلات السابقة"
msgstr "عدد الإدخالات السابقة منذ"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
#, fuzzy
#| msgid "Number of previous entries"
msgid "Number of previous entries before"
msgstr "عدد المدخلات السابقة"
msgstr "عدد الإدخالات السابقة قبل"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Number of days with a previous entry"
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
#, fuzzy
#| msgid "Number of days with a previous entry"
msgid "Number of days with a previous entry since"
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
msgstr "عدد الأيام التي تحتوي على قيد سابق منذ"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
#, fuzzy
#| msgid "Number of days with a previous entry"
msgid "Number of days with a previous entry before"
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
msgstr "عدد الأيام التي يوجد فيها قيد سابق قبل ذلك"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Minutes since last entry (-1 on first entry)"
msgstr ""
msgstr "عدد الدقائق منذ آخر تسجيل (-1 عند التسجيل الأول)"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Minutes since first entry (-1 on first entry)"
msgstr ""
msgstr "عدد الدقائق منذ أول دخول (-1 عند أول دخول)"
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts
msgid "Error: Product not found!"
msgstr ""
msgstr "خطأ: لم يتم العثور على المنتج!"
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts
msgid "Error: Variation not found!"
msgstr ""
msgstr "خطأ: لم يتم العثور على النسخة!"
#: pretix/static/pretixcontrol/js/ui/editor.js
msgid "Check-in QR"
@@ -613,16 +595,12 @@ msgid "Group of objects"
msgstr "مجموعة من العناصر"
#: pretix/static/pretixcontrol/js/ui/editor.js
#, fuzzy
#| msgid "Text object"
msgid "Text object (deprecated)"
msgstr "عنصر نص"
msgstr "كائن نصي (مهمل)"
#: pretix/static/pretixcontrol/js/ui/editor.js
#, fuzzy
#| msgid "Text object"
msgid "Text box"
msgstr نصر نص"
msgstr "مربع نصي"
#: pretix/static/pretixcontrol/js/ui/editor.js
msgid "Barcode area"
@@ -669,28 +647,28 @@ msgid "Unknown error."
msgstr "خطأ غير معروف."
#: pretix/static/pretixcontrol/js/ui/main.js
#, 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
#, 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
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
"اللون الذي اخترته لا يتمتع بتباين كافٍ مع اللون الأبيض. وسيؤثر ذلك على إمكانية "
"الوصول إلى موقعك."
#: pretix/static/pretixcontrol/js/ui/main.js
msgid "Search query"
msgstr "البحث في الاستفسارات"
msgstr "استعلام البحث"
#: pretix/static/pretixcontrol/js/ui/main.js
msgid "All"
@@ -706,11 +684,11 @@ msgstr "المختارة فقط"
#: pretix/static/pretixcontrol/js/ui/main.js
msgid "Enter page number between 1 and %(max)s."
msgstr ""
msgstr "أدخل رقم الصفحة بين 1 و %(max)s."
#: pretix/static/pretixcontrol/js/ui/main.js
msgid "Invalid page number."
msgstr ""
msgstr "رقم الصفحة غير صحيح."
#: pretix/static/pretixcontrol/js/ui/main.js
msgid "Use a different name internally"
@@ -729,10 +707,8 @@ msgid "Calculating default price…"
msgstr "حساب السعر الافتراضي…"
#: pretix/static/pretixcontrol/js/ui/plugins.js
#, fuzzy
#| msgid "Search results"
msgid "No results"
msgstr "البحث في النتائج"
msgstr "البحث: لا توجد نتائج"
#: pretix/static/pretixcontrol/js/ui/question.js
msgid "Others"
@@ -740,7 +716,7 @@ msgstr "غير ذلك"
#: pretix/static/pretixcontrol/js/ui/question.js
msgid "Count"
msgstr "احسب"
msgstr "العدد"
#: pretix/static/pretixcontrol/js/ui/subevent.js
msgid "(one more date)"
@@ -753,12 +729,12 @@ msgstr[4] "أيام عديدة"
msgstr[5] "أخرى"
#: pretix/static/pretixpresale/js/ui/cart.js
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr "لم تعد العناصر الموجودة في عربة التسوق الخاصة بك محجوزة لك."
msgstr ""
"لم تعد المنتجات الموجودة في سلة التسوق محجوزة لك. لا يزال بإمكانك إتمام طلبك "
"طالما كانت هذه المنتجات متوفرة."
#: pretix/static/pretixpresale/js/ui/cart.js
msgid "Cart expired"
@@ -766,13 +742,9 @@ msgstr "انتهت صلاحية عربة التسوق"
#: pretix/static/pretixpresale/js/ui/cart.js
msgid "Your cart is about to expire."
msgstr ""
msgstr "سوف تنتهي صلاحية سلة التسوق الخاصة بك قريبًا."
#: pretix/static/pretixpresale/js/ui/cart.js
#, fuzzy
#| msgid "The items in your cart are reserved for you for one minute."
#| msgid_plural ""
#| "The items in your cart are reserved for you for {num} minutes."
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "سيتم حجز العناصر لك في سلة التسوق لمدة {num}."
@@ -783,26 +755,24 @@ msgstr[4] "سيتم حجز العناصر لك في سلة التسوق لدقا
msgstr[5] "سيتم حجز العناصر لك في سلة التسوق لمدة {num}."
#: pretix/static/pretixpresale/js/ui/cart.js
#, fuzzy
#| msgid "Cart expired"
msgid "Your cart has expired."
msgstr "انتهت صلاحية عربة التسوق"
#: pretix/static/pretixpresale/js/ui/cart.js
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
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
msgid "Do you want to renew the reservation period?"
msgstr ""
msgstr "هل ترغب في تمديد فترة الحجز؟"
#: pretix/static/pretixpresale/js/ui/cart.js
msgid "Renew reservation"
msgstr ""
msgstr "تجديد الحجز"
#: pretix/static/pretixpresale/js/ui/main.js
msgid "The organizer keeps %(currency)s %(amount)s"
@@ -822,82 +792,77 @@ msgstr "التوقيت المحلي:"
#: pretix/static/pretixpresale/js/walletdetection.js
msgid "Google Pay"
msgstr ""
msgstr "Google Pay"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Quantity"
msgstr ""
msgstr "الكمية"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Decrease quantity"
msgstr ""
msgstr "تقليل الكمية"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Increase quantity"
msgstr ""
msgstr "زيادة الكمية"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Filter events by"
msgstr ""
msgstr "تصفية الأحداث حسب"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Filter"
msgstr ""
msgstr "فلتر"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Price"
msgstr ""
msgstr "السعر"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, javascript-format
msgctxt "widget"
msgid "Original price: %s"
msgstr ""
msgstr "السعر الأصلي: %s"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, javascript-format
msgctxt "widget"
msgid "New price: %s"
msgstr ""
msgstr "السعر الجديد: %s"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgid "Selected only"
msgctxt "widget"
msgid "Select"
msgstr "المختارة فقط"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy, javascript-format
#| msgid "Selected only"
#, javascript-format
msgctxt "widget"
msgid "Select %s"
msgstr "المختارة فقط"
msgstr "تم اختيار %s فقط"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy, javascript-format
#| msgctxt "widget"
#| msgid "See variations"
#, javascript-format
msgctxt "widget"
msgid "Select variant %s"
msgstr "أنظر إلى الاختلافات"
msgstr "اختر النوع %s"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -940,7 +905,7 @@ msgstr "من %(currency) s %(price)s"
#, javascript-format
msgctxt "widget"
msgid "Image of %s"
msgstr ""
msgstr "صورة لـ %s"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -981,27 +946,21 @@ msgstr "متوفرة مع القسيمة فقط"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "currently available: %s"
msgctxt "widget"
msgid "Not yet available"
msgstr "متوفر حاليا: %s"
msgstr "حاليًا غير متوفر: %s"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Not available anymore"
msgstr ""
msgstr "لم يعد متوفراً"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "currently available: %s"
msgctxt "widget"
msgid "Currently not available"
msgstr "متوفر حاليا: %s"
msgstr "غير متوفر حاليًا: %s"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -1029,24 +988,20 @@ msgid ""
"There are currently a lot of users in this ticket shop. Please open the shop "
"in a new tab to continue."
msgstr ""
"يوجد حالياً عدد كبير من المستخدمين في متجر التذاكر هذا. يرجى فتح المتجر في "
"علامة تبويب جديدة للمتابعة."
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "Close ticket shop"
msgctxt "widget"
msgid "Open ticket shop"
msgstr "إغلاق متجر التذاكر"
msgstr "إغلاق/فتح متجر التذاكر"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "Resume checkout"
msgctxt "widget"
msgid "Checkout"
msgstr "استئناف الدفع"
msgstr "استئناف عملية الدفع"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -1061,6 +1016,9 @@ msgid ""
"We could not create your cart, since there are currently too many users in "
"this ticket shop. Please click \"Continue\" to retry in a new tab."
msgstr ""
"لم نتمكن من إنشاء سلة التسوق الخاصة بك، نظرًا لوجود عدد كبير جدًّا من "
"المستخدمين حاليًّا في متجر التذاكر هذا. يرجى النقر على «متابعة» لإعادة المحاولة "
"في علامة تبويب جديدة."
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -1110,18 +1068,15 @@ msgstr "إغلاق"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "Resume checkout"
msgctxt "widget"
msgid "Close checkout"
msgstr "استئناف الدفع"
msgstr "إغلاق عملية الدفع"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "You cannot cancel this operation. Please wait for loading to finish."
msgstr ""
msgstr "لا يمكنك إلغاء هذه العملية. يرجى الانتظار حتى ينتهي التحميل."
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -1131,21 +1086,15 @@ msgstr "استمرار"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "See variations"
msgctxt "widget"
msgid "Show variants"
msgstr "أنظر إلى الاختلافات"
msgstr "انظر الاختلافات"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "See variations"
msgctxt "widget"
msgid "Hide variants"
msgstr "أنظر إلى الاختلافات"
msgstr "إخفاء الاختلافات"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -1203,11 +1152,11 @@ msgid ""
"add yourself to the waiting list. We will then notify if seats are available "
"again."
msgstr ""
"تم بيع جميع فئات التذاكر أو بعضها حاليًا. إذا أردت، يمكنك إضافة اسمك إلى "
"قائمة الانتظار. وسنقوم بإخطارك إذا توفرت مقاعد مرة أخرى."
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgid "Load more"
msgctxt "widget"
msgid "Load more"
msgstr "تحميل المزيد"
@@ -1250,37 +1199,37 @@ msgstr "الأحد"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Monday"
msgstr ""
msgstr "الاثنين"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Tuesday"
msgstr ""
msgstr "الثلاثاء"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Wednesday"
msgstr ""
msgstr "الأربعاء"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Thursday"
msgstr ""
msgstr "الخميس"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Friday"
msgstr ""
msgstr "الجمعة"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Saturday"
msgstr ""
msgstr "السبت"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Sunday"
msgstr ""
msgstr "الأحد"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -1305,7 +1254,7 @@ msgstr "أبريل"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "May"
msgstr "مايو"
msgstr "هناك"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
+4 -4
View File
@@ -5,8 +5,8 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
"PO-Revision-Date: 2026-07-07 09:55+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"PO-Revision-Date: 2026-07-15 14:20+0000\n"
"Last-Translator: Jochen Siebert <siebert@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
"de/>\n"
"Language: de\n"
@@ -14,7 +14,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.6.1\n"
"X-Generator: Weblate 2026.7.1\n"
"X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html
@@ -8166,7 +8166,7 @@ msgstr "Zugang zu existierenden Veranstaltungen"
msgctxt "permission_level"
msgid "Access existing and create new events"
msgstr ""
"Zugang zu existieren Veranstaltungen und neue Veranstaltungen erstellen"
"Zugang zu existierenden Veranstaltungen und neue Veranstaltungen erstellen"
#: pretix/base/permissions.py
msgid ""
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
"PO-Revision-Date: 2026-05-04 07:42+0000\n"
"Last-Translator: Martin Gross <gross@rami.io>\n"
"PO-Revision-Date: 2026-07-13 11:28+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
"pretix/pretix-js/de_Informal/>\n"
"Language: de_Informal\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.17\n"
"X-Generator: Weblate 2026.7.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
msgid "Marked as paid"
@@ -430,11 +430,11 @@ msgstr "Drücke Strg+C zum kopieren!"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid "Edit"
msgstr ""
msgstr "Bearbeiten"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid "Visualize"
msgstr ""
msgstr "Visualisieren"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid ""
@@ -442,10 +442,13 @@ msgid ""
"or variations are not contained in any of your rule parts so people with "
"these tickets will not get in:"
msgstr ""
"Deine Regeln filtern immer nach Produkt oder Variation, jedoch sind die "
"folgenden Produkte oder Variationen nicht in einer Ihrer Regeln enthalten, "
"sodass Kunden mit diesen Tickets keinen Eintritt erhalten werden:"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid "Please double-check if this was intentional."
msgstr ""
msgstr "Bitte überprüfe, ob dies beabsichtigt war."
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "All of the conditions below (AND)"
+15 -31
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
"PO-Revision-Date: 2026-07-07 09:22+0000\n"
"PO-Revision-Date: 2026-07-08 16:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n"
@@ -11453,10 +11453,8 @@ msgstr ""
"contacto con usted."
#: pretix/base/settings.py pretix/control/forms/event.py
#, fuzzy
#| msgid "Contact"
msgid "Contact URL"
msgstr "Contacto"
msgstr "URL de contacto"
#: pretix/base/settings.py pretix/control/forms/event.py
msgid ""
@@ -11464,6 +11462,10 @@ msgid ""
"the email address above. Please note that you still need to add a contact "
"email address that will be shared with all emails you send."
msgstr ""
"Si activas esta opción, el enlace de contacto del pie de página redirigirá "
"aquí, en lugar de utilizar la dirección de correo electrónico indicada más "
"arriba. Ten en cuenta que aún así tendrás que añadir una dirección de correo "
"electrónico de contacto que se incluirá en todos los correos que envíes."
#: pretix/base/settings.py pretix/control/forms/event.py
msgid "Imprint URL"
@@ -19946,25 +19948,7 @@ msgstr ""
"El equipo de %(instance)s\n"
#: pretix/control/templates/pretixcontrol/email/email_setup.txt
#, fuzzy, python-format
#| msgid ""
#| "Hello,\n"
#| "\n"
#| "Someone requested to use %(address)s as a sender address on %(instance)s. "
#| "Once verified, emails sent from %(instance)s can show this address as the "
#| "sender.\n"
#| "\n"
#| "If this was you, enter the following code in the setup form:\n"
#| "\n"
#| " %(code)s\n"
#| "\n"
#| "Don't share this code with anyone. The %(instance)s team will never ask "
#| "you for it.\n"
#| "\n"
#| "If you didn't request this, you can safely ignore this email.\n"
#| "\n"
#| "Thanks, \n"
#| "The %(instance)s Team\n"
#, python-format
msgid ""
"Hello,\n"
"\n"
@@ -19987,19 +19971,19 @@ msgid ""
msgstr ""
"Hola,\n"
"\n"
"Alguien ha solicitado utilizar %(address)s como dirección de remitente en "
"%(instance)s. Una vez verificado, los correos electrónicos enviados desde "
"%(instance)s podrán mostrar esta dirección como remitente.\n"
"Alguien ha solicitado utilizar %(address)s como dirección de remitente en %"
"(instance)s. Una vez verificado, los correos electrónicos enviados desde %"
"(instance)s podrán mostrar esta dirección como remitente.\n"
"\n"
"Si has sido tú, introduce el siguiente código en el formulario de "
"configuración:\n"
"Si ha sido usted quien lo ha solicitado, introduzca el siguiente código en "
"el formulario de configuración:\n"
"\n"
" %(code)s\n"
"\n"
"No compartas este código con nadie. El equipo de %(instance)s nunca te lo "
"pedirá.\n"
"No comparta este código con nadie a menos que quiera autorizarle a utilizar "
"esta dirección para este fin. El equipo de %(instance)s nunca se lo pedirá.\n"
"\n"
"Si no has solicitado esto, puedes ignorar este correo electrónico sin ningún "
"Si no ha solicitado esto, puede ignorar este correo electrónico sin ningún "
"problema.\n"
"\n"
"Gracias, \n"
+33 -39
View File
@@ -4,10 +4,10 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
"PO-Revision-Date: 2026-07-07 09:22+0000\n"
"PO-Revision-Date: 2026-07-08 16:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
">\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/"
"fr/>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -1933,7 +1933,7 @@ msgid ""
"redeemed."
msgstr ""
"Ce produit ne sera affiché que si un bon de réduction correspondant au "
"produit est échangé."
"produit est validé."
#: pretix/base/exporters/items.py pretix/base/models/items.py
msgid "Buying this product requires approval"
@@ -4109,8 +4109,8 @@ msgstr ""
msgid ""
"To verify your email address {email} on {instance}, use the following code:"
msgstr ""
"Pour valider votre adresse e-mail {email} sur\n"
"{instance}, utilisez le code suivant :"
"Pour valider votre adresse e-mail {email} sur {instance}, utilisez le code "
"suivant :"
#: pretix/base/models/auth.py
msgid "Your confirmation code"
@@ -6975,7 +6975,7 @@ msgstr "Nombre de fois que ce bon peut être utilisé."
#: pretix/base/models/vouchers.py pretix/control/views/vouchers.py
msgid "Redeemed"
msgstr "Échangé"
msgstr "Validé"
#: pretix/base/models/vouchers.py
msgid ""
@@ -11542,10 +11542,8 @@ msgstr ""
"contacter."
#: pretix/base/settings.py pretix/control/forms/event.py
#, fuzzy
#| msgid "Contact"
msgid "Contact URL"
msgstr "Contact"
msgstr "URL de contact"
#: pretix/base/settings.py pretix/control/forms/event.py
msgid ""
@@ -11553,6 +11551,10 @@ msgid ""
"the email address above. Please note that you still need to add a contact "
"email address that will be shared with all emails you send."
msgstr ""
"Si vous activez cette option, le lien de contact figurant dans le pied de "
"page redirigera vers cette page au lieu d'utiliser l'adresse e-mail indiquée "
"ci-dessus. Veuillez noter que vous devrez tout de même ajouter une adresse e-"
"mail de contact qui sera mentionnée dans tous les e-mails que vous enverrez."
#: pretix/base/settings.py pretix/control/forms/event.py
msgid "Imprint URL"
@@ -20094,25 +20096,7 @@ msgstr ""
"L'équipe %(instance)s\n"
#: pretix/control/templates/pretixcontrol/email/email_setup.txt
#, fuzzy, python-format
#| msgid ""
#| "Hello,\n"
#| "\n"
#| "Someone requested to use %(address)s as a sender address on %(instance)s. "
#| "Once verified, emails sent from %(instance)s can show this address as the "
#| "sender.\n"
#| "\n"
#| "If this was you, enter the following code in the setup form:\n"
#| "\n"
#| " %(code)s\n"
#| "\n"
#| "Don't share this code with anyone. The %(instance)s team will never ask "
#| "you for it.\n"
#| "\n"
#| "If you didn't request this, you can safely ignore this email.\n"
#| "\n"
#| "Thanks, \n"
#| "The %(instance)s Team\n"
#, python-format
msgid ""
"Hello,\n"
"\n"
@@ -20136,22 +20120,23 @@ msgstr ""
"Bonjour,\n"
"\n"
"Une personne a demandé à utiliser %(address)s comme adresse d'expéditeur sur "
"%(instance)s. Une fois cette demande validée, les e-mails envoyés depuis "
"%(instance)s pourront afficher cette adresse comme expéditeur.\n"
"%(instance)s. Une fois cette demande validée, les e-mails envoyés depuis %"
"(instance)s pourront afficher cette adresse comme expéditeur.\n"
"\n"
"Si c'est vous qui avez fait cette demande, veuillez saisir le code suivant "
"dans le formulaire de configuration :\n"
"Si c'est bien vous qui en avez fait la demande, veuillez saisir le code "
"suivant dans le formulaire de configuration :\n"
"\n"
" %(code)s\n"
"\n"
"Ne communiquez ce code à personne. L'équipe de %(instance)s ne vous le "
"demandera jamais.\n"
"Ne communiquez ce code à personne, sauf si vous souhaitez autoriser cette "
"personne à utiliser cette adresse à cette fin. L'équipe de %(instance)s ne "
"vous le demandera jamais.\n"
"\n"
"Si vous nen avez pas fait la demande, vous pouvez ignorer cet e-mail en "
"Si vous n'en avez pas fait la demande, vous pouvez ignorer cet e-mail en "
"toute sécurité.\n"
"\n"
"Merci, \n"
"Léquipe %(instance)s\n"
"L'équipe de %(instance)s\n"
#: pretix/control/templates/pretixcontrol/email/forgot.txt
#, python-format
@@ -36830,17 +36815,19 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
msgid "How to fix this:"
msgstr ""
msgstr "Comment résoudre ce problème :"
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
msgid "Close this page."
msgstr ""
msgstr "Fermer cette page."
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
msgid ""
"Open this website again directly in your main browser (for example Safari or "
"Chrome)."
msgstr ""
"Rouvrez ce site directement dans votre navigateur principal (par exemple "
"Safari ou Chrome)."
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
msgid ""
@@ -36848,6 +36835,9 @@ msgid ""
"browser window, without refreshing the page, opening new tabs, or switching "
"apps part-way through."
msgstr ""
"Recommencez la procédure de paiement ou de connexion et menez-la à son terme "
"dans la même fenêtre de navigateur, sans actualiser la page, ouvrir de "
"nouveaux onglets ni changer d'application en cours de route."
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
msgid ""
@@ -36855,6 +36845,10 @@ msgid ""
"before retrying can help. As a last step, try using a different browser or "
"another device (for example, a desktop or laptop computer)."
msgstr ""
"Si le problème persiste, il peut être utile de fermer toutes les fenêtres du "
"navigateur et d'effacer les cookies avant de réessayer. En dernier recours, "
"essayez d'utiliser un autre navigateur ou un autre appareil (par exemple, un "
"ordinateur de bureau ou un ordinateur portable)."
#: pretix/presale/templates/pretixpresale/organizers/customer_membership.html
msgid "Your membership"
+3 -3
View File
@@ -7,8 +7,8 @@ msgstr ""
"Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"PO-Revision-Date: 2026-07-08 16:00+0000\n"
"Last-Translator: Joram Schwartzmann <schwartzmann@pretix.eu>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
"fr/>\n"
"Language: fr\n"
@@ -244,7 +244,7 @@ msgstr "En attente d'approbation"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Redeemed"
msgstr "Échangé"
msgstr "Validé"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Cancel"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+13 -29
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
"PO-Revision-Date: 2026-07-07 09:17+0000\n"
"PO-Revision-Date: 2026-07-14 15:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 2026.6.1\n"
"X-Generator: Weblate 2026.7.1\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html
#: pretix/control/templates/pretixcontrol/events/index.html
@@ -11076,10 +11076,8 @@ msgid "We'll show this publicly to allow attendees to contact you."
msgstr "参加者があなたに連絡できるように、これを公に表示します。"
#: pretix/base/settings.py pretix/control/forms/event.py
#, fuzzy
#| msgid "Contact"
msgid "Contact URL"
msgstr "連絡先"
msgstr "連絡先URL"
#: pretix/base/settings.py pretix/control/forms/event.py
msgid ""
@@ -11087,6 +11085,9 @@ msgid ""
"the email address above. Please note that you still need to add a contact "
"email address that will be shared with all emails you send."
msgstr ""
"これを設定すると、フッターの連絡先リンクが上記のメールアドレスではなくこちら"
"を指すことになります。追加が必要な連絡先メールアドレスは、送信するすべての電"
"子メールで共有されるのでご注意ください。"
#: pretix/base/settings.py pretix/control/forms/event.py
msgid "Imprint URL"
@@ -19360,25 +19361,7 @@ msgstr ""
"%(instance)sのチーム\n"
#: pretix/control/templates/pretixcontrol/email/email_setup.txt
#, fuzzy, python-format
#| msgid ""
#| "Hello,\n"
#| "\n"
#| "Someone requested to use %(address)s as a sender address on %(instance)s. "
#| "Once verified, emails sent from %(instance)s can show this address as the "
#| "sender.\n"
#| "\n"
#| "If this was you, enter the following code in the setup form:\n"
#| "\n"
#| " %(code)s\n"
#| "\n"
#| "Don't share this code with anyone. The %(instance)s team will never ask "
#| "you for it.\n"
#| "\n"
#| "If you didn't request this, you can safely ignore this email.\n"
#| "\n"
#| "Thanks, \n"
#| "The %(instance)s Team\n"
#, python-format
msgid ""
"Hello,\n"
"\n"
@@ -19401,16 +19384,17 @@ msgid ""
msgstr ""
"こんにちは、\n"
"\n"
"誰かが %(address)s を送信者アドレスとして %(instance)s に使用するようリクエス"
"トしました。検証が完了すると、%(instance)s から送信されたメールはこのアドレス"
"を送信者として表示できます。\n"
"誰かが %(address)s を送信者アドレスとして %(instance)s に使用するよう"
"リクエストしました。検証が完了すると、%(instance)s から送信されたメールはこの"
"アドレスを送信者として表示できます。\n"
"\n"
"もしこれがあなたであれば、設定フォームに以下のコードを入力してください。\n"
"\n"
"…%(code)s\n"
"\n"
"このコードを誰にも共有しないでください。%(instance)sのチームは、決してあなた"
"にそれを求めることはありません。\n"
"このコードを誰にも共有しないでください。ただし、このアドレスをこの目的で使用"
"することを許可したい場合は除きます。%(instance)sのチームは、決してあなたにそ"
"れを求めることはありません。\n"
"\n"
"もしこのメールをご依頼いただいていない場合は、このメールはご無視いただいて構"
"いません。\n"
+73 -52
View File
@@ -34,8 +34,6 @@
import logging
import time
from datetime import datetime
from http.cookies import Morsel
from urllib.parse import urlparse
from django.conf import settings
@@ -53,12 +51,16 @@ from django.middleware.csrf import (
from django.shortcuts import render
from django.urls import set_urlconf
from django.utils.cache import patch_vary_headers
from django.utils.decorators import decorator_from_middleware
from django.utils.deprecation import MiddlewareMixin
from django.utils.http import http_date
from django_scopes import scopes_disabled
from pretix.base.models import Event, Organizer
from pretix.helpers.cookies import set_cookie_without_samesite
from pretix.helpers.cookies import (
get_all_values_of_cookie, set_cookie_without_samesite,
thoroughly_delete_cookie,
)
from pretix.multidomain.models import KnownDomain
logger = logging.getLogger(__name__)
@@ -181,9 +183,23 @@ class SessionMiddleware(BaseSessionMiddleware):
# The session should be deleted only if the session is entirely empty
is_secure = request.scheme == 'https'
if '__Host-' + settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
response.delete_cookie('__Host-' + settings.SESSION_COOKIE_NAME)
# response.delete_cookie does not work as we might have set a partitioned cookie
thoroughly_delete_cookie(
response,
'__Host-' + settings.SESSION_COOKIE_NAME,
path=settings.SESSION_COOKIE_PATH,
secure=is_secure,
httponly=settings.SESSION_COOKIE_HTTPONLY or None
)
elif settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
response.delete_cookie(settings.SESSION_COOKIE_NAME)
# response.delete_cookie does not work as we might have set a partitioned cookie
thoroughly_delete_cookie(
response,
settings.SESSION_COOKIE_NAME,
path=settings.SESSION_COOKIE_PATH,
secure=is_secure,
httponly=settings.SESSION_COOKIE_HTTPONLY or None
)
else:
if accessed:
patch_vary_headers(response, ('Cookie',))
@@ -200,15 +216,21 @@ class SessionMiddleware(BaseSessionMiddleware):
if response.status_code != 500:
request.session.save()
if is_secure and settings.SESSION_COOKIE_NAME in request.COOKIES: # remove legacy cookie
response.delete_cookie(settings.SESSION_COOKIE_NAME)
response.delete_cookie(settings.SESSION_COOKIE_NAME, samesite="None")
# response.delete_cookie does not work as we might have set a partitioned cookie
thoroughly_delete_cookie(
response,
settings.SESSION_COOKIE_NAME,
path=settings.SESSION_COOKIE_PATH,
secure=is_secure,
httponly=settings.SESSION_COOKIE_HTTPONLY or None
)
set_cookie_without_samesite(
request, response,
'__Host-' + settings.SESSION_COOKIE_NAME if is_secure else settings.SESSION_COOKIE_NAME,
request.session.session_key, max_age=max_age,
expires=expires,
path=settings.SESSION_COOKIE_PATH,
secure=request.scheme == 'https',
secure=is_secure,
httponly=settings.SESSION_COOKIE_HTTPONLY or None
)
return response
@@ -253,75 +275,74 @@ class CsrfViewMiddleware(BaseCsrfMiddleware):
if request.session.get(CSRF_SESSION_KEY) != request.META["CSRF_COOKIE"]:
request.session[CSRF_SESSION_KEY] = request.META["CSRF_COOKIE"]
else:
is_secure = request.scheme == 'https'
# Set the CSRF cookie even if it's already set, so we renew
# the expiry timer.
if is_secure and settings.CSRF_COOKIE_NAME in request.COOKIES: # remove legacy cookie
response.delete_cookie(settings.CSRF_COOKIE_NAME)
response.delete_cookie(settings.CSRF_COOKIE_NAME, samesite="None")
# remove legacy cookie
if request.is_secure() and settings.CSRF_COOKIE_NAME in request.COOKIES:
thoroughly_delete_cookie(
response,
settings.CSRF_COOKIE_NAME,
path=settings.CSRF_COOKIE_PATH,
secure=request.is_secure(),
httponly=settings.CSRF_COOKIE_HTTPONLY
)
handle_duplicated_csrftoken(request, response)
set_cookie_without_samesite(
request, response,
'__Host-' + settings.CSRF_COOKIE_NAME if is_secure else settings.CSRF_COOKIE_NAME,
'__Host-' + settings.CSRF_COOKIE_NAME if request.is_secure() else settings.CSRF_COOKIE_NAME,
request.META["CSRF_COOKIE"],
max_age=settings.CSRF_COOKIE_AGE,
path=settings.CSRF_COOKIE_PATH,
secure=is_secure,
secure=request.is_secure(),
httponly=settings.CSRF_COOKIE_HTTPONLY
)
# Content varies with the CSRF cookie, so set the Vary header.
patch_vary_headers(response, ('Cookie',))
def process_response(self, request, response):
if (
not settings.CSRF_USE_SESSIONS
and request.is_secure()
and settings.CSRF_COOKIE_NAME in response.cookies
and response.cookies[settings.CSRF_COOKIE_NAME].value
):
logger.warning("Usage of djangos CsrfViewMiddleware detected (legacy cookie found in response). "
"This may be caused by using csrf_project or requires_csrf_token from django.views.decorators.csrf. "
"Use the pretix.multidomain.middlewares equivalent instead.")
return super().process_response(request, response)
def handle_duplicated_csrftoken(request, response):
# Due to a Safari bug, in some browser, two csrftoken cookies with different values
# exist: one unpartitioned, one partitioned. This function generates an additional
# Due to a Safari bug, in some browser, two csrftoken cookies can exist:
# one unpartitioned, one partitioned. This function generates an additional
# Set-Cookie header to get rid of the unpartitioned one.
cookie_name = '__Host-' + settings.CSRF_COOKIE_NAME
if request.scheme == 'https' and cookie_name in request.COOKIES:
if request.is_secure() and cookie_name in request.COOKIES:
values = get_all_values_of_cookie(request.headers.get('Cookie'), cookie_name)
if len(values) > 1:
logger.info('Trying to remove duplicated %s cookies: %r', cookie_name, values)
# Make sure the set_cookie_without_samesite below will add a new item in the dictionary, placing
# it below our deletion header.
response.cookies.pop(cookie_name, None)
# Add the deletion Set-Cookie header to the cookie dict under a wrong name, so it doesn't get
# overwritten by the set_cookie_without_samesite call below. This works because the code in
# django.core.handlers.wsgi/asgi, that generates the actual Set-Cookie headers, only iterates
# over cookie.values(), ignoring the keys.
response.cookies['___DELETECOOKIE___' + cookie_name] = make_delete_morsel(cookie_name)
thoroughly_delete_cookie(
response,
cookie_name,
secure=request.is_secure(),
path=settings.CSRF_COOKIE_PATH,
httponly=settings.CSRF_COOKIE_HTTPONLY
)
def get_all_values_of_cookie(cookie_header, cookie_name):
# like django.http.cookie.parse_cookie, but returns all values of duplicated cookies instead of only the last
values = list()
if not cookie_header:
return values
for chunk in cookie_header.split(";"):
if "=" in chunk:
key, val = chunk.split("=", 1)
else:
# Assume an empty name per
# https://bugzilla.mozilla.org/show_bug.cgi?id=169091
key, val = "", chunk
key, val = key.strip(), val.strip()
if key == cookie_name:
values.append(val)
return values
csrf_protect = decorator_from_middleware(CsrfViewMiddleware)
def make_delete_morsel(name):
m = Morsel()
m.set(name, '', '')
m['expires'] = datetime.utcfromtimestamp(0).strftime("%a, %d %b %Y %H:%M:%S GMT")
m['samesite'] = 'None'
m['secure'] = True
m['path'] = settings.CSRF_COOKIE_PATH
m['httponly'] = settings.CSRF_COOKIE_HTTPONLY
return m
class _EnsureCsrfToken(CsrfViewMiddleware):
# Behave like CsrfViewMiddleware but don't reject requests or log warnings.
def _reject(self, request, reason):
return None
requires_csrf_token = decorator_from_middleware(_EnsureCsrfToken)
+2 -1
View File
@@ -26,6 +26,7 @@ from collections import defaultdict
from django.dispatch import receiver
from django.template.loader import get_template
from django.urls import resolve, reverse
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from pretix.base.logentrytypes import EventLogEntryType, log_entry_types
@@ -153,7 +154,7 @@ def control_order_position_info(sender: Event, position, request, order: Order,
'event': sender,
'position': position
}
return template.render(ctx, request=request).strip()
return mark_safe(template.render(ctx, request=request).strip())
@receiver(order_info, dispatch_uid="badges_control_order_info")
-1
View File
@@ -162,7 +162,6 @@ class XHRView(View):
paypal_order = prov._create_paypal_order(request, None, cart_total)
r = JsonResponse(paypal_order.dict() if paypal_order else {})
r._csp_ignore = True
return r
@@ -28,7 +28,7 @@ from decimal import Decimal
from django import forms
from django.db.models import F, Sum
from django.db.models.functions import Coalesce
from django.utils.formats import date_format, localize
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.timezone import now
from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
@@ -43,7 +43,7 @@ from pretix.base.exporter import BaseExporter
from pretix.base.models import (
GiftCardTransaction, OrderFee, OrderPayment, OrderRefund, Transaction,
)
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.money import money_filter, tax_rate_format
from pretix.base.timeframes import (
DateFrameField,
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
@@ -382,7 +382,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
else "",
tstyle_right,
),
PlainTextParagraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right),
PlainTextParagraph(tax_rate_format(r["tax_rate"]) + " %", tstyle_right),
PlainTextParagraph(str(r["sum_cont"]), tstyle_right),
PlainTextParagraph(
money_filter(r["sum_price"] - r["sum_tax"], currency), tstyle_right
@@ -408,7 +408,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
[
PlainTextParagraph(_("Sum"), tstyle),
PlainTextParagraph("", tstyle_right),
PlainTextParagraph(localize(tax_rate.normalize()) + " %", tstyle_right),
PlainTextParagraph(tax_rate_format(tax_rate) + " %", tstyle_right),
PlainTextParagraph("", tstyle_right),
PlainTextParagraph(
money_filter(
+26 -23
View File
@@ -162,6 +162,8 @@ class ScheduledMail(models.Model):
send_to_orders = self.rule.send_to in (Rule.CUSTOMERS, Rule.BOTH)
send_to_attendees = self.rule.send_to in (Rule.ATTENDEES, Rule.BOTH)
position_ids = op_qs.values_list('id', flat=True)
for o in orders:
with language(o.locale, e.settings.region):
positions = list(o.positions.all())
@@ -191,28 +193,29 @@ class ScheduledMail(models.Model):
positions = [p for p in positions if p.subevent_id == self.subevent_id]
for p in positions:
if p.attendee_email and (p.attendee_email != o.email or not o_sent):
email_ctx = get_email_context(
event=e,
order=o,
invoice_address=ia,
position=p,
event_or_subevent=self.subevent or e,
)
p.send_mail(self.rule.subject, self.rule.template, email_ctx,
attach_ical=self.rule.attach_ical,
log_entry_type='pretix.plugins.sendmail.rule.order.position.email.sent')
elif not o_sent and o.email:
email_ctx = get_email_context(
event=e,
order=o,
invoice_address=ia,
event_or_subevent=self.subevent or e,
)
o.send_mail(self.rule.subject, self.rule.template, email_ctx,
attach_ical=self.rule.attach_ical,
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
o_sent = True
if p.id in position_ids:
if p.attendee_email and (p.attendee_email != o.email or not o_sent):
email_ctx = get_email_context(
event=e,
order=o,
invoice_address=ia,
position=p,
event_or_subevent=self.subevent or e,
)
p.send_mail(self.rule.subject, self.rule.template, email_ctx,
attach_ical=self.rule.attach_ical,
log_entry_type='pretix.plugins.sendmail.rule.order.position.email.sent')
elif not o_sent and o.email:
email_ctx = get_email_context(
event=e,
order=o,
invoice_address=ia,
event_or_subevent=self.subevent or e,
)
o.send_mail(self.rule.subject, self.rule.template, email_ctx,
attach_ical=self.rule.attach_ical,
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
o_sent = True
self.last_successful_order_id = o.pk
@@ -270,7 +273,7 @@ class Rule(models.Model, LoggingMixin):
date_is_absolute = models.BooleanField(default=True, blank=True)
offset_to_event_end = models.BooleanField(default=False, blank=True) # no verbose name because not actually
offset_is_after = models.BooleanField(default=False, blank=True) # displayed in any forms
offset_is_after = models.BooleanField(default=False, blank=True) # displayed in any forms
send_to = models.CharField(max_length=10, choices=SEND_TO_CHOICES, default=CUSTOMERS, verbose_name=_('Send email to'))
@@ -189,7 +189,7 @@ def control_order_position_info(sender: Event, position, request, order, **kwarg
'position': position,
'layouts': layouts,
}
return template.render(ctx, request=request).strip()
return template.render(ctx, request=request)
override_layout = EventPluginSignal()
+16 -10
View File
@@ -161,7 +161,8 @@ voucher_redeem_info = EventPluginSignal()
"""
Arguments: ``voucher``
This signal is sent out to display additional information on the "redeem a voucher" page
This signal is sent out to display additional information on the "redeem a voucher" page.
You are expected to return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -194,6 +195,7 @@ Arguments: ``request``
This signals allows you to add HTML content to the confirmation page that is presented at the
end of the checkout process, just before the order is being created.
You are expected to return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event. A ``request``
argument will contain the request object.
@@ -276,7 +278,8 @@ order_info = EventPluginSignal()
"""
Arguments: ``order``, ``request``
This signal is sent out to display additional information on the order detail page
This signal is sent out to display additional information on the order detail page.
Return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -285,7 +288,8 @@ position_info = EventPluginSignal()
"""
Arguments: ``order``, ``position``, ``request``
This signal is sent out to display additional information on the position detail page
This signal is sent out to display additional information on the position detail page.
Return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -294,7 +298,8 @@ order_info_top = EventPluginSignal()
"""
Arguments: ``order``, ``request``
This signal is sent out to display additional information on top of the order detail page
This signal is sent out to display additional information on top of the order detail page.
Return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -303,7 +308,8 @@ position_info_top = EventPluginSignal()
"""
Arguments: ``order``, ``position``, ``request``
This signal is sent out to display additional information on top of the position detail page
This signal is sent out to display additional information on top of the position detail page.
Return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -349,7 +355,7 @@ This signal is sent out to display additional information on the frontpage above
of products and but below a custom frontpage text.
As with all event plugin signals, the ``sender`` keyword argument will contain the event. The
receivers are expected to return HTML.
receivers are expected to return a SafeString containing HTML, or a string that will be HTML-escaped.
"""
render_seating_plan = EventPluginSignal()
@@ -361,7 +367,7 @@ You will be passed the ``request`` as a keyword argument. If applicable, a ``sub
``voucher`` argument might be given.
As with all event plugin signals, the ``sender`` keyword argument will contain the event. The
receivers are expected to return HTML.
receivers are expected to return a SafeString containing HTML, or a string that will be HTML-escaped.
"""
front_page_bottom = EventPluginSignal()
@@ -372,7 +378,7 @@ This signal is sent out to display additional information on the frontpage below
of products.
As with all event plugin signals, the ``sender`` keyword argument will contain the event. The
receivers are expected to return HTML.
receivers are expected to return a SafeString containing HTML, or a string that will be HTML-escaped.
"""
front_page_bottom_widget = EventPluginSignal()
@@ -383,7 +389,7 @@ This signal is sent out to display additional information on the frontpage below
of products if the front page is shown in the widget.
As with all event plugin signals, the ``sender`` keyword argument will contain the event. The
receivers are expected to return HTML.
receivers are expected to return a SafeString containing HTML, or a string that will be HTML-escaped.
"""
checkout_all_optional = EventPluginSignal()
@@ -403,7 +409,7 @@ Arguments: ``item``, ``variation``, ``subevent``
This signal is sent out when the description of an item or variation is rendered and allows you to append
additional text to the description. You are passed the ``item``, ``variation`` and ``subevent``. You are
expected to return HTML.
expected to return markdown.
"""
register_cookie_providers = EventPluginSignal()
@@ -173,11 +173,11 @@
<small>{% trans "incl. taxes" %}</small>
{% endif %}
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
<small>{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
<small>{% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% elif var.display_price.rate and var.display_price.gross %}
<small>{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
<small>{% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% endif %}
@@ -313,11 +313,11 @@
<small>{% trans "incl. taxes" %}</small>
{% endif %}
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
<small>{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
<small>{% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% elif item.display_price.rate and item.display_price.gross %}
<small>{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
<small>{% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}</small>
{% endif %}
@@ -357,7 +357,7 @@
{% if line.tax_rate and line.total %}
<br />
<small>
{% blocktrans trimmed with rate=line.tax_rate|floatformat:-2 taxname=line.tax_rule.name|default:s_taxes %}
{% blocktrans trimmed with rate=line.tax_rate|tax_rate_format taxname=line.tax_rule.name|default:s_taxes %}
<strong>plus</strong> {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
@@ -367,7 +367,7 @@
{% if line.tax_rate and line.total %}
<br />
<small>
{% blocktrans trimmed with rate=line.tax_rate|floatformat:-2 taxname=line.tax_rule.name|default:s_taxes %}
{% blocktrans trimmed with rate=line.tax_rate|tax_rate_format taxname=line.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
@@ -416,7 +416,7 @@
{% if fee.tax_rate %}
<br />
<small>
{% blocktrans trimmed with rate=fee.tax_rate|floatformat:-2 taxname=fee.tax_rule.name|default:s_taxes %}
{% blocktrans trimmed with rate=fee.tax_rate|tax_rate_format taxname=fee.tax_rule.name|default:s_taxes %}
<strong>plus</strong> {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
@@ -426,7 +426,7 @@
{% if fee.tax_rate %}
<br />
<small>
{% blocktrans trimmed with rate=fee.tax_rate|floatformat:-2 taxname=fee.tax_rule.name|default:s_taxes %}
{% blocktrans trimmed with rate=fee.tax_rate|tax_rate_format taxname=fee.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
@@ -207,13 +207,13 @@
{% endif %}
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=var.display_price.gross|money:event.currency %}{{ value }} incl. taxes{% endblocktrans %}" data-placement="bottom">
{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
{% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}
</small>
{% elif var.display_price.rate and var.display_price.gross %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=var.display_price.net|money:event.currency %}{{ value }} without taxes{% endblocktrans %}" data-placement="bottom">
{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
{% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}
</small>
@@ -372,13 +372,13 @@
{% endif %}
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=item.display_price.gross|money:event.currency %}{{ value }} incl. taxes{% endblocktrans %}" data-placement="bottom">
{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
{% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}
</small>
{% elif item.display_price.rate and item.display_price.gross %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=item.display_price.net|money:event.currency %}{{ value }} without taxes{% endblocktrans %}" data-placement="bottom">
{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
{% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}
</small>
@@ -201,13 +201,13 @@
{% endif %}
{% elif var.display_price.rate and var.display_price.gross and event.settings.display_net_prices %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=var.display_price.gross|money:event.currency %}{{ value }} incl. taxes{% endblocktrans %}" data-placement="bottom">
{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
{% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}
</small>
{% elif var.display_price.rate and var.display_price.gross %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=var.display_price.net|money:event.currency %}{{ value }} without taxes{% endblocktrans %}" data-placement="bottom">
{% blocktrans trimmed with rate=var.display_price.rate|floatformat:-2 name=var.display_price.name %}
{% blocktrans trimmed with rate=var.display_price.rate|tax_rate_format name=var.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}
</small>
@@ -356,13 +356,13 @@
{% endif %}
{% elif item.display_price.rate and item.display_price.gross and event.settings.display_net_prices %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=item.display_price.gross|money:event.currency %}{{ value }} incl. taxes{% endblocktrans %}" data-placement="bottom">
{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
{% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}
</small>
{% elif item.display_price.rate and item.display_price.gross %}
<small data-toggle="tooltip" title="{% blocktrans trimmed with value=item.display_price.net|money:event.currency %}{{ value }} without taxes{% endblocktrans %}" data-placement="bottom">
{% blocktrans trimmed with rate=item.display_price.rate|floatformat:-2 name=item.display_price.name %}
{% blocktrans trimmed with rate=item.display_price.rate|tax_rate_format name=item.display_price.name %}
incl. {{ rate }}% {{ name }}
{% endblocktrans %}
</small>
-9
View File
@@ -44,7 +44,6 @@ from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, ListView, View
@@ -101,7 +100,6 @@ class LoginView(RedirectBackMixin, FormView):
redirect_authenticated_user = True
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
@@ -213,7 +211,6 @@ class RegistrationView(RedirectBackMixin, FormView):
redirect_authenticated_user = True
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
@@ -257,7 +254,6 @@ class SetPasswordView(FormView):
template_name = 'pretixpresale/organizers/customer_setpassword.html'
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
@@ -301,7 +297,6 @@ class ResetPasswordView(FormView):
template_name = 'pretixpresale/organizers/customer_resetpw.html'
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
@@ -527,7 +522,6 @@ class ChangePasswordView(CustomerAccountBaseMixin, FormView):
form_class = ChangePasswordForm
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
@@ -561,7 +555,6 @@ class ChangeInformationView(CustomerAccountBaseMixin, FormView):
form_class = ChangeInfoForm
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
@@ -669,7 +662,6 @@ class SSOLoginView(RedirectBackMixin, View):
redirect_authenticated_user = True
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
@@ -732,7 +724,6 @@ class SSOLoginReturnView(RedirectBackMixin, View):
redirect_authenticated_user = True
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
-1
View File
@@ -48,7 +48,6 @@ def theme_css(request, **kwargs):
obj = getattr(request, "event", request.organizer)
css = get_theme_vars_css(obj, widget=False)
resp = HttpResponse(css, content_type="text/css")
resp._csp_ignore = True
resp["Access-Control-Allow-Origin"] = "*"
if "version" in request.GET:
resp["Expires"] = http_date(time.time() + 3600 * 24 * 30)
-4
View File
@@ -164,7 +164,6 @@ def widget_css(request, version, **kwargs):
css = f"/* v{version} */\n" + theme_css + widget_css
resp = FileResponse(css, content_type='text/css')
resp._csp_ignore = True
resp['Access-Control-Allow-Origin'] = '*'
return resp
@@ -246,7 +245,6 @@ def widget_js(request, version, lang, **kwargs):
cached_js = cache.get(cache_prefix)
if cached_js and not settings.DEBUG:
resp = HttpResponse(cached_js, content_type='text/javascript')
resp._csp_ignore = True
resp['Access-Control-Allow-Origin'] = '*'
return resp
@@ -278,7 +276,6 @@ def widget_js(request, version, lang, **kwargs):
gs.settings.set(checksum_key, checksum)
cache.set(cache_prefix, data, 3600 * 4)
resp = HttpResponse(data, content_type='text/javascript')
resp._csp_ignore = True
resp['Access-Control-Allow-Origin'] = '*'
return resp
@@ -414,7 +411,6 @@ class WidgetAPIProductList(EventListMixin, View):
self.post_process(data)
resp = JsonResponse(data)
resp['Access-Control-Allow-Origin'] = '*'
resp._csp_ignore = True
return resp
def get(self, request, *args, **kwargs):
+3
View File
@@ -262,6 +262,9 @@ MAIL_FROM_NOTIFICATIONS = config.get('mail', 'from_notifications', fallback=MAIL
MAIL_FROM_ORGANIZERS = config.get('mail', 'from_organizers', fallback=MAIL_FROM)
MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED = config.getboolean('mail', 'custom_sender_verification_required', fallback=True)
MAIL_CUSTOM_SENDER_SPF_STRING = config.get('mail', 'custom_sender_spf_string', fallback='')
MAIL_CUSTOM_SENDER_DKIM_SELECTOR = config.get('mail', 'custom_sender_dkim_selector', fallback='')
MAIL_CUSTOM_SENDER_DKIM_CNAME = config.get('mail', 'custom_sender_dkim_cname', fallback='')
MAIL_CUSTOM_SENDER_DMARC_REQUIRED = config.getboolean('mail', 'custom_sender_dmarc_required', fallback=False)
MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = config.getboolean('mail', 'custom_smtp_allow_private_networks', fallback=DEBUG)
EMAIL_HOST = config.get('mail', 'host', fallback='localhost')
EMAIL_PORT = config.getint('mail', 'port', fallback=25)
File diff suppressed because one or more lines are too long
@@ -1,4 +1,7 @@
/*globals $, gettext, fabric, PDFJS*/
window.module = {} // Hack to load ajv-compiled schema without actual module loading
fabric.Poweredby = fabric.util.createClass(fabric.Image, {
type: 'poweredby',
@@ -219,7 +222,6 @@ var editor = {
uploaded_file_id: null,
_window_loaded: false,
_fabric_loaded: false,
schema: null,
_px2mm: function (v) {
return v / editor.pdf_scale / 72 * editor.pdf_page.userUnit * 25.4;
@@ -1320,14 +1322,11 @@ var editor = {
_source_save: function () {
try {
var Ajv = window.ajv2020
var ajv = new Ajv()
var validate = ajv.compile(editor.schema)
var data = JSON.parse($("#source-textarea").val())
var valid = validate(data)
var valid = validate30(data)
if (!valid) {
console.log(validate.errors)
console.log(validate30.errors)
alert("Invalid input syntax. If you're familiar with this, check out the developer console for a full " +
"error log. Otherwise, please contact support.")
} else {
@@ -1463,10 +1462,6 @@ var editor = {
editor._update_save_button();
});
$("#pdf-info-width, #pdf-info-height").bind('change input', editor._paper_size_warning);
$.getJSON($("#schema-url").text(), function (data) {
editor.schema = data;
})
}
};
@@ -1,5 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$comment": "If the schema is changed, regenerate pdf-layout.validate.js with $ ajv compile -s ./pretix/static/schema/pdf-layout.schema.json --spec=draft2020 -o ./pretix/static/schema/pdf-layout.validate.js",
"title": "Ticket Layout",
"description": "Dynamic elements for a PDF layout",
"type": "array",
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -258,7 +258,7 @@ def test_list_list(token_client, organizer, event, clist, item, subevent, django
res["id"] = clist.pk
res["limit_products"] = [item.pk]
with django_assert_num_queries(11):
with django_assert_num_queries(9):
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
@@ -437,7 +437,7 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
p3["addon_to"] = p1["id"]
# All items
with django_assert_num_queries(24):
with django_assert_num_queries(22):
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format(
organizer.slug, event.slug, clist_all.pk
))
@@ -1127,6 +1127,7 @@ def test_store_failed(token_client, organizer, clist, event, order):
organizer.slug, event.slug, clist.pk,
), {
'raw_barcode': '123456',
'raw_source_type': 'nfc_uid',
'nonce': '4321',
'error_reason': 'invalid'
}, format='json')
+4 -4
View File
@@ -1865,7 +1865,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
assert resp.status_code == 200
event.refresh_from_db()
with assert_num_queries(12):
with assert_num_queries(10):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=true'
.format(organizer.slug, event.slug))
@@ -1875,7 +1875,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
with scope(organizer=organizer):
v0 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-0'))
with assert_num_queries(14):
with assert_num_queries(12):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=false'
.format(organizer.slug, event.slug))
@@ -1883,7 +1883,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
assert len(resp.data['results']) == 1
assert resp.data['results'][0]['voucher']['id'] == v0.pk
with assert_num_queries(12):
with assert_num_queries(10):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=true'
.format(organizer.slug, event.slug))
@@ -1894,7 +1894,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
v1 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-1'))
v2 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-2'))
with assert_num_queries(16):
with assert_num_queries(14):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=false'
.format(organizer.slug, event.slug))
+1 -1
View File
@@ -425,7 +425,7 @@ def test_item_list(token_client, organizer, event, team, item):
@pytest.mark.django_db
def test_item_list_queries(token_client, organizer, event, team, item, item3):
with assert_num_queries(18):
with assert_num_queries(16):
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
+2 -2
View File
@@ -1166,10 +1166,10 @@ def test_orderposition_list(
'type': 'entry'
}]
if '/events/' in endpoint:
with django_assert_num_queries(18):
with django_assert_num_queries(16):
resp = token_client.get(endpoint + '?has_checkin=true')
else:
with django_assert_num_queries(17):
with django_assert_num_queries(15):
resp = token_client.get(endpoint + '?has_checkin=true')
assert [res] == resp.data['results']
+63 -2
View File
@@ -159,6 +159,22 @@ class OrganizerTest(SoupTest):
self.orga1.settings.flush()
assert "mail_from" not in self.orga1.settings._cache()
@staticmethod
def _fake_dmarc_record(hostname):
return {
'test.pretix.dev': 'v=DMARC1; p=quarantine; sp=none; adkim=r; aspf=r;',
'bad.pretix.dev': None,
'none.pretix.dev': None,
}[hostname]
@staticmethod
def _fake_cname_record(hostname):
return {
'pretix._domainkey.test.pretix.dev': 'test-pretix-dev.dkim.pretix.eu.',
'pretix._domainkey.bad.pretix.dev': 'example.org',
'pretix._domainkey.none.pretix.dev': None,
}[hostname]
@staticmethod
def _fake_spf_record(hostname):
return {
@@ -172,9 +188,17 @@ class OrganizerTest(SoupTest):
'spftest.pretix.dev': None,
}[hostname]
@override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False, MAIL_CUSTOM_SENDER_SPF_STRING="include:spftest.pretix.dev include:test2.pretix.dev")
def test_email_setup_no_verification_spf_success(self):
@override_settings(
MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False,
MAIL_CUSTOM_SENDER_SPF_STRING="include:spftest.pretix.dev include:test2.pretix.dev",
MAIL_CUSTOM_SENDER_DKIM_SELECTOR="pretix",
MAIL_CUSTOM_SENDER_DKIM_CNAME="dkim.pretix.eu.",
MAIL_CUSTOM_SENDER_DMARC_REQUIRED=True,
)
def test_email_setup_no_verification_spf_dmarc_dkim_success(self):
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_spf_record", OrganizerTest._fake_spf_record)
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_cname_record", OrganizerTest._fake_cname_record)
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_dmarc_record", OrganizerTest._fake_dmarc_record)
doc = self.post_doc(
'/control/organizer/%s/settings/email/setup' % self.orga1.slug,
{
@@ -213,6 +237,43 @@ class OrganizerTest(SoupTest):
# not yet saved
assert "mail_from" not in self.orga1.settings._cache()
@override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False,
MAIL_CUSTOM_SENDER_DKIM_SELECTOR="pretix",
MAIL_CUSTOM_SENDER_DKIM_CNAME="dkim.pretix.eu.",
MAIL_CUSTOM_SENDER_SPF_STRING="")
def test_email_setup_no_verification_dkim_warning(self):
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_cname_record", OrganizerTest._fake_cname_record)
doc = self.post_doc(
'/control/organizer/%s/settings/email/setup' % self.orga1.slug,
{
'mode': 'simple',
'simple-mail_from': 'test@bad.pretix.dev',
},
follow=True
)
assert doc.select('.alert-danger')
self.orga1.settings.flush()
# not yet saved
assert "mail_from" not in self.orga1.settings._cache()
@override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False,
MAIL_CUSTOM_SENDER_DMARC_REQUIRED=True,
MAIL_CUSTOM_SENDER_SPF_STRING="")
def test_email_setup_no_verification_dmarc_warning(self):
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_dmarc_record", OrganizerTest._fake_dmarc_record)
doc = self.post_doc(
'/control/organizer/%s/settings/email/setup' % self.orga1.slug,
{
'mode': 'simple',
'simple-mail_from': 'test@bad.pretix.dev',
},
follow=True
)
assert doc.select('.alert-danger')
self.orga1.settings.flush()
# not yet saved
assert "mail_from" not in self.orga1.settings._cache()
def test_email_setup_smtp(self):
self.monkeypatch.setattr("pretix.base.email.test_custom_smtp_backend", lambda b, a: None)
self.monkeypatch.setattr("socket.gethostbyname", lambda h: "8.8.8.8")
+32
View File
@@ -0,0 +1,32 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from decimal import Decimal
from pretix.helpers.models import _normalize_decimal
def test_normalize_decimal():
assert str(_normalize_decimal(Decimal("20.0000"))) == "20"
assert str(_normalize_decimal(Decimal("20.0001"))) == "20.0001"
assert str(_normalize_decimal(Decimal("20.0100"))) == "20.01"
assert str(_normalize_decimal(Decimal("2000000.0000"))) == "2000000"
assert str(_normalize_decimal(Decimal("0.0001"))) == "0.0001"
+91
View File
@@ -426,6 +426,97 @@ def test_sendmail_rule_checked_in_get_mail(event, order, item):
assert len(djmail.outbox) == 1, "email not sent"
@pytest.mark.django_db
@scopes_disabled()
def test_sendmail_rule_checked_in_mixed_order(event, order, item):
order.status = Order.STATUS_PAID
order.save()
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
order.all_positions.create(item=item, price=13, attendee_email='item2@dummy.test') # p2
clist = event.checkin_lists.create(name="Default", all_products=True)
# receives no mail when checked in
djmail.outbox = []
perform_checkin(p1, clist, {})
assert clist.checkin_count == 1
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="checked_in",
subject='meow', template='meow meow meow')
sendmail_run_rules(None)
assert len(djmail.outbox) == 1
@pytest.mark.django_db
@scopes_disabled()
def test_sendmail_rule_not_checked_in_mixed_order(event, order, item):
order.status = Order.STATUS_PAID
order.save()
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
order.all_positions.create(item=item, price=13, attendee_email='item2@dummy.test') # p2
clist = event.checkin_lists.create(name="Default", all_products=True)
# receives no mail when checked in
djmail.outbox = []
perform_checkin(p1, clist, {})
assert clist.checkin_count == 1
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="no_checkin",
subject='meow', template='meow meow meow')
sendmail_run_rules(None)
assert len(djmail.outbox) == 1
@pytest.mark.django_db
@scopes_disabled()
def test_sendmail_rule_not_checked_in_mixed_order_position_without_email_not_matching_status(event, order, item, item2):
order.status = Order.STATUS_PAID
order.save()
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
p2 = order.all_positions.create(item=item, price=13, attendee_email='item2@dummy.test')
clist = event.checkin_lists.create(name="Default", all_products=True)
# receives no mail when checked in
djmail.outbox = []
perform_checkin(p1, clist, {})
# we have no email and we are checked in
# we shouldn't trigger a fallback to order.email
p3 = order.all_positions.create(item=item, price=13)
perform_checkin(p3, clist, {})
assert clist.checkin_count == 2
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="no_checkin",
subject='meow', template='meow meow meow', send_to=Rule.ATTENDEES)
sendmail_run_rules(None)
assert len(djmail.outbox) == 1
recipients = [m.to for m in djmail.outbox]
assert [p2.attendee_email] in recipients # for p2
@pytest.mark.django_db
@scopes_disabled()
def test_sendmail_rule_not_checked_in_mixed_order_position_without_email(event, order, item, item2):
order.status = Order.STATUS_PAID
order.save()
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
p2 = order.all_positions.create(item=item, price=13, attendee_email='item2@dummy.test')
order.all_positions.create(item=item, price=13) # p3
order.all_positions.create(item=item, price=13) # p4
clist = event.checkin_lists.create(name="Default", all_products=True)
# receives no mail when checked in
djmail.outbox = []
perform_checkin(p1, clist, {})
assert clist.checkin_count == 1
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="no_checkin",
subject='meow', template='meow meow meow', send_to=Rule.ATTENDEES)
sendmail_run_rules(None)
assert len(djmail.outbox) == 2
recipients = [m.to for m in djmail.outbox]
assert [order.email] in recipients # for p3 and p4
assert [p2.attendee_email] in recipients # for p2
@pytest.mark.django_db
@scopes_disabled()
def run_restriction_test(event, order, restrictions_pass=[], restrictions_fail=[]):
+15 -15
View File
@@ -189,8 +189,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
"current_unavailability_reason": None,
"order_min": None,
"max_price": None,
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19", "includes_mixed_tax_rate": False},
"picture": None,
"picture_fullsize": None,
"has_variations": False,
@@ -226,9 +226,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
"id": self.shirt_red.pk,
'original_price': None,
"price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
"rate": "19.00", "includes_mixed_tax_rate": False},
"rate": "19", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
"rate": "19.00", "includes_mixed_tax_rate": False},
"rate": "19", "includes_mixed_tax_rate": False},
"description": None,
"avail": [100, None],
"order_max": 2,
@@ -239,9 +239,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
"id": self.shirt_blue.pk,
'original_price': None,
"price": {"gross": "12.00", "net": "10.08", "tax": "1.92", "name": "",
"rate": "19.00", "includes_mixed_tax_rate": False},
"rate": "19", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "12.00", "net": "10.08", "tax": "1.92", "name": "",
"rate": "19.00", "includes_mixed_tax_rate": False},
"rate": "19", "includes_mixed_tax_rate": False},
"description": None,
"avail": [100, None],
"order_max": 2,
@@ -278,9 +278,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
"current_unavailability_reason": None,
"order_min": None,
"max_price": None,
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00",
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19",
"includes_mixed_tax_rate": False},
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00",
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19",
"includes_mixed_tax_rate": False},
"picture": None,
"picture_fullsize": None,
@@ -343,9 +343,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
"id": self.shirt_red.pk,
'original_price': None,
"price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
"rate": "19.00", "includes_mixed_tax_rate": False},
"rate": "19", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
"rate": "19.00", "includes_mixed_tax_rate": False},
"rate": "19", "includes_mixed_tax_rate": False},
"description": None,
"avail": [100, None],
"order_max": 2,
@@ -395,8 +395,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
"current_unavailability_reason": None,
"order_min": None,
"max_price": None,
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19", "includes_mixed_tax_rate": False},
"suggested_price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19", "includes_mixed_tax_rate": False},
"picture": None,
"picture_fullsize": None,
"has_variations": False,
@@ -481,7 +481,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
'gross': '14.00',
'net': '11.76',
'tax': '2.24',
'rate': '19.00',
'rate': '19',
'name': '',
'includes_mixed_tax_rate': False
},
@@ -489,7 +489,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
'gross': '14.00',
'net': '11.76',
'tax': '2.24',
'rate': '19.00',
'rate': '19',
'name': '',
'includes_mixed_tax_rate': False
},
@@ -601,7 +601,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"net": "19.52",
"tax": "3.48",
"name": "MIXED!",
"rate": "19.00",
"rate": "19",
"includes_mixed_tax_rate": True
}