mirror of
https://github.com/pretix/pretix.git
synced 2026-08-14 11:26:26 +00:00
Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1ba5c298d | ||
|
|
c6c48537dd | ||
|
|
ab08dea9f7 | ||
|
|
4d15731528 | ||
|
|
a2cef22ea8 | ||
|
|
3843448812 | ||
|
|
49893ca9df | ||
|
|
4eade5070e | ||
|
|
32b1997208 | ||
|
|
eaf4a310f6 | ||
|
|
8dc0f7c1b2 | ||
|
|
dd3e6c4692 | ||
|
|
c7437336b4 | ||
|
|
4c0c775baa | ||
|
|
394652a5ff | ||
|
|
3f50d065ec | ||
|
|
4121061267 | ||
|
|
aed2220139 | ||
|
|
4b2c54d38e | ||
|
|
0113a3dc1f | ||
|
|
c12a8935f1 | ||
|
|
a86a6cc2c7 | ||
|
|
fec2b9a2fc | ||
|
|
d847a7e8f8 | ||
|
|
c58a968196 | ||
|
|
81cbaca162 | ||
|
|
218df7a49f | ||
|
|
f64343d977 | ||
|
|
b36c7cbef3 | ||
|
|
18b39ba7cd | ||
|
|
1383e967df | ||
|
|
c743e9fd3f | ||
|
|
a71efa6747 | ||
|
|
4fed47fb9b | ||
|
|
c143d50290 | ||
|
|
88cd715ece | ||
|
|
3513de6a45 | ||
|
|
fd6d3934c0 |
@@ -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 }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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.*",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
+310
-288
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user