Compare commits

...
Author SHA1 Message Date
Raphael Michel d1ba5c298d Create regression tests 2026-03-30 19:16:17 +02:00
Raphael Michel c6c48537dd Do not create cart session on view with active session 2026-03-30 19:16:03 +02:00
Raphael Michel ab08dea9f7 Skip useless code paths in CartMixin 2026-03-30 18:58:10 +02:00
Raphael Michel 4d15731528 Do not create useless cart session accessing invoice address 2026-03-30 18:57:45 +02:00
Raphael Michel a2cef22ea8 Bump version to 2026.4.0.dev0 2026-03-30 15:01:39 +02:00
Raphael Michel 3843448812 Bump version to 2026.3.0 2026-03-30 15:01:30 +02:00
Kara Engelhardtandpajowu 49893ca9df Fix crash in mail_send_task for nonexistant mails 2026-03-30 14:57:56 +02:00
Raphael MichelandRaphael Michel 4eade5070e Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6287 of 6287 strings)

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

powered by weblate
2026-03-30 14:01:13 +02:00
Raphael MichelandRaphael Michel 32b1997208 Translations: Update German
Currently translated at 100.0% (6287 of 6287 strings)

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

powered by weblate
2026-03-30 14:01:13 +02:00
Raphael Michel eaf4a310f6 Translations: Update wordlist 2026-03-30 13:59:37 +02:00
Raphael Michel 8dc0f7c1b2 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2026-03-30 13:26:02 +02:00
CVZ-esandRaphael Michel dd3e6c4692 Translations: Update Spanish
Currently translated at 100.0% (256 of 256 strings)

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

