Compare commits

...
Author SHA1 Message Date
ef51a570ae Update src/pretix/base/migrations/0304_tax_rate_decimals.py
Co-authored-by: robbi5 <richt@rami.io>
2026-07-14 20:56:02 +02:00
Raphael Michel 8fe436cb25 Rebase migration 2026-07-14 09:27:10 +02:00
Raphael Michel 3d82b79cc9 Add uncommitted tests 2026-07-14 09:26:40 +02:00
Raphael Michel 40959ee24a More tax rate handling 2026-07-14 09:26:39 +02:00
Raphael Michel b3eec43645 Fix normalization 2026-07-14 09:25:46 +02:00
Raphael MichelandRaphael Michel b46956ba0a Allow more decimal places for tax rates 2026-07-14 09:25:46 +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]GitHubluelistadependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Raphael MichelRaphael Michel
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
36 changed files with 447 additions and 155 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ dependencies = [
"django-countries==8.2.*",
"django-filter==25.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.*",
+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'
+6 -1
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
@@ -591,6 +591,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 +748,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 +1914,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 +1998,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
@@ -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),
),
]
+6 -9
View File
@@ -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)
+9 -8
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 (
@@ -2354,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(
@@ -2551,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(
@@ -3070,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(
@@ -3186,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 = []
+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.
@@ -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>
@@ -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>
+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():
+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
+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")
@@ -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(
@@ -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:
+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"
+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
}