Compare commits

..
Author SHA1 Message Date
Mira Weller 9da8c1f7b2 reauth flow token
add a special token to always allow completing a form submission, even if the reauthentication time has expired
2026-03-26 12:41:19 +01:00
Mira Weller 6b340682b2 ui changes 2026-03-20 12:56:57 +01:00
Mira Weller 0dc436067f always perform 2fa activation as dedicated step 2026-03-20 12:56:38 +01:00
Mira Weller db66c91108 generate emergency tokens during 2fa activation 2026-03-20 12:55:58 +01:00
Mira Weller 3a1db55e8b remove broken blackberry link 2026-03-20 12:06:56 +01:00
Mira Weller 57da5cbae2 improve 2fa type selection 2026-03-20 11:44:10 +01:00
107 changed files with 16310 additions and 17652 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
name: Packaging name: Packaging
strategy: strategy:
matrix: matrix:
python-version: ["3.13"] python-version: ["3.11"]
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.13 - name: Set up Python 3.11
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: 3.13 python-version: 3.11
- 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.13 - name: Set up Python 3.11
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: 3.13 python-version: 3.11
- 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.13 - name: Set up Python 3.11
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: 3.13 python-version: 3.11
- 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.13 - name: Set up Python 3.11
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: 3.13 python-version: 3.11
- 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.13 - name: Set up Python 3.11
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: 3.13 python-version: 3.11
- name: Install Dependencies - name: Install Dependencies
run: pip3 install licenseheaders run: pip3 install licenseheaders
- name: Run licenseheaders - name: Run licenseheaders
+2 -4
View File
@@ -23,15 +23,13 @@ jobs:
name: Tests name: Tests
strategy: strategy:
matrix: matrix:
python-version: ["3.11", "3.13", "3.14"] python-version: ["3.10", "3.11", "3.13"]
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
@@ -83,4 +81,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.13' if: matrix.database == 'postgres' && matrix.python-version == '3.11'
+7 -8
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.11" requires-python = ">=3.10"
license = {file = "LICENSE"} license = {file = "LICENSE"}
keywords = ["tickets", "web", "shop", "ecommerce"] keywords = ["tickets", "web", "shop", "ecommerce"]
authors = [ authors = [
@@ -19,11 +19,10 @@ 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",
"Programming Language :: Python :: 3.12", "Framework :: Django :: 4.2",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Framework :: Django :: 5.2",
] ]
dependencies = [ dependencies = [
@@ -37,7 +36,7 @@ dependencies = [
"css-inline==0.20.*", "css-inline==0.20.*",
"defusedcsv>=1.1.0", "defusedcsv>=1.1.0",
"dnspython==2.*", "dnspython==2.*",
"Django[argon2]==5.2.*", "Django[argon2]==4.2.*,>=4.2.26",
"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.*",
@@ -60,7 +59,7 @@ dependencies = [
"dnspython==2.8.*", "dnspython==2.8.*",
"drf_ujson2==1.7.*", "drf_ujson2==1.7.*",
"geoip2==5.*", "geoip2==5.*",
"importlib_metadata==9.*", # Polyfill, we can probably drop this once we require Python 3.10+ "importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek", "isoweek",
"jsonschema", "jsonschema",
"kombu==5.6.*", "kombu==5.6.*",
@@ -93,7 +92,7 @@ dependencies = [
"redis==7.1.*", "redis==7.1.*",
"reportlab==4.4.*", "reportlab==4.4.*",
"requests==2.32.*", "requests==2.32.*",
"sentry-sdk==2.56.*", "sentry-sdk==2.54.*",
"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.1" __version__ = "2026.3.0.dev0"
+1 -1
View File
@@ -1122,7 +1122,7 @@ class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
permission = 'event.orders:read' permission = 'event.orders:read'
def get_queryset(self): def get_queryset(self):
qs = Checkin.all.filter(list__event=self.request.event).select_related( qs = Checkin.all.filter().select_related(
"position", "position",
"device", "device",
) )
+2 -1
View File
@@ -196,7 +196,8 @@ 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'))
validate_password(password1, user=user) if validate_password(password1, user=user) is not None:
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,6 +45,7 @@ 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 (
@@ -101,7 +102,6 @@ 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 = get_geoip() g = GeoIP2()
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,9 +36,8 @@ 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)[0]: for receiver in periodic_task._live_receivers(self):
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,20 +41,16 @@ class Migration(migrations.Migration):
name='datetime', name='datetime',
field=models.DateTimeField(), field=models.DateTimeField(),
), ),
migrations.AddIndex( migrations.AlterIndexTogether(
'logentry', name='logentry',
models.Index(fields=('datetime', 'id'), name="pretixbase__datetim_b1fe5a_idx"), index_together={('datetime', 'id')},
), ),
migrations.AddIndex( migrations.AlterIndexTogether(
'order', name='order',
models.Index(fields=["datetime", "id"], name="pretixbase__datetim_66aff0_idx"), index_together={('datetime', 'id'), ('last_modified', 'id')},
), ),
migrations.AddIndex( migrations.AlterIndexTogether(
'order', name='transaction',
models.Index(fields=["last_modified", "id"], name="pretixbase__last_mo_4ebf8b_idx"), index_together={('datetime', 'id')},
),
migrations.AddIndex(
'transaction',
models.Index(fields=('datetime', 'id'), name="pretixbase__datetim_b20405_idx"),
), ),
] ]
@@ -61,10 +61,7 @@ class Migration(migrations.Migration):
options={ options={
'ordering': ('identifier', 'type', 'organizer'), 'ordering': ('identifier', 'type', 'organizer'),
'unique_together': {('identifier', 'type', 'organizer')}, 'unique_together': {('identifier', 'type', 'organizer')},
'indexes': [ 'index_together': {('identifier', 'type', 'organizer'), ('updated', 'id')},
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,6 +9,31 @@ 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, models from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -10,8 +10,8 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.RemoveIndex( migrations.AlterIndexTogether(
"reusablemedium", name="reusablemedium",
'reusable_medium_organizer_index', index_together=set(),
), ),
] ]
+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"], name="pretixbase__datetim_b1fe5a_idx")] indexes = [models.Index(fields=["datetime", "id"])]
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"), name="pretixbase__updated_093277_idx"), models.Index(fields=("updated", "id")),
] ]
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"], name="pretixbase__datetim_66aff0_idx"), models.Index(fields=["datetime", "id"]),
models.Index(fields=["last_modified", "id"], name="pretixbase__last_mo_4ebf8b_idx"), models.Index(fields=["last_modified", "id"]),
] ]
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'], name="pretixbase__datetim_b20405_idx") models.Index(fields=['datetime', 'id'])
] ]
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}") logger.info(f"Ignoring job for non existing email {outgoing_mail.guid}")
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}")
+1 -9
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, FontSelect.FontOption(title=a, data=v)) for a, v in get_fonts(pdf_support_required=False).items() (a, {"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,14 +4148,6 @@ 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:
+19 -60
View File
@@ -32,7 +32,6 @@
# 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
@@ -49,8 +48,6 @@ from .plugins import (
PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_ORGANIZER,
) )
logger = logging.getLogger(__name__)
app_cache = {} app_cache = {}
T = TypeVar('T') T = TypeVar('T')
@@ -63,25 +60,23 @@ 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
module = getattr(o, "__module__", None) if "sentry" in o.__module__:
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 = module or getattr(o.__class__, "__module__", None) or "" searchpath = o.__module__
# Core modules are always active # Core modules are always active
if searchpath and any(searchpath.startswith(cm) for cm in settings.CORE_MODULES): if 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()
app = None while True:
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
@@ -162,7 +157,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._live_receivers(sender)[0]: for receiver in self._sorted_receivers(sender):
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))
@@ -184,7 +179,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._live_receivers(sender)[0]: for receiver in self._sorted_receivers(sender):
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)
@@ -209,7 +204,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._live_receivers(sender)[0]: for receiver in self._sorted_receivers(sender):
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)
@@ -219,35 +214,17 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
responses.append((receiver, response)) responses.append((receiver, response))
return responses return responses
def asend(self, sender: T, **named): def _sorted_receivers(self, sender):
raise NotImplementedError() # NOQA orig_list = self._live_receivers(sender)
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 _is_core_module(receiver) else 1, 0 if any(receiver.__module__.startswith(m) for m in settings.CORE_MODULES) else 1,
_getattr_fallback_to_class(receiver, "__module__"), receiver.__module__,
_getattr_fallback_to_class(receiver, "__name__"), receiver.__name__,
) )
) )
return sorted_list, [] return sorted_list
class EventPluginSignal(PluginSignal[Event]): class EventPluginSignal(PluginSignal[Event]):
@@ -323,41 +300,23 @@ 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)[0]: for receiver in self._live_receivers(sender):
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, orig_async_list = super()._live_receivers(sender) orig_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 _is_core_module(receiver) else 1, 0 if any(receiver.__module__.startswith(m) for m in settings.CORE_MODULES) else 1,
_getattr_fallback_to_class(receiver, "__module__"), receiver.__module__,
_getattr_fallback_to_class(receiver, "__name__"), receiver.__name__,
) )
) )
return sorted_list, [] return sorted_list
class DeprecatedSignal(GlobalSignal): class DeprecatedSignal(GlobalSignal):
-6
View File
@@ -34,7 +34,6 @@
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
@@ -421,11 +420,6 @@ 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):
+4 -11
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, VAT_ID_COUNTRIES from pretix.base.models.tax import TAX_CODE_LISTS
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 (
FontSelect, MultipleLanguagesWidget, SalesChannelCheckboxSelectMultiple, MultipleLanguagesWidget, SalesChannelCheckboxSelectMultiple, SlugWidget,
SlugWidget, SplitDateTimeField, SplitDateTimePickerWidget, 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,13 +531,6 @@ 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()
@@ -729,7 +722,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, FontSelect.FontOption(title=a, data=v)) for a, v in get_fonts(self.event, pdf_support_required=False).items() (a, {"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 or request.user.is_staff: if request.user.is_in_any_teams:
nav += [ nav += [
{ {
'label': _('Events'), 'label': _('Events'),
@@ -6,44 +6,35 @@
<h1>{% trans "Add a two-factor authentication device" %}</h1> <h1>{% trans "Add a two-factor authentication device" %}</h1>
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
{% bootstrap_form_errors form %} {% bootstrap_form_errors form %}
{% bootstrap_field form.name layout='horizontal' %} {% bootstrap_field form.name layout='horizontal' %}
<div class="form-group{% if form.devicetype.errors %} has-error{% endif %}"> <div class="form-group">
<label class="col-md-3 control-label">{% trans "Device type" %}</label> <label class="col-md-3 control-label">{% trans "Device type" %}</label>
<div class="col-md-9"> <div class="col-md-9">
<div> <div class="big-radio radio">
<div class="big-radio radio"> <label>
<label> <input type="radio" value="totp" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "totp" %}checked{% endif %}>
<input type="radio" required value="totp" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "totp" %}checked{% endif %}> <strong>{% trans "Smartphone with the Authenticator application" %}</strong><br>
<strong>{% trans "Smartphone with Authenticator app" %}</strong><br> <div class="help-block">
<div class="help-block"> {% blocktrans trimmed %}
{% blocktrans trimmed %} Use your smartphone with any Time-based One-Time-Password app like freeOTP, Google Authenticator or Proton Authenticator.
Use your smartphone with any Time-based One-Time-Password app like freeOTP, Google Authenticator or Proton Authenticator. {% endblocktrans %}
{% endblocktrans %} </div>
</div> </label>
</label> </div>
</div> <div class="big-radio radio">
<div class="big-radio radio"> <label>
<label> <input type="radio" value="webauthn" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "webauthn" %}checked{% endif %}>
<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>
<strong>{% trans "WebAuthn-compatible hardware token" %}</strong><br> <div class="help-block">
<div class="help-block"> {% blocktrans trimmed %}
{% blocktrans trimmed %} Use a hardware token like the Yubikey, or biometric authentication on iOS, macOS and Android.
Use a hardware token like the Yubikey, or other biometric authentication like fingerprint or face recognition. {% endblocktrans %}
{% endblocktrans %} </div>
</div> </label>
</label>
</div>
</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> </div>
@@ -69,14 +69,11 @@
{% trans "Enter the displayed code here:" %} {% trans "Enter the displayed code here:" %}
<form class="form form-inline" method="post" action=""> <form class="form form-inline" method="post" action="">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
<input type="number" name="token" class="form-control" required="required"> <input type="number" name="token" class="form-control" required="required">
<button class="btn btn-primary" type="submit"> <button class="btn btn-primary" type="submit">
{% trans "Continue" %} {% trans "Continue" %}
</button><br> </button><br>
<label>
<input type="checkbox" name="activate" checked="checked" value="on">
{% trans "Require second factor for future logins" %}
</label>
</form> </form>
</li> </li>
</ol> </ol>
@@ -12,13 +12,9 @@
</p> </p>
<form class="form form-inline" method="post" action="" id="webauthn-form"> <form class="form form-inline" method="post" action="" id="webauthn-form">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
<input type="hidden" id="webauthn-response" name="token" class="form-control" required="required"> <input type="hidden" id="webauthn-response" name="token" class="form-control" required="required">
<p>
<label>
<input type="checkbox" name="activate" checked="checked" value="on">
{% trans "Require second factor for future logins" %}
</label>
</p>
<button class="btn btn-primary sr-only" type="submit"></button> <button class="btn btn-primary sr-only" type="submit"></button>
</form> </form>
@@ -6,6 +6,7 @@
<h1>{% trans "Delete a two-factor authentication device" %}</h1> <h1>{% trans "Delete a two-factor authentication device" %}</h1>
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
<p>{% blocktrans trimmed with device=device.name %} <p>{% blocktrans trimmed with device=device.name %}
Are you sure you want to delete the authentication device "{{ device }}"? Are you sure you want to delete the authentication device "{{ device }}"?
{% endblocktrans %}</p> {% endblocktrans %}</p>
@@ -6,6 +6,7 @@
<h1>{% trans "Disable two-factor authentication" %}</h1> <h1>{% trans "Disable two-factor authentication" %}</h1>
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
<p> <p>
{% trans "Do you really want to disable two-factor authentication?" %} {% trans "Do you really want to disable two-factor authentication?" %}
</p> </p>
@@ -1,23 +1,58 @@
{% extends "pretixcontrol/base.html" %} {% extends "pretixcontrol/base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load icon %}
{% block title %}{% trans "Enable two-factor authentication" %}{% endblock %} {% block title %}{% trans "Enable two-factor authentication" %}{% endblock %}
{% block content %} {% block content %}
<h1>{% trans "Enable two-factor authentication" %}</h1> <h1>{% trans "Enable two-factor authentication" %}</h1>
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
<p> <p>
{% trans "Do you really want to enable two-factor authentication?" %} {% trans "Do you really want to enable two-factor authentication?" %}
</p> </p>
<p> <p>
{% trans "You will no longer be able to log in to pretix without one of your configured devices." %} {% trans "You will no longer be able to log in to pretix without one of your configured devices." %}
{% trans "Please make sure to print out or copy the emergency tokens and store them in a safe place." %}
</p> </p>
{% if new_emergency_tokens %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Your emergency codes" %}</h3>
</div>
<div class="panel-body">
<p>
{% blocktrans trimmed %}
If you lose access to your devices, you can use one of your emergency tokens to log in.
We recommend to store them in a safe place, e.g. printed out or in a password manager.
Every token can be used at most once.
{% endblocktrans %}
</p>
<ul>
{% for code in new_emergency_tokens %}
<li>{{ code }}</li>
{% endfor %}
</ul>
<p>
<label>
<input type="checkbox" required>
{% trans "I stored my emergency tokens in a safe place." %}
</label>
</p>
</div>
</div>
{% else %}
<p>
{% icon "info-circle" %}
{% blocktrans trimmed with generation_date_time=static_tokens_device.created_at %}
You generated your emergency tokens on {{ generation_date_time }}.
{% endblocktrans %}
</p>
{% endif %}
<div class="form-group submit-group"> <div class="form-group submit-group">
<a href="{% url "control:user.settings.2fa" %}" class="btn btn-default btn-cancel"> <a href="{% url "control:user.settings.2fa" %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %} {% trans "Cancel" %}
</a> </a>
<button type="submit" class="btn btn-danger btn-save"> <button type="submit" class="btn btn-primary btn-save">
{% trans "Enable" %} {% trans "Enable" %}
</button> </button>
</div> </div>
@@ -6,6 +6,7 @@
<h1>{% trans "Leave teams that require two-factor authentication" %}</h1> <h1>{% trans "Leave teams that require two-factor authentication" %}</h1>
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
<p> <p>
<strong>{% trans "Do you really want to leave the following teams?" %}</strong> <strong>{% trans "Do you really want to leave the following teams?" %}</strong>
</p> </p>
@@ -1,5 +1,6 @@
{% extends "pretixcontrol/base.html" %} {% extends "pretixcontrol/base.html" %}
{% load i18n %} {% load i18n %}
{% load icon %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% block title %}{% trans "Two-factor authentication" %}{% endblock %} {% block title %}{% trans "Two-factor authentication" %}{% endblock %}
{% block content %} {% block content %}
@@ -120,7 +121,7 @@
Delete Delete
</a> </a>
{% if d.devicetype == "totp" %} {% if d.devicetype == "totp" %}
<span class="fa fa-mobile"></span> <span class="fa fa-mobile fa-lg"></span>
{% elif d.devicetype == "webauthn" %} {% elif d.devicetype == "webauthn" %}
<span class="fa fa-usb"></span> <span class="fa fa-usb"></span>
{% elif d.devicetype == "u2f" %} {% elif d.devicetype == "u2f" %}
@@ -152,19 +153,30 @@
</p> </p>
{% if static_tokens_device %} {% if static_tokens_device %}
<p> <p>
{% icon "info-circle" %}
{% blocktrans trimmed with generation_date_time=static_tokens_device.created_at %} {% blocktrans trimmed with generation_date_time=static_tokens_device.created_at %}
You generated your emergency tokens on {{ generation_date_time }}. You generated your emergency tokens on {{ generation_date_time }}.
{% endblocktrans %} {% endblocktrans %}
</p> </p>
{% else %} <a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default">
<span class="fa fa-refresh"></span>
{% trans "Generate new emergency tokens" %}
</a>
{% elif user.require_2fa %}
<p> <p>
{% trans "You don't have any emergency tokens yet." %} {% icon "warning" %}
<strong>{% trans "You don't have any emergency tokens yet." %}</strong>
</p>
<a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default">
<span class="fa fa-refresh"></span>
{% trans "Generate emergency tokens" %}
</a>
{% else %}
<p class="help-block">
{% icon "info-circle" %}
{% trans "Emergency tokens will be generated when you enable two-factor authentication." %}
</p> </p>
{% endif %} {% endif %}
<a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default">
<span class="fa fa-refresh"></span>
{% trans "Generate new emergency tokens" %}
</a>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
@@ -6,6 +6,7 @@
<h1>{% trans "Regenerate emergency codes" %}</h1> <h1>{% trans "Regenerate emergency codes" %}</h1>
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
<p> <p>
{% trans "Do you really want to regenerate your emergency codes?" %} {% trans "Do you really want to regenerate your emergency codes?" %}
</p> </p>
@@ -8,6 +8,7 @@
{% trans "Change login email address" %} {% trans "Change login email address" %}
</h1> </h1>
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
{% bootstrap_form_errors form %} {% bootstrap_form_errors form %}
<p class="text-muted"> <p class="text-muted">
{% trans "This changes the email address used to login to your account, as well as where we send email notifications." %} {% trans "This changes the email address used to login to your account, as well as where we send email notifications." %}
@@ -9,6 +9,7 @@
</h1> </h1>
<br> <br>
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
{% bootstrap_form_errors form %} {% bootstrap_form_errors form %}
{% bootstrap_field form.email %} {% bootstrap_field form.email %}
{% bootstrap_field form.old_pw %} {% bootstrap_field form.old_pw %}
+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() or request.user.is_staff, 'can_create_event': request.user.teams.with_organizer_permission("organizer.events:create").exists(),
'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(
+62 -36
View File
@@ -89,13 +89,31 @@ logger = logging.getLogger(__name__)
class RecentAuthenticationRequiredMixin: class RecentAuthenticationRequiredMixin:
max_time = 900 max_time = 900
max_form_time = 900
@method_decorator(never_cache) @method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
tdelta = time.time() - request.session.get('pretix_auth_login_time', 0) auth_is_recent = time.time() - request.session.get('pretix_auth_login_time', 0) < self.max_time
if tdelta > self.max_time: allowed_by_token = (
request.session.pop('pretix_reauthed_flow_token', None) == request.POST.get('flow_token', '')
and request.session.pop('pretix_reauthed_flow_allowed_url', None) == request.get_full_path()
and time.time() - request.session.pop('pretix_reauthed_flow_start_time', 0) < self.max_form_time
)
if auth_is_recent or allowed_by_token:
return super().dispatch(request, *args, **kwargs)
else:
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path())) return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
return super().dispatch(request, *args, **kwargs)
def get_flow_token(self):
self.request.session['pretix_reauthed_flow_allowed_url'] = self.request.get_full_path()
self.request.session['pretix_reauthed_flow_token'] = get_random_string(22)
self.request.session['pretix_reauthed_flow_start_time'] = time.time()
return self.request.session['pretix_reauthed_flow_token']
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['flow_token'] = self.get_flow_token()
return ctx
class ReauthView(TemplateView): class ReauthView(TemplateView):
@@ -283,6 +301,7 @@ class UserHistoryView(ListView):
class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView): class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
max_time = 7200
template_name = 'pretixcontrol/user/2fa_main.html' template_name = 'pretixcontrol/user/2fa_main.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@@ -465,25 +484,15 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
notices = [ notices = [
_('A new two-factor authentication device has been added to your account.') _('A new two-factor authentication device has been added to your account.')
] ]
activate = request.POST.get('activate', '')
if activate == 'on' and not self.request.user.require_2fa:
self.request.user.require_2fa = True
self.request.user.save()
self.request.user.log_action('pretix.user.settings.2fa.enabled', user=self.request.user)
notices.append(
_('Two-factor authentication has been enabled.')
)
self.request.user.send_security_notice(notices) self.request.user.send_security_notice(notices)
self.request.user.update_session_token() self.request.user.update_session_token()
update_session_auth_hash(self.request, self.request.user) update_session_auth_hash(self.request, self.request.user)
note = '' messages.success(request, str(_('The device has been verified and can now be used.')))
if not self.request.user.require_2fa: if self.request.user.require_2fa:
note = ' ' + str(_('Please note that you still need to enable two-factor authentication for your ' return redirect(reverse('control:user.settings.2fa'))
'account using the buttons below to make a second factor required for logging ' else:
'into your account.')) return redirect(reverse('control:user.settings.2fa.enable'))
messages.success(request, str(_('The device has been verified and can now be used.')) + note)
return redirect(reverse('control:user.settings.2fa'))
except Exception: except Exception:
messages.error(request, _('The registration could not be completed. Please try again.')) messages.error(request, _('The registration could not be completed. Please try again.'))
logger.exception('WebAuthn registration failed') logger.exception('WebAuthn registration failed')
@@ -494,6 +503,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateView): class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_confirm_totp.html' template_name = 'pretixcontrol/user/2fa_confirm_totp.html'
max_form_time = 7200 # this should have effectively no timeout, as the user might need to download the 2fa app first
@cached_property @cached_property
def device(self): def device(self):
@@ -514,7 +524,6 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
token = request.POST.get('token', '') token = request.POST.get('token', '')
activate = request.POST.get('activate', '')
if self.device.verify_token(token): if self.device.verify_token(token):
self.device.confirmed = True self.device.confirmed = True
self.device.save() self.device.save()
@@ -526,24 +535,15 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
notices = [ notices = [
_('A new two-factor authentication device has been added to your account.') _('A new two-factor authentication device has been added to your account.')
] ]
if activate == 'on' and not self.request.user.require_2fa:
self.request.user.require_2fa = True
self.request.user.save()
self.request.user.log_action('pretix.user.settings.2fa.enabled', user=self.request.user)
notices.append(
_('Two-factor authentication has been enabled.')
)
self.request.user.send_security_notice(notices) self.request.user.send_security_notice(notices)
self.request.user.update_session_token() self.request.user.update_session_token()
update_session_auth_hash(self.request, self.request.user) update_session_auth_hash(self.request, self.request.user)
note = '' messages.success(request, str(_('The device has been verified and can now be used.')))
if not self.request.user.require_2fa: if self.request.user.require_2fa:
note = ' ' + str(_('Please note that you still need to enable two-factor authentication for your ' return redirect(reverse('control:user.settings.2fa'))
'account using the buttons below to make a second factor required for logging ' else:
'into your account.')) return redirect(reverse('control:user.settings.2fa.enable'))
messages.success(request, str(_('The device has been verified and can now be used.')) + note)
return redirect(reverse('control:user.settings.2fa'))
else: else:
messages.error(request, _('The code you entered was not valid. If this problem persists, please check ' messages.error(request, _('The code you entered was not valid. If this problem persists, please check '
'that the date and time of your phone are configured correctly.')) 'that the date and time of your phone are configured correctly.'))
@@ -576,6 +576,7 @@ class User2FALeaveTeamsView(RecentAuthenticationRequiredMixin, TemplateView):
class User2FAEnableView(RecentAuthenticationRequiredMixin, TemplateView): class User2FAEnableView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_enable.html' template_name = 'pretixcontrol/user/2fa_enable.html'
max_form_time = 7200 # this should have effectively no timeout, as the user might take some time to print out their emergency codes, and they would become invalid in case of a timeout
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not any(dt.objects.filter(user=self.request.user, confirmed=True) for dt in REAL_DEVICE_TYPES): if not any(dt.objects.filter(user=self.request.user, confirmed=True) for dt in REAL_DEVICE_TYPES):
@@ -584,14 +585,39 @@ class User2FAEnableView(RecentAuthenticationRequiredMixin, TemplateView):
return redirect(reverse('control:user.settings.2fa')) return redirect(reverse('control:user.settings.2fa'))
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
new_tokens = None
try:
static_tokens_device = StaticDevice.objects.get(user=self.request.user, name='emergency')
except StaticDevice.MultipleObjectsReturned:
static_tokens_device = StaticDevice.objects.filter(
user=self.request.user, name='emergency'
).first()
except StaticDevice.DoesNotExist:
static_tokens_device = None
new_tokens = [get_random_string(length=12, allowed_chars='1234567890') for _ in range(10)]
request.session['pretix_2fa_new_emergency_tokens'] = new_tokens
return super().get(request, *args, new_emergency_tokens=new_tokens, static_tokens_device=static_tokens_device, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
notices = [
_('Two-factor authentication has been enabled.')
]
if 'pretix_2fa_new_emergency_tokens' in request.session:
d = StaticDevice.objects.create(user=self.request.user, name='emergency')
for code in request.session['pretix_2fa_new_emergency_tokens']:
d.token_set.create(token=code)
self.request.user.log_action('pretix.user.settings.2fa.regenemergency', user=self.request.user)
notices += [
_('Your two-factor emergency codes have been regenerated.')
]
del request.session['pretix_2fa_new_emergency_tokens']
self.request.user.require_2fa = True self.request.user.require_2fa = True
self.request.user.save() self.request.user.save()
self.request.user.log_action('pretix.user.settings.2fa.enabled', user=self.request.user) self.request.user.log_action('pretix.user.settings.2fa.enabled', user=self.request.user)
messages.success(request, _('Two-factor authentication is now enabled for your account.')) messages.success(request, _('Two-factor authentication is now enabled for your account.'))
self.request.user.send_security_notice([ self.request.user.send_security_notice(notices)
_('Two-factor authentication has been enabled.')
])
self.request.user.update_session_token() self.request.user.update_session_token()
update_session_auth_hash(self.request, self.request.user) update_session_auth_hash(self.request, self.request.user)
return redirect(reverse('control:user.settings.2fa')) return redirect(reverse('control:user.settings.2fa'))
+4 -10
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 import geoip2 from django.contrib.gis.geoip2 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,20 +63,14 @@ def get_user_agent_hash(request):
_geoip = None _geoip = None
def get_geoip() -> geoip2.GeoIP2: def _get_country(request):
# See https://code.djangoproject.com/ticket/36988#ticket
global _geoip global _geoip
geoip2.SUPPORTED_DATABASE_TYPES.add("Geoacumen-Country")
if not _geoip: if not _geoip:
_geoip = geoip2.GeoIP2() _geoip = GeoIP2()
return _geoip
def _get_country(request):
try: try:
res = get_geoip().country(get_client_ip(request)) res = _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,7 +32,6 @@ ausgecheckt
ausgeklappt ausgeklappt
auswahl auswahl
Authentication Authentication
Authenticator
Authenticator-App Authenticator-App
Autorisierungscode Autorisierungscode
Autorisierungs-Endpunktes Autorisierungs-Endpunktes
@@ -131,7 +130,6 @@ Eingangsscan
Einlassbuchung Einlassbuchung
Einlassdatum Einlassdatum
Einlasskontrolle Einlasskontrolle
Einmalpasswörter
einzuchecken einzuchecken
email email
E-Mail-Renderer E-Mail-Renderer
@@ -165,7 +163,6 @@ Explorer
FA FA
Favicon Favicon
F-Droid F-Droid
freeOTP
Footer Footer
Footer-Link Footer-Link
Footer-Text Footer-Text
@@ -560,7 +557,6 @@ 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,7 +32,6 @@ ausgecheckt
ausgeklappt ausgeklappt
auswahl auswahl
Authentication Authentication
Authenticator
Authenticator-App Authenticator-App
Autorisierungscode Autorisierungscode
Autorisierungs-Endpunktes Autorisierungs-Endpunktes
@@ -131,7 +130,6 @@ Eingangsscan
Einlassbuchung Einlassbuchung
Einlassdatum Einlassdatum
Einlasskontrolle Einlasskontrolle
Einmalpasswörter
einzuchecken einzuchecken
email email
E-Mail-Renderer E-Mail-Renderer
@@ -165,7 +163,6 @@ Explorer
FA FA
Favicon Favicon
F-Droid F-Droid
freeOTP
Footer Footer
Footer-Link Footer-Link
Footer-Text Footer-Text
@@ -560,7 +557,6 @@ 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-30 11:25+0000\n" "POT-Creation-Date: 2026-03-17 14:06+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-30 03:00+0000\n" "PO-Revision-Date: 2026-03-18 12:23+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 "Billetes registrados" msgstr "Registro de código QR"
#: 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
+6 -5
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-03-25 14:14+0000\n" "PO-Revision-Date: 2026-02-10 16:49+0000\n"
"Last-Translator: Pietro Isotti <isottipietro@gmail.com>\n" "Last-Translator: Raffaele Doretto <ced@comune.portogruaro.ve.it>\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.16.2\n" "X-Generator: Weblate 5.15.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,8 +310,9 @@ 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 bloccato" msgstr "Biglietto non pagato"
#: 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"
@@ -428,7 +429,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 "Se questa operazione richiede alcuni minuti, si prega di contattarci." msgstr ""
#: 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-03-23 21:00+0000\n" "PO-Revision-Date: 2026-02-23 10: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.2\n" "X-Generator: Weblate 5.16\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 "iDEAL | Wero" msgstr ""
#: 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-03-25 08:00+0000\n" "PO-Revision-Date: 2026-01-26 22: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.16.2\n" "X-Generator: Weblate 5.15.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 "iDEAL | Wero" msgstr ""
#: 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
+3 -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-26 14:29+0000\n" "PO-Revision-Date: 2025-10-10 17:00+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.16.2\n" "X-Generator: Weblate 5.13.3\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,6 +1023,7 @@ 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,7 +12,6 @@ anonymized
Auth Auth
authentification authentification
authenticator authenticator
Authenticator
automatical automatical
availabilities availabilities
backend backend
@@ -23,7 +22,6 @@ barcodes
Bcc Bcc
BCC BCC
BezahlCode BezahlCode
biometric
BLIK BLIK
blocklist blocklist
BN BN
@@ -58,7 +56,6 @@ EPS
eps eps
favicon favicon
filetype filetype
freeOTP
frontend frontend
frontpage frontpage
Galician Galician
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More