powered by weblate
2026-03-30 13:21:45 +02:00
Kara Engelhardtandpajowu c7437336b4 Add length help text to customer password forms
Also cleans up dead code, as `validate_password` always returns None or raises a ValidationError.
2026-03-30 11:25:14 +02:00
luelistaandGitHub 4c0c775baa Improve 2fa type selection UI (#6031) 2026-03-27 13:47:10 +01:00
Linnea ThelanderandMartin Gross 394652a5ff Translations: Update Swedish
Currently translated at 88.0% (5530 of 6283 strings)

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

powered by weblate
2026-03-27 10:05:21 +01:00
Ivano VogheraandMartin Gross 3f50d065ec Translations: Update Italian
Currently translated at 40.0% (2515 of 6283 strings)

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

powered by weblate
2026-03-27 10:05:21 +01:00
Ivano VogheraandMartin Gross 4121061267 Translations: Update Italian
Currently translated at 40.0% (2515 of 6283 strings)

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

powered by weblate
2026-03-26 18:50:24 +01:00
Linnea ThelanderandMartin Gross aed2220139 Translations: Update Swedish
Currently translated at 76.9% (197 of 256 strings)

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

powered by weblate
2026-03-26 18:50:24 +01:00
Linnea ThelanderandMartin Gross 4b2c54d38e Translations: Update Swedish
Currently translated at 88.0% (5530 of 6283 strings)

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

powered by weblate
2026-03-26 18:50:24 +01:00
Ivano VogheraandMartin Gross 0113a3dc1f Translations: Update Italian
Currently translated at 39.9% (2507 of 6283 strings)

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

powered by weblate
2026-03-26 18:50:24 +01:00
Linnea ThelanderandMartin Gross c12a8935f1 Translations: Update Swedish
Currently translated at 87.9% (5529 of 6283 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Pietro IsottiandMartin Gross a86a6cc2c7 Translations: Update Italian
Currently translated at 39.5% (2486 of 6283 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Pietro IsottiandMartin Gross fec2b9a2fc Translations: Update Italian
Currently translated at 68.3% (175 of 256 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Pietro IsottiandMartin Gross d847a7e8f8 Translations: Update Italian
Currently translated at 39.2% (2463 of 6283 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Ruud HendrickxandMartin Gross c58a968196 Translations: Update Dutch (Belgium)
Currently translated at 79.1% (4970 of 6283 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Renne RochaandMartin Gross 81cbaca162 Translations: Update Portuguese (Brazil)
Currently translated at 100.0% (256 of 256 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Renne RochaandMartin Gross 218df7a49f Translations: Update Portuguese (Brazil)
Currently translated at 95.1% (5979 of 6283 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Ruud HendrickxandMartin Gross f64343d977 Translations: Update Dutch (Belgium)
Currently translated at 78.7% (4948 of 6283 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Hijiri UmemotoandMartin Gross b36c7cbef3 Translations: Update Japanese
Currently translated at 100.0% (256 of 256 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Hijiri UmemotoandMartin Gross 18b39ba7cd Translations: Update Japanese
Currently translated at 100.0% (6283 of 6283 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Raphael Michel 1383e967df Hotfix font select in organizer 2026-03-25 15:14:20 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c743e9fd3f Update sentry-sdk requirement from ==2.54.* to ==2.56.* (#6023)
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.54.0...2.56.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 13:41:13 +01:00
Raphael MichelandGitHub a71efa6747 Event settings: Workaround for Django 5.2 change (#6025) 2026-03-24 22:00:05 +01:00
Richard Schreiber 4fed47fb9b Fix live_receivers for django 5 2026-03-24 17:14:05 +01:00
Phin WolkwitzandGitHub c143d50290 Update django to 5.2 2026-03-24 16:33:28 +01:00
luelistaandGitHub 88cd715ece Always show Organizers and Events menu entries for staff (#6011) 2026-03-24 11:26:54 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3513de6a45 Update importlib-metadata requirement from ==8.* to ==9.* (#6016)
Updates the requirements on [importlib-metadata](https://github.com/python/importlib_metadata) to permit the latest version.
- [Release notes](https://github.com/python/importlib_metadata/releases)
- [Changelog](https://github.com/python/importlib_metadata/blob/main/NEWS.rst)
- [Commits](https://github.com/python/importlib_metadata/compare/v8.0.0...v9.0.0)

---
updated-dependencies:
- dependency-name: importlib-metadata
  dependency-version: 9.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-24 09:09:07 +01:00
Richard SchreiberandGitHub fd6d3934c0 Remove invoice_address_from_vat_id on save if it is not used 2026-03-23 14:33:17 +01:00
99 changed files with 17663 additions and 16178 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
name: Packaging name: Packaging
strategy: strategy:
matrix: matrix:
python-version: ["3.11"] python-version: ["3.13"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
+4 -4
View File
@@ -24,10 +24,10 @@ jobs:
name: Check gettext syntax name: Check gettext syntax
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python 3.11 - name: Set up Python 3.13
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: 3.11 python-version: 3.13
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
path: ~/.cache/pip path: ~/.cache/pip
@@ -49,10 +49,10 @@ jobs:
name: Spellcheck name: Spellcheck
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python 3.11 - name: Set up Python 3.13
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: 3.11 python-version: 3.13
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
path: ~/.cache/pip path: ~/.cache/pip
+6 -6
View File
@@ -24,10 +24,10 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python 3.11 - name: Set up Python 3.13
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: 3.11 python-version: 3.13
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
path: ~/.cache/pip path: ~/.cache/pip
@@ -44,10 +44,10 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python 3.11 - name: Set up Python 3.13
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: 3.11 python-version: 3.13
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
path: ~/.cache/pip path: ~/.cache/pip
@@ -64,10 +64,10 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python 3.11 - name: Set up Python 3.13
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: 3.11 python-version: 3.13
- name: Install Dependencies - name: Install Dependencies
run: pip3 install licenseheaders run: pip3 install licenseheaders
- name: Run licenseheaders - name: Run licenseheaders
+4 -2
View File
@@ -23,13 +23,15 @@ jobs:
name: Tests name: Tests
strategy: strategy:
matrix: matrix:
python-version: ["3.10", "3.11", "3.13"] python-version: ["3.11", "3.13", "3.14"]
database: [sqlite, postgres] database: [sqlite, postgres]
exclude: exclude:
- database: sqlite - database: sqlite
python-version: "3.10" python-version: "3.10"
- database: sqlite - database: sqlite
python-version: "3.11" python-version: "3.11"
- database: sqlite
python-version: "3.12"
services: services:
postgres: postgres:
image: postgres:15 image: postgres:15
@@ -81,4 +83,4 @@ jobs:
file: src/coverage.xml file: src/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false fail_ci_if_error: false
if: matrix.database == 'postgres' && matrix.python-version == '3.11' if: matrix.database == 'postgres' && matrix.python-version == '3.13'
+8 -7
View File
@@ -3,7 +3,7 @@ name = "pretix"
dynamic = ["version"] dynamic = ["version"]
description = "Reinventing presales, one ticket at a time" description = "Reinventing presales, one ticket at a time"
readme = "README.rst" readme = "README.rst"
requires-python = ">=3.10" requires-python = ">=3.11"
license = {file = "LICENSE"} license = {file = "LICENSE"}
keywords = ["tickets", "web", "shop", "ecommerce"] keywords = ["tickets", "web", "shop", "ecommerce"]
authors = [ authors = [
@@ -19,10 +19,11 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Environment :: Web Environment", "Environment :: Web Environment",
"License :: OSI Approved :: GNU Affero General Public License v3", "License :: OSI Approved :: GNU Affero General Public License v3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Framework :: Django :: 4.2", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Framework :: Django :: 5.2",
] ]
dependencies = [ dependencies = [
@@ -36,7 +37,7 @@ dependencies = [
"css-inline==0.20.*", "css-inline==0.20.*",
"defusedcsv>=1.1.0", "defusedcsv>=1.1.0",
"dnspython==2.*", "dnspython==2.*",
"Django[argon2]==4.2.*,>=4.2.26", "Django[argon2]==5.2.*",
"django-bootstrap3==26.1", "django-bootstrap3==26.1",
"django-compressor==4.6.0", "django-compressor==4.6.0",
"django-countries==8.2.*", "django-countries==8.2.*",
@@ -59,7 +60,7 @@ dependencies = [
"dnspython==2.8.*", "dnspython==2.8.*",
"drf_ujson2==1.7.*", "drf_ujson2==1.7.*",
"geoip2==5.*", "geoip2==5.*",
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+ "importlib_metadata==9.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek", "isoweek",
"jsonschema", "jsonschema",
"kombu==5.6.*", "kombu==5.6.*",
@@ -92,7 +93,7 @@ dependencies = [
"redis==7.1.*", "redis==7.1.*",
"reportlab==4.4.*", "reportlab==4.4.*",
"requests==2.32.*", "requests==2.32.*",
"sentry-sdk==2.54.*", "sentry-sdk==2.56.*",
"sepaxml==2.7.*", "sepaxml==2.7.*",
"stripe==7.9.*", "stripe==7.9.*",
"text-unidecode==1.*", "text-unidecode==1.*",
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # 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/>. # <https://www.gnu.org/licenses/>.
# #
__version__ = "2026.3.0.dev0" __version__ = "2026.4.0.dev0"
+1 -2
View File
@@ -196,8 +196,7 @@ class RegistrationForm(forms.Form):
def clean_password(self): def clean_password(self):
password1 = self.cleaned_data.get('password', '') password1 = self.cleaned_data.get('password', '')
user = User(email=self.cleaned_data.get('email')) user = User(email=self.cleaned_data.get('email'))
if validate_password(password1, user=user) is not None: validate_password(password1, user=user)
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
return password1 return password1
def clean_email(self): def clean_email(self):
+2 -2
View File
@@ -45,7 +45,6 @@ import pycountry
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.gis.geoip2 import GeoIP2
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.validators import ( from django.core.validators import (
@@ -102,6 +101,7 @@ from pretix.helpers.countries import (
from pretix.helpers.escapejson import escapejson_attr from pretix.helpers.escapejson import escapejson_attr
from pretix.helpers.http import get_client_ip from pretix.helpers.http import get_client_ip
from pretix.helpers.i18n import get_format_without_seconds from pretix.helpers.i18n import get_format_without_seconds
from pretix.helpers.security import get_geoip
from pretix.presale.signals import question_form_fields from pretix.presale.signals import question_form_fields
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -393,7 +393,7 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
def guess_country_from_request(request, event): def guess_country_from_request(request, event):
if settings.HAS_GEOIP: if settings.HAS_GEOIP:
g = GeoIP2() g = get_geoip()
try: try:
res = g.country(get_client_ip(request)) res = g.country(get_client_ip(request))
if res['country_code'] and len(res['country_code']) == 2: if res['country_code'] and len(res['country_code']) == 2:
@@ -36,8 +36,9 @@ from django.core.management.commands.makemigrations import Command as Parent
from ._migrations import monkeypatch_migrations from ._migrations import monkeypatch_migrations
monkeypatch_migrations()
class Command(Parent): class Command(Parent):
pass
def handle(self, *args, **kwargs):
monkeypatch_migrations()
return super().handle(*args, **kwargs)
@@ -64,7 +64,7 @@ class Command(BaseCommand):
if not periodic_task.receivers or periodic_task.sender_receivers_cache.get(self) is NO_RECEIVERS: if not periodic_task.receivers or periodic_task.sender_receivers_cache.get(self) is NO_RECEIVERS:
return return
for receiver in periodic_task._live_receivers(self): for receiver in periodic_task._live_receivers(self)[0]:
name = f'{receiver.__module__}.{receiver.__name__}' name = f'{receiver.__module__}.{receiver.__name__}'
if options['list_tasks']: if options['list_tasks']:
print(name) print(name)
@@ -41,16 +41,20 @@ class Migration(migrations.Migration):
name='datetime', name='datetime',
field=models.DateTimeField(), field=models.DateTimeField(),
), ),
migrations.AlterIndexTogether( migrations.AddIndex(
name='logentry', 'logentry',
index_together={('datetime', 'id')}, models.Index(fields=('datetime', 'id'), name="pretixbase__datetim_b1fe5a_idx"),
), ),
migrations.AlterIndexTogether( migrations.AddIndex(
name='order', 'order',
index_together={('datetime', 'id'), ('last_modified', 'id')}, models.Index(fields=["datetime", "id"], name="pretixbase__datetim_66aff0_idx"),
), ),
migrations.AlterIndexTogether( migrations.AddIndex(
name='transaction', 'order',
index_together={('datetime', 'id')}, models.Index(fields=["last_modified", "id"], name="pretixbase__last_mo_4ebf8b_idx"),
),
migrations.AddIndex(
'transaction',
models.Index(fields=('datetime', 'id'), name="pretixbase__datetim_b20405_idx"),
), ),
] ]
@@ -61,7 +61,10 @@ class Migration(migrations.Migration):
options={ options={
'ordering': ('identifier', 'type', 'organizer'), 'ordering': ('identifier', 'type', 'organizer'),
'unique_together': {('identifier', 'type', 'organizer')}, 'unique_together': {('identifier', 'type', 'organizer')},
'index_together': {('identifier', 'type', 'organizer'), ('updated', 'id')}, 'indexes': [
models.Index(fields=('identifier', 'type', 'organizer'), name='reusable_medium_organizer_index'),
models.Index(fields=('updated', 'id'), name="pretixbase__updated_093277_idx")
],
}, },
bases=(models.Model, pretix.base.models.base.LoggingMixin), bases=(models.Model, pretix.base.models.base.LoggingMixin),
), ),
-25
View File
@@ -9,31 +9,6 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.RenameIndex(
model_name="logentry",
new_name="pretixbase__datetim_b1fe5a_idx",
old_fields=("datetime", "id"),
),
migrations.RenameIndex(
model_name="order",
new_name="pretixbase__datetim_66aff0_idx",
old_fields=("datetime", "id"),
),
migrations.RenameIndex(
model_name="order",
new_name="pretixbase__last_mo_4ebf8b_idx",
old_fields=("last_modified", "id"),
),
migrations.RenameIndex(
model_name="reusablemedium",
new_name="pretixbase__updated_093277_idx",
old_fields=("updated", "id"),
),
migrations.RenameIndex(
model_name="transaction",
new_name="pretixbase__datetim_b20405_idx",
old_fields=("datetime", "id"),
),
migrations.AlterField( migrations.AlterField(
model_name="attendeeprofile", model_name="attendeeprofile",
name="id", name="id",
@@ -1,6 +1,6 @@
# Generated by Django 4.2.10 on 2024-04-02 15:16 # Generated by Django 4.2.10 on 2024-04-02 15:16
from django.db import migrations from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -10,8 +10,8 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.AlterIndexTogether( migrations.RemoveIndex(
name="reusablemedium", "reusablemedium",
index_together=set(), 'reusable_medium_organizer_index',
), ),
] ]
+1 -1
View File
@@ -88,7 +88,7 @@ class LogEntry(models.Model):
class Meta: class Meta:
ordering = ('-datetime', '-id') ordering = ('-datetime', '-id')
indexes = [models.Index(fields=["datetime", "id"])] indexes = [models.Index(fields=["datetime", "id"], name="pretixbase__datetim_b1fe5a_idx")]
def display(self): def display(self):
from pretix.base.logentrytype_registry import log_entry_types from pretix.base.logentrytype_registry import log_entry_types
+1 -1
View File
@@ -122,7 +122,7 @@ class ReusableMedium(LoggedModel):
class Meta: class Meta:
unique_together = (("identifier", "type", "organizer"),) unique_together = (("identifier", "type", "organizer"),)
indexes = [ indexes = [
models.Index(fields=("updated", "id")), models.Index(fields=("updated", "id"), name="pretixbase__updated_093277_idx"),
] ]
ordering = "identifier", "type", "organizer" ordering = "identifier", "type", "organizer"
+3 -3
View File
@@ -336,8 +336,8 @@ class Order(LockModel, LoggedModel):
verbose_name_plural = _("Orders") verbose_name_plural = _("Orders")
ordering = ("-datetime", "-pk") ordering = ("-datetime", "-pk")
indexes = [ indexes = [
models.Index(fields=["datetime", "id"]), models.Index(fields=["datetime", "id"], name="pretixbase__datetim_66aff0_idx"),
models.Index(fields=["last_modified", "id"]), models.Index(fields=["last_modified", "id"], name="pretixbase__last_mo_4ebf8b_idx"),
] ]
constraints = [ constraints = [
models.UniqueConstraint(fields=["organizer", "code"], name="order_organizer_code_uniq"), models.UniqueConstraint(fields=["organizer", "code"], name="order_organizer_code_uniq"),
@@ -3080,7 +3080,7 @@ class Transaction(models.Model):
class Meta: class Meta:
ordering = 'datetime', 'pk' ordering = 'datetime', 'pk'
indexes = [ indexes = [
models.Index(fields=['datetime', 'id']) models.Index(fields=['datetime', 'id'], name="pretixbase__datetim_b20405_idx")
] ]
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
+1 -1
View File
@@ -411,7 +411,7 @@ def mail_send_task(self, **kwargs) -> bool:
try: try:
outgoing_mail = OutgoingMail.objects.select_for_update(of=OF_SELF).get(pk=outgoing_mail) outgoing_mail = OutgoingMail.objects.select_for_update(of=OF_SELF).get(pk=outgoing_mail)
except OutgoingMail.DoesNotExist: except OutgoingMail.DoesNotExist:
logger.info(f"Ignoring job for non existing email {outgoing_mail.guid}") logger.info(f"Ignoring job for non existing email {outgoing_mail}")
return False return False
if outgoing_mail.status == OutgoingMail.STATUS_INFLIGHT: if outgoing_mail.status == OutgoingMail.STATUS_INFLIGHT:
logger.info(f"Ignoring job for inflight email {outgoing_mail.guid}") logger.info(f"Ignoring job for inflight email {outgoing_mail.guid}")
+9 -1
View File
@@ -100,7 +100,7 @@ def primary_font_kwargs():
choices = [('Open Sans', 'Open Sans')] choices = [('Open Sans', 'Open Sans')]
choices += sorted([ choices += sorted([
(a, {"title": a, "data": v}) for a, v in get_fonts(pdf_support_required=False).items() (a, FontSelect.FontOption(title=a, data=v)) for a, v in get_fonts(pdf_support_required=False).items()
], key=lambda a: a[0]) ], key=lambda a: a[0])
return { return {
'choices': choices, 'choices': choices,
@@ -4148,6 +4148,14 @@ def validate_event_settings(event, settings_dict):
) )
]} ]}
) )
if (
settings_dict.get('invoice_address_from_vat_id') and
settings_dict.get('invoice_address_from_country') and
settings_dict.get('invoice_address_from_country') not in VAT_ID_COUNTRIES
):
raise ValidationError({
'invoice_address_from_vat_id': _('VAT-ID is not supported for "{}".').format(settings_dict.get('invoice_address_from_country'))
})
payment_term_last = settings_dict.get('payment_term_last') payment_term_last = settings_dict.get('payment_term_last')
if payment_term_last and event.presale_end: if payment_term_last and event.presale_end:
+60 -19
View File
@@ -32,6 +32,7 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License. # License for the specific language governing permissions and limitations under the License.
import logging
import warnings import warnings
from typing import Any, Callable, Generic, List, Tuple, TypeVar from typing import Any, Callable, Generic, List, Tuple, TypeVar
@@ -48,6 +49,8 @@ from .plugins import (
PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_ORGANIZER,
) )
logger = logging.getLogger(__name__)
app_cache = {} app_cache = {}
T = TypeVar('T') T = TypeVar('T')
@@ -60,23 +63,25 @@ def _populate_app_cache():
def get_defining_app(o): def get_defining_app(o):
# If sentry packed this in a wrapper, unpack that # If sentry packed this in a wrapper, unpack that
if "sentry" in o.__module__: module = getattr(o, "__module__", None)
if module and "sentry" in module:
o = o.__wrapped__ o = o.__wrapped__
if hasattr(o, "__mocked_app"): if hasattr(o, "__mocked_app"):
return o.__mocked_app return o.__mocked_app
# Find the Django application this belongs to # Find the Django application this belongs to
searchpath = o.__module__ searchpath = module or getattr(o.__class__, "__module__", None) or ""
# Core modules are always active # Core modules are always active
if any(searchpath.startswith(cm) for cm in settings.CORE_MODULES): if searchpath and any(searchpath.startswith(cm) for cm in settings.CORE_MODULES):
return 'CORE' return 'CORE'
if not app_cache: if not app_cache:
_populate_app_cache() _populate_app_cache()
while True: app = None
while searchpath:
app = app_cache.get(searchpath) app = app_cache.get(searchpath)
if "." not in searchpath or app: if "." not in searchpath or app:
break break
@@ -157,7 +162,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
if not app_cache: if not app_cache:
_populate_app_cache() _populate_app_cache()
for receiver in self._sorted_receivers(sender): for receiver in self._live_receivers(sender)[0]:
if self._is_receiver_active(sender, receiver): if self._is_receiver_active(sender, receiver):
response = receiver(signal=self, sender=sender, **named) response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response)) responses.append((receiver, response))
@@ -179,7 +184,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
if not app_cache: if not app_cache:
_populate_app_cache() _populate_app_cache()
for receiver in self._sorted_receivers(sender): for receiver in self._live_receivers(sender)[0]:
if self._is_receiver_active(sender, receiver): if self._is_receiver_active(sender, receiver):
named[chain_kwarg_name] = response named[chain_kwarg_name] = response
response = receiver(signal=self, sender=sender, **named) response = receiver(signal=self, sender=sender, **named)
@@ -204,7 +209,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
if not app_cache: if not app_cache:
_populate_app_cache() _populate_app_cache()
for receiver in self._sorted_receivers(sender): for receiver in self._live_receivers(sender)[0]:
if self._is_receiver_active(sender, receiver): if self._is_receiver_active(sender, receiver):
try: try:
response = receiver(signal=self, sender=sender, **named) response = receiver(signal=self, sender=sender, **named)
@@ -214,17 +219,35 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
responses.append((receiver, response)) responses.append((receiver, response))
return responses return responses
def _sorted_receivers(self, sender): def asend(self, sender: T, **named):
orig_list = self._live_receivers(sender) raise NotImplementedError() # NOQA
def asend_robust(self, sender: T, **named):
raise NotImplementedError() # NOQA
def _live_receivers(self, sender):
orig_list, orig_async_list = super()._live_receivers(sender)
if orig_async_list:
logger.error('Async receivers are not supported.')
raise NotImplementedError
def _getattr_fallback_to_class(obj, key):
return getattr(obj, key, getattr(obj.__class__, key))
def _is_core_module(receiver):
m = _getattr_fallback_to_class(receiver, "__module__")
return any(m.startswith(c) for c in settings.CORE_MODULES)
sorted_list = sorted( sorted_list = sorted(
orig_list, orig_list,
key=lambda receiver: ( key=lambda receiver: (
0 if any(receiver.__module__.startswith(m) for m in settings.CORE_MODULES) else 1, 0 if _is_core_module(receiver) else 1,
receiver.__module__, _getattr_fallback_to_class(receiver, "__module__"),
receiver.__name__, _getattr_fallback_to_class(receiver, "__name__"),
) )
) )
return sorted_list return sorted_list, []
class EventPluginSignal(PluginSignal[Event]): class EventPluginSignal(PluginSignal[Event]):
@@ -300,23 +323,41 @@ class GlobalSignal(django.dispatch.Signal):
if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS: if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
return response return response
for receiver in self._live_receivers(sender): for receiver in self._live_receivers(sender)[0]:
named[chain_kwarg_name] = response named[chain_kwarg_name] = response
response = receiver(signal=self, sender=sender, **named) response = receiver(signal=self, sender=sender, **named)
return response return response
def asend(self, sender: T, **named):
raise NotImplementedError() # NOQA
def asend_robust(self, sender: T, **named):
raise NotImplementedError() # NOQA
def _live_receivers(self, sender): def _live_receivers(self, sender):
# Ensure consistent sorting of receivers # Ensure consistent sorting of receivers
orig_list = super()._live_receivers(sender) orig_list, orig_async_list = super()._live_receivers(sender)
if orig_async_list:
logger.error('Async receivers are not supported.')
raise NotImplementedError
def _getattr_fallback_to_class(obj, key):
return getattr(obj, key, getattr(obj.__class__, key))
def _is_core_module(receiver):
m = _getattr_fallback_to_class(receiver, "__module__")
return any(m.startswith(c) for c in settings.CORE_MODULES)
sorted_list = sorted( sorted_list = sorted(
orig_list, orig_list,
key=lambda receiver: ( key=lambda receiver: (
0 if any(receiver.__module__.startswith(m) for m in settings.CORE_MODULES) else 1, 0 if _is_core_module(receiver) else 1,
receiver.__module__, _getattr_fallback_to_class(receiver, "__module__"),
receiver.__name__, _getattr_fallback_to_class(receiver, "__name__"),
) )
) )
return sorted_list return sorted_list, []
class DeprecatedSignal(GlobalSignal): class DeprecatedSignal(GlobalSignal):
+6
View File
@@ -34,6 +34,7 @@
import datetime import datetime
import os import os
from dataclasses import dataclass
from django import forms from django import forms
from django.conf import settings from django.conf import settings
@@ -420,6 +421,11 @@ class SplitDateTimeField(forms.SplitDateTimeField):
class FontSelect(forms.RadioSelect): class FontSelect(forms.RadioSelect):
option_template_name = 'pretixcontrol/font_option.html' option_template_name = 'pretixcontrol/font_option.html'
@dataclass
class FontOption:
title: str
data: str
class ItemMultipleChoiceField(SafeModelMultipleChoiceField): class ItemMultipleChoiceField(SafeModelMultipleChoiceField):
def label_from_instance(self, obj): def label_from_instance(self, obj):
+11 -4
View File
@@ -63,7 +63,7 @@ from pretix.base.forms import (
from pretix.base.models import Event, Organizer, TaxRule, Team from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
from pretix.base.models.organizer import TeamQuerySet from pretix.base.models.organizer import TeamQuerySet
from pretix.base.models.tax import TAX_CODE_LISTS from pretix.base.models.tax import TAX_CODE_LISTS, VAT_ID_COUNTRIES
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.services.placeholders import FormPlaceholderMixin from pretix.base.services.placeholders import FormPlaceholderMixin
from pretix.base.settings import ( from pretix.base.settings import (
@@ -73,8 +73,8 @@ from pretix.base.settings import (
) )
from pretix.base.validators import multimail_validate from pretix.base.validators import multimail_validate
from pretix.control.forms import ( from pretix.control.forms import (
MultipleLanguagesWidget, SalesChannelCheckboxSelectMultiple, SlugWidget, FontSelect, MultipleLanguagesWidget, SalesChannelCheckboxSelectMultiple,
SplitDateTimeField, SplitDateTimePickerWidget, SlugWidget, SplitDateTimeField, SplitDateTimePickerWidget,
) )
from pretix.control.forms.widgets import Select2 from pretix.control.forms.widgets import Select2
from pretix.helpers.countries import CachedCountries from pretix.helpers.countries import CachedCountries
@@ -531,6 +531,13 @@ class EventUpdateForm(I18nModelForm):
class EventSettingsValidationMixin: class EventSettingsValidationMixin:
def clean_invoice_address_from_vat_id(self):
value = self.cleaned_data.get('invoice_address_from_vat_id')
country = self.cleaned_data.get('invoice_address_from_country')
if value and country and country not in VAT_ID_COUNTRIES:
return None
return value
def clean(self): def clean(self):
data = super().clean() data = super().clean()
settings_dict = self.obj.settings.freeze() settings_dict = self.obj.settings.freeze()
@@ -722,7 +729,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
del self.fields['event_list_filters'] del self.fields['event_list_filters']
del self.fields['event_calendar_future_only'] del self.fields['event_calendar_future_only']
self.fields['primary_font'].choices = [('Open Sans', 'Open Sans')] + sorted([ self.fields['primary_font'].choices = [('Open Sans', 'Open Sans')] + sorted([
(a, {"title": a, "data": v}) for a, v in get_fonts(self.event, pdf_support_required=False).items() (a, FontSelect.FontOption(title=a, data=v)) for a, v in get_fonts(self.event, pdf_support_required=False).items()
], key=lambda a: a[0]) ], key=lambda a: a[0])
# create "virtual" fields for better UX when editing <name>_asked and <name>_required fields # create "virtual" fields for better UX when editing <name>_asked and <name>_required fields
+1 -1
View File
@@ -363,7 +363,7 @@ def get_global_navigation(request):
'icon': 'dashboard', 'icon': 'dashboard',
}, },
] ]
if request.user.is_in_any_teams: if request.user.is_in_any_teams or request.user.is_staff:
nav += [ nav += [
{ {
'label': _('Events'), 'label': _('Events'),
@@ -8,7 +8,45 @@
{% csrf_token %} {% csrf_token %}
{% bootstrap_form_errors form %} {% bootstrap_form_errors form %}
{% bootstrap_field form.name layout='horizontal' %} {% bootstrap_field form.name layout='horizontal' %}
{% bootstrap_field form.devicetype layout='horizontal' %}
<div class="form-group{% if form.devicetype.errors %} has-error{% endif %}">
<label class="col-md-3 control-label">{% trans "Device type" %}</label>
<div class="col-md-9">
<div>
<div class="big-radio radio">
<label>
<input type="radio" required value="totp" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "totp" %}checked{% endif %}>
<strong>{% trans "Smartphone with Authenticator app" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
Use your smartphone with any Time-based One-Time-Password app like freeOTP, Google Authenticator or Proton Authenticator.
{% endblocktrans %}
</div>
</label>
</div>
<div class="big-radio radio">
<label>
<input type="radio" required value="webauthn" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "webauthn" %}checked{% endif %}>
<strong>{% trans "WebAuthn-compatible hardware token" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
Use a hardware token like the Yubikey, or other biometric authentication like fingerprint or face recognition.
{% endblocktrans %}
</div>
</label>
</div>
</div>
{% if form.devicetype.errors %}
<div class="help-block">
{% for error in form.devicetype.errors %}
<p>{{ error|escape }}</p>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="form-group submit-group"> <div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-primary btn-save">
{% trans "Continue" %} {% trans "Continue" %}
@@ -28,11 +28,6 @@
{% trans "iOS (iTunes)" %} {% trans "iOS (iTunes)" %}
</a> </a>
</li> </li>
<li>
<a href="https://m.google.com/authenticator">
{% trans "Blackberry (Link via Google)" %}
</a>
</li>
</ul> </ul>
</li> </li>
<li> <li>
+1 -1
View File
@@ -641,7 +641,7 @@ def user_index(request):
ctx = { ctx = {
'widgets': rearrange(widgets), 'widgets': rearrange(widgets),
'can_create_event': request.user.teams.with_organizer_permission("organizer.events:create").exists(), 'can_create_event': request.user.teams.with_organizer_permission("organizer.events:create").exists() or request.user.is_staff,
'upcoming': widgets_for_event_qs( 'upcoming': widgets_for_event_qs(
request, request,
annotated_event_query(request, lazy=True).filter( annotated_event_query(request, lazy=True).filter(
+11 -5
View File
@@ -25,7 +25,7 @@ import time
from django.conf import settings from django.conf import settings
from django.contrib.auth import login as auth_login from django.contrib.auth import login as auth_login
from django.contrib.gis.geoip2 import GeoIP2 from django.contrib.gis import geoip2
from django.core.cache import cache from django.core.cache import cache
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -63,14 +63,20 @@ def get_user_agent_hash(request):
_geoip = None _geoip = None
def _get_country(request): def get_geoip() -> geoip2.GeoIP2:
# See https://code.djangoproject.com/ticket/36988#ticket
global _geoip global _geoip
if not _geoip: geoip2.SUPPORTED_DATABASE_TYPES.add("Geoacumen-Country")
_geoip = GeoIP2()
if not _geoip:
_geoip = geoip2.GeoIP2()
return _geoip
def _get_country(request):
try: try:
res = _geoip.country(get_client_ip(request)) res = get_geoip().country(get_client_ip(request))
except AddressNotFoundError: except AddressNotFoundError:
return None return None
return res['country_code'] return res['country_code']
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
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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4
View File
@@ -32,6 +32,7 @@ ausgecheckt
ausgeklappt ausgeklappt
auswahl auswahl
Authentication Authentication
Authenticator
Authenticator-App Authenticator-App
Autorisierungscode Autorisierungscode
Autorisierungs-Endpunktes Autorisierungs-Endpunktes
@@ -130,6 +131,7 @@ Eingangsscan
Einlassbuchung Einlassbuchung
Einlassdatum Einlassdatum
Einlasskontrolle Einlasskontrolle
Einmalpasswörter
einzuchecken einzuchecken
email email
E-Mail-Renderer E-Mail-Renderer
@@ -163,6 +165,7 @@ Explorer
FA FA
Favicon Favicon
F-Droid F-Droid
freeOTP
Footer Footer
Footer-Link Footer-Link
Footer-Text Footer-Text
@@ -557,6 +560,7 @@ Zahlungs-ID
Zahlungspflichtig Zahlungspflichtig
Zehnerkarten Zehnerkarten
Zeitbasiert Zeitbasiert
zeitbasierte
Zeitslotbuchung Zeitslotbuchung
Zimpler Zimpler
ZIP-Datei ZIP-Datei
File diff suppressed because it is too large Load Diff
@@ -32,6 +32,7 @@ ausgecheckt
ausgeklappt ausgeklappt
auswahl auswahl
Authentication Authentication
Authenticator
Authenticator-App Authenticator-App
Autorisierungscode Autorisierungscode
Autorisierungs-Endpunktes Autorisierungs-Endpunktes
@@ -130,6 +131,7 @@ Eingangsscan
Einlassbuchung Einlassbuchung
Einlassdatum Einlassdatum
Einlasskontrolle Einlasskontrolle
Einmalpasswörter
einzuchecken einzuchecken
email email
E-Mail-Renderer E-Mail-Renderer
@@ -163,6 +165,7 @@ Explorer
FA FA
Favicon Favicon
F-Droid F-Droid
freeOTP
Footer Footer
Footer-Link Footer-Link
Footer-Text Footer-Text
@@ -557,6 +560,7 @@ Zahlungs-ID
Zahlungspflichtig Zahlungspflichtig
Zehnerkarten Zehnerkarten
Zeitbasiert Zeitbasiert
zeitbasierte
Zeitslotbuchung Zeitslotbuchung
Zimpler Zimpler
ZIP-Datei ZIP-Datei
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n" "POT-Creation-Date: 2026-03-30 11:25+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
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
+2 -2
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n" "POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2026-03-18 12:23+0000\n" "PO-Revision-Date: 2026-03-30 03:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n" "Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/es/>\n" "js/es/>\n"
@@ -329,7 +329,7 @@ msgstr "Pedido no aprobado"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
msgid "Checked-in Tickets" msgid "Checked-in Tickets"
msgstr "Registro de código QR" msgstr "Billetes registrados"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
msgid "Valid Tickets" msgid "Valid Tickets"
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
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
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
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
+5 -6
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n" "POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2026-02-10 16:49+0000\n" "PO-Revision-Date: 2026-03-25 14:14+0000\n"
"Last-Translator: Raffaele Doretto <ced@comune.portogruaro.ve.it>\n" "Last-Translator: Pietro Isotti <isottipietro@gmail.com>\n"
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
"js/it/>\n" "js/it/>\n"
"Language: it\n" "Language: it\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.15.2\n" "X-Generator: Weblate 5.16.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -310,9 +310,8 @@ msgid "Ticket code revoked/changed"
msgstr "Codice biglietto annullato/modificato" msgstr "Codice biglietto annullato/modificato"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
#, fuzzy
msgid "Ticket blocked" msgid "Ticket blocked"
msgstr "Biglietto non pagato" msgstr "Biglietto bloccato"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
msgid "Ticket not valid at this time" msgid "Ticket not valid at this time"
@@ -429,7 +428,7 @@ msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:276 #: pretix/static/pretixbase/js/asynctask.js:276
msgid "If this takes longer than a few minutes, please contact us." msgid "If this takes longer than a few minutes, please contact us."
msgstr "" msgstr "Se questa operazione richiede alcuni minuti, si prega di contattarci."
#: pretix/static/pretixbase/js/asynctask.js:331 #: pretix/static/pretixbase/js/asynctask.js:331
msgid "Close message" msgid "Close message"
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n" "POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2026-02-23 10:00+0000\n" "PO-Revision-Date: 2026-03-23 21:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n" "Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
"js/ja/>\n" "js/ja/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.16\n" "X-Generator: Weblate 5.16.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -60,7 +60,7 @@ msgstr "PayPal後払い"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41
msgid "iDEAL | Wero" msgid "iDEAL | Wero"
msgstr "" msgstr "iDEAL | Wero"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
msgid "SEPA Direct Debit" msgid "SEPA Direct Debit"
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
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
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
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
File diff suppressed because it is too large Load Diff
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n" "POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2026-01-26 22:00+0000\n" "PO-Revision-Date: 2026-03-25 08:00+0000\n"
"Last-Translator: Renne Rocha <renne@rocha.dev.br>\n" "Last-Translator: Renne Rocha <renne@rocha.dev.br>\n"
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/" "Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
"pretix/pretix-js/pt_BR/>\n" "pretix/pretix-js/pt_BR/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n" "Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.15.2\n" "X-Generator: Weblate 5.16.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -60,7 +60,7 @@ msgstr "PayPal Pay Later"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41
msgid "iDEAL | Wero" msgid "iDEAL | Wero"
msgstr "" msgstr "iDEAL | Wero"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
msgid "SEPA Direct Debit" msgid "SEPA Direct Debit"
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
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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n" "POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2025-10-10 17:00+0000\n" "PO-Revision-Date: 2026-03-26 14:29+0000\n"
"Last-Translator: Linnea Thelander <linnea@coeo.events>\n" "Last-Translator: Linnea Thelander <linnea@coeo.events>\n"
"Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/sv/>\n" "js/sv/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.13.3\n" "X-Generator: Weblate 5.16.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -1023,7 +1023,6 @@ msgid "Waiting list"
msgstr "Väntelista" msgstr "Väntelista"
#: pretix/static/pretixpresale/js/widget/widget.js:55 #: pretix/static/pretixpresale/js/widget/widget.js:55
#, fuzzy
msgctxt "widget" msgctxt "widget"
msgid "" msgid ""
"You currently have an active cart for this event. If you select more " "You currently have an active cart for this event. If you select more "
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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3
View File
@@ -12,6 +12,7 @@ anonymized
Auth Auth
authentification authentification
authenticator authenticator
Authenticator
automatical automatical
availabilities availabilities
backend backend
@@ -22,6 +23,7 @@ barcodes
Bcc Bcc
BCC BCC
BezahlCode BezahlCode
biometric
BLIK BLIK
blocklist blocklist
BN BN
@@ -56,6 +58,7 @@ EPS
eps eps
favicon favicon
filetype filetype
freeOTP
frontend frontend
frontpage frontpage
Galician Galician
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+23 -10
View File
@@ -28,7 +28,7 @@ from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.hashers import check_password from django.contrib.auth.hashers import check_password
from django.contrib.auth.password_validation import ( from django.contrib.auth.password_validation import (
get_password_validators, password_validators_help_texts, validate_password, MinimumLengthValidator, get_password_validators, validate_password,
) )
from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core import signing from django.core import signing
@@ -300,13 +300,12 @@ class SetPasswordForm(forms.Form):
) )
password = forms.CharField( password = forms.CharField(
label=_('Password'), label=_('Password'),
widget=forms.PasswordInput(attrs={'minlength': '8', 'autocomplete': 'new-password'}), widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
max_length=4096, max_length=4096,
required=True
) )
password_repeat = forms.CharField( password_repeat = forms.CharField(
label=_('Repeat password'), label=_('Repeat password'),
widget=forms.PasswordInput(attrs={'minlength': '8', 'autocomplete': 'new-password'}), widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
max_length=4096, max_length=4096,
) )
@@ -316,6 +315,14 @@ class SetPasswordForm(forms.Form):
kwargs['initial']['email'] = self.customer.email kwargs['initial']['email'] = self.customer.email
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
pw_min_len_validators = [v for v in get_customer_password_validators() if isinstance(v, MinimumLengthValidator)]
if pw_min_len_validators:
self.fields['password'].widget.attrs['minlength'] = max(v.min_length for v in pw_min_len_validators)
self.fields['password_repeat'].widget.attrs['minlength'] = max(v.min_length for v in pw_min_len_validators)
if 'password' not in self.data:
self.fields['password'].help_text = ' '.join(v.get_help_text() for v in pw_min_len_validators)
def clean(self): def clean(self):
password1 = self.cleaned_data.get('password', '') password1 = self.cleaned_data.get('password', '')
password2 = self.cleaned_data.get('password_repeat') password2 = self.cleaned_data.get('password_repeat')
@@ -329,8 +336,7 @@ class SetPasswordForm(forms.Form):
def clean_password(self): def clean_password(self):
password1 = self.cleaned_data.get('password', '') password1 = self.cleaned_data.get('password', '')
if validate_password(password1, user=self.customer, password_validators=get_customer_password_validators()) is not None: validate_password(password1, user=self.customer, password_validators=get_customer_password_validators())
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
return password1 return password1
@@ -395,13 +401,13 @@ class ChangePasswordForm(forms.Form):
) )
password = forms.CharField( password = forms.CharField(
label=_('New password'), label=_('New password'),
widget=forms.PasswordInput(attrs={'minlength': '8', 'autocomplete': 'new-password'}), widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
max_length=4096, max_length=4096,
required=True required=True
) )
password_repeat = forms.CharField( password_repeat = forms.CharField(
label=_('Repeat password'), label=_('Repeat password'),
widget=forms.PasswordInput(attrs={'minlength': '8', 'autocomplete': 'new-password'}), widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
max_length=4096, max_length=4096,
) )
@@ -411,6 +417,14 @@ class ChangePasswordForm(forms.Form):
kwargs['initial']['email'] = self.customer.email kwargs['initial']['email'] = self.customer.email
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
pw_min_len_validators = [v for v in get_customer_password_validators() if isinstance(v, MinimumLengthValidator)]
if pw_min_len_validators:
self.fields['password'].widget.attrs['minlength'] = max(v.min_length for v in pw_min_len_validators)
self.fields['password_repeat'].widget.attrs['minlength'] = max(v.min_length for v in pw_min_len_validators)
if 'password' not in self.data:
self.fields['password'].help_text = ' '.join(v.get_help_text() for v in pw_min_len_validators)
def clean(self): def clean(self):
password1 = self.cleaned_data.get('password', '') password1 = self.cleaned_data.get('password', '')
password2 = self.cleaned_data.get('password_repeat') password2 = self.cleaned_data.get('password_repeat')
@@ -424,8 +438,7 @@ class ChangePasswordForm(forms.Form):
def clean_password(self): def clean_password(self):
password1 = self.cleaned_data.get('password', '') password1 = self.cleaned_data.get('password', '')
if validate_password(password1, user=self.customer, password_validators=get_customer_password_validators()) is not None: validate_password(password1, user=self.customer, password_validators=get_customer_password_validators())
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
return password1 return password1
def clean_password_current(self): def clean_password_current(self):
+26 -10
View File
@@ -70,18 +70,21 @@ def cached_invoice_address(request):
# do not create a session, if we don't have a session we also don't have an invoice address ;) # do not create a session, if we don't have a session we also don't have an invoice address ;)
request._checkout_flow_invoice_address = InvoiceAddress() request._checkout_flow_invoice_address = InvoiceAddress()
return request._checkout_flow_invoice_address return request._checkout_flow_invoice_address
cs = cart_session(request) cs = cart_session(request, create=False)
iapk = cs.get('invoice_address') if cs is None:
if not iapk:
request._checkout_flow_invoice_address = InvoiceAddress() request._checkout_flow_invoice_address = InvoiceAddress()
else: else:
try: iapk = cs.get('invoice_address')
with scopes_disabled(): if not iapk:
request._checkout_flow_invoice_address = InvoiceAddress.objects.get(
pk=iapk, order__isnull=True
)
except InvoiceAddress.DoesNotExist:
request._checkout_flow_invoice_address = InvoiceAddress() request._checkout_flow_invoice_address = InvoiceAddress()
else:
try:
with scopes_disabled():
request._checkout_flow_invoice_address = InvoiceAddress.objects.get(
pk=iapk, order__isnull=True
)
except InvoiceAddress.DoesNotExist:
request._checkout_flow_invoice_address = InvoiceAddress()
return request._checkout_flow_invoice_address return request._checkout_flow_invoice_address
@@ -111,6 +114,12 @@ class CartMixin:
return cached_invoice_address(self.request) return cached_invoice_address(self.request)
def get_cart(self, answers=False, queryset=None, order=None, downloads=False, payments=None): def get_cart(self, answers=False, queryset=None, order=None, downloads=False, payments=None):
if not self.request.session.session_key and not order:
# The user has not even a session ID yet, so they can't have a cart and we can save a lot of work
return {
'positions': [],
# Other keys are not used on non-checkout pages
}
if queryset is not None: if queryset is not None:
prefetch = [] prefetch = []
if answers: if answers:
@@ -166,7 +175,8 @@ class CartMixin:
else: else:
fees = [] fees = []
if not order: if not order and lcp:
# Do not re-round for empty cart (useless) or confirmed order (incorrect)
apply_rounding(self.request.event.settings.tax_rounding, self.invoice_address, self.request.event.currency, [*lcp, *fees]) apply_rounding(self.request.event.settings.tax_rounding, self.invoice_address, self.request.event.currency, [*lcp, *fees])
total = sum([c.price for c in lcp]) + sum([f.value for f in fees]) total = sum([c.price for c in lcp]) + sum([f.value for f in fees])
@@ -277,6 +287,12 @@ class CartMixin:
} }
def current_selected_payments(self, positions, fees, invoice_address, *, warn=False): def current_selected_payments(self, positions, fees, invoice_address, *, warn=False):
from pretix.presale.views.cart import get_or_create_cart_id
if not get_or_create_cart_id(self.request, create=False):
# No active cart ID, no payments there
return []
raw_payments = copy.deepcopy(self.cart_session.get('payments', [])) raw_payments = copy.deepcopy(self.cart_session.get('payments', []))
fees = [f for f in fees if f.fee_type != OrderFee.FEE_TYPE_PAYMENT] # we re-compute these here fees = [f for f in fees if f.fee_type != OrderFee.FEE_TYPE_PAYMENT] # we re-compute these here
+4 -2
View File
@@ -417,7 +417,7 @@ def get_or_create_cart_id(request, create=True):
return new_id return new_id
def cart_session(request): def cart_session(request, create=True):
""" """
Before pretix 1.8.0, all checkout-related information (like the entered email address) was stored Before pretix 1.8.0, all checkout-related information (like the entered email address) was stored
in the user's regular session dictionary. This led to data interference and leaks for example if a in the user's regular session dictionary. This led to data interference and leaks for example if a
@@ -428,7 +428,9 @@ def cart_session(request):
active cart session sub-dictionary for read and write access. active cart session sub-dictionary for read and write access.
""" """
request.session.modified = True request.session.modified = True
cart_id = get_or_create_cart_id(request) cart_id = get_or_create_cart_id(request, create=create)
if not cart_id and not create:
return None
return request.session['carts'][cart_id] return request.session['carts'][cart_id]
+1 -1
View File
@@ -576,7 +576,7 @@ def filter_subevents_with_plugins(subevents, sales_channel=None):
if not app_cache: if not app_cache:
_populate_app_cache() _populate_app_cache()
for receiver in filter_subevents._live_receivers(None): for receiver in filter_subevents._live_receivers(None)[0]:
app = get_defining_app(receiver) app = get_defining_app(receiver)
event_state = {} event_state = {}
+3 -2
View File
@@ -49,7 +49,7 @@ from django.views.decorators.cache import cache_page
from django.views.decorators.gzip import gzip_page from django.views.decorators.gzip import gzip_page
from django.views.decorators.http import condition from django.views.decorators.http import condition
from django.views.i18n import ( from django.views.i18n import (
JavaScriptCatalog, get_formats, js_catalog_template, JavaScriptCatalog, builtin_template_path, get_formats,
) )
from lxml import html from lxml import html
@@ -170,7 +170,8 @@ def generate_widget_js(version, lang):
'September', 'October', 'November', 'December' 'September', 'October', 'November', 'December'
) )
catalog = dict((k, v) for k, v in catalog.items() if k.startswith('widget\u0004') or k in str_wl) catalog = dict((k, v) for k, v in catalog.items() if k.startswith('widget\u0004') or k in str_wl)
template = Engine().from_string(js_catalog_template) with builtin_template_path("i18n_catalog.js").open(encoding="utf-8") as fh:
template = Engine().from_string(fh.read())
context = Context({ context = Context({
'catalog_str': indent(json.dumps( 'catalog_str': indent(json.dumps(
catalog, sort_keys=True, indent=2)) if catalog else None, catalog, sort_keys=True, indent=2)) if catalog else None,
+1
View File
@@ -534,6 +534,7 @@ X_FRAME_OPTIONS = 'DENY'
# URL settings # URL settings
ROOT_URLCONF = 'pretix.multidomain.maindomain_urlconf' ROOT_URLCONF = 'pretix.multidomain.maindomain_urlconf'
FORMS_URLFIELD_ASSUME_HTTPS = True # transitional for django 6.0
WSGI_APPLICATION = 'pretix.wsgi.application' WSGI_APPLICATION = 'pretix.wsgi.application'
+1 -3
View File
@@ -23,9 +23,7 @@ filterwarnings =
error error
ignore:.*invalid escape sequence.*: ignore:.*invalid escape sequence.*:
ignore:The 'warn' method is deprecated:DeprecationWarning ignore:The 'warn' method is deprecated:DeprecationWarning
ignore::django.utils.deprecation.RemovedInDjango51Warning:django.core.files.storage ignore::django.utils.deprecation.RemovedInDjango60Warning:
ignore:.*index_together.*:django.utils.deprecation.RemovedInDjango51Warning:
ignore:.*get_storage_class.*:django.utils.deprecation.RemovedInDjango51Warning:compressor
ignore:.*This signal will soon be only available for plugins that declare to be organizer-level.*:DeprecationWarning: ignore:.*This signal will soon be only available for plugins that declare to be organizer-level.*:DeprecationWarning:
ignore::DeprecationWarning:mt940 ignore::DeprecationWarning:mt940
ignore::DeprecationWarning:cbor2 ignore::DeprecationWarning:cbor2
+4 -5
View File
@@ -19,14 +19,13 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # 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/>. # <https://www.gnu.org/licenses/>.
# #
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
import aiohttp import aiohttp
import pytest import pytest
import pytest_asyncio import pytest_asyncio
from django.utils.timezone import now from django.utils.timezone import now
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from pytz import UTC
from pretix.base.models import ( from pretix.base.models import (
Device, Event, Item, Organizer, Quota, SeatingPlan, Device, Event, Item, Organizer, Quota, SeatingPlan,
@@ -53,7 +52,7 @@ def organizer():
def event(organizer): def event(organizer):
e = Event.objects.create( e = Event.objects.create(
organizer=organizer, name='Dummy', slug='dummy', organizer=organizer, name='Dummy', slug='dummy',
date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC), date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=timezone.utc),
presale_end=now() + timedelta(days=300), presale_end=now() + timedelta(days=300),
plugins='pretix.plugins.banktransfer,pretix.plugins.ticketoutputpdf', plugins='pretix.plugins.banktransfer,pretix.plugins.ticketoutputpdf',
is_public=True, live=True is_public=True, live=True
@@ -109,8 +108,8 @@ def customer(event, membership_type):
def membership(event, membership_type, customer): def membership(event, membership_type, customer):
return customer.memberships.create( return customer.memberships.create(
membership_type=membership_type, membership_type=membership_type,
date_start=datetime(2017, 1, 1, 0, 0, tzinfo=UTC), date_start=datetime(2017, 1, 1, 0, 0, tzinfo=timezone.utc),
date_end=datetime(2099, 1, 1, 0, 0, tzinfo=UTC), date_end=datetime(2099, 1, 1, 0, 0, tzinfo=timezone.utc),
) )
+29
View File
@@ -36,6 +36,7 @@
import datetime import datetime
import re import re
from decimal import Decimal from decimal import Decimal
from importlib import import_module
from json import loads from json import loads
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
@@ -80,6 +81,34 @@ class EventMiddlewareTest(EventTestMixin, SoupTest):
doc = self.get_doc('/%s/%s/' % (self.orga.slug, self.event.slug)) doc = self.get_doc('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertIn(str(self.event.name), doc.find("h1").text) self.assertIn(str(self.event.name), doc.find("h1").text)
def test_no_session_cookie_set_on_event_index_view(self):
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertEqual(resp.status_code, 200)
assert settings.SESSION_COOKIE_NAME not in self.client.cookies
def test_no_cart_session_added_on_event_index_view(self):
# Make sure a session is present by doing a cart op on another event
event2 = Event.objects.create(
organizer=self.orga, name='30C3b', slug='30c3b',
date_from=datetime.datetime(now().year + 1, 12, 26, 14, 0, tzinfo=datetime.timezone.utc),
live=True,
)
self.client.post('/%s/%s/cart/add' % (self.orga.slug, event2.slug), {
'item_%d' % 1337: '1', # item does not need to exist
'ajax': 1
})
assert settings.SESSION_COOKIE_NAME in self.client.cookies
# Visit shop, make sure no session is created
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertEqual(resp.status_code, 200)
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
session = SessionStore(self.client.cookies[settings.SESSION_COOKIE_NAME].value).load()
assert set(session.keys()) == {
f"current_cart_event_{event2.pk}", "carts"
}
def test_not_found(self): def test_not_found(self):
resp = self.client.get('/%s/%s/' % ('foo', 'bar')) resp = self.client.get('/%s/%s/' % ('foo', 'bar'))
self.assertEqual(resp.status_code, 404) self.assertEqual(resp.status_code, 404)