Compare commits

..

6 Commits

Author SHA1 Message Date
Raphael Michel
6860e4d21e Delete unused code 2026-03-24 22:00:41 +01:00
Richard Schreiber
4fed47fb9b Fix live_receivers for django 5 2026-03-24 17:14:05 +01:00
Phin Wolkwitz
c143d50290 Update django to 5.2 2026-03-24 16:33:28 +01:00
luelista
88cd715ece Always show Organizers and Events menu entries for staff (#6011) 2026-03-24 11:26:54 +01:00
dependabot[bot]
3513de6a45 Update importlib-metadata requirement from ==8.* to ==9.* (#6016)
Updates the requirements on [importlib-metadata](https://github.com/python/importlib_metadata) to permit the latest version.
- [Release notes](https://github.com/python/importlib_metadata/releases)
- [Changelog](https://github.com/python/importlib_metadata/blob/main/NEWS.rst)
- [Commits](https://github.com/python/importlib_metadata/compare/v8.0.0...v9.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-24 09:09:07 +01:00
Richard Schreiber
fd6d3934c0 Remove invoice_address_from_vat_id on save if it is not used 2026-03-23 14:33:17 +01:00
39 changed files with 215 additions and 307 deletions

View File

@@ -24,7 +24,7 @@ jobs:
name: Packaging
strategy:
matrix:
python-version: ["3.11"]
python-version: ["3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}

View File

@@ -24,10 +24,10 @@ jobs:
name: Check gettext syntax
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: 3.11
python-version: 3.13
- uses: actions/cache@v4
with:
path: ~/.cache/pip
@@ -49,10 +49,10 @@ jobs:
name: Spellcheck
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: 3.11
python-version: 3.13
- uses: actions/cache@v4
with:
path: ~/.cache/pip

View File

@@ -24,10 +24,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: 3.11
python-version: 3.13
- uses: actions/cache@v4
with:
path: ~/.cache/pip
@@ -44,10 +44,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: 3.11
python-version: 3.13
- uses: actions/cache@v4
with:
path: ~/.cache/pip
@@ -64,10 +64,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: 3.11
python-version: 3.13
- name: Install Dependencies
run: pip3 install licenseheaders
- name: Run licenseheaders

View File

@@ -23,13 +23,15 @@ jobs:
name: Tests
strategy:
matrix:
python-version: ["3.10", "3.11", "3.13"]
python-version: ["3.11", "3.13", "3.14"]
database: [sqlite, postgres]
exclude:
- database: sqlite
python-version: "3.10"
- database: sqlite
python-version: "3.11"
- database: sqlite
python-version: "3.12"
services:
postgres:
image: postgres:15
@@ -81,4 +83,4 @@ jobs:
file: src/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
if: matrix.database == 'postgres' && matrix.python-version == '3.13'

View File

@@ -3,7 +3,7 @@ name = "pretix"
dynamic = ["version"]
description = "Reinventing presales, one ticket at a time"
readme = "README.rst"
requires-python = ">=3.10"
requires-python = ">=3.11"
license = {file = "LICENSE"}
keywords = ["tickets", "web", "shop", "ecommerce"]
authors = [
@@ -19,10 +19,11 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Environment :: Web Environment",
"License :: OSI Approved :: GNU Affero General Public License v3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"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 = [
@@ -36,7 +37,7 @@ dependencies = [
"css-inline==0.20.*",
"defusedcsv>=1.1.0",
"dnspython==2.*",
"Django[argon2]==4.2.*,>=4.2.26",
"Django[argon2]==5.2.*",
"django-bootstrap3==26.1",
"django-compressor==4.6.0",
"django-countries==8.2.*",
@@ -59,7 +60,7 @@ dependencies = [
"dnspython==2.8.*",
"drf_ujson2==1.7.*",
"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",
"jsonschema",
"kombu==5.6.*",

View File

@@ -45,7 +45,6 @@ import pycountry
from django import forms
from django.conf import settings
from django.contrib import messages
from django.contrib.gis.geoip2 import GeoIP2
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.validators import (
@@ -102,6 +101,7 @@ from pretix.helpers.countries import (
from pretix.helpers.escapejson import escapejson_attr
from pretix.helpers.http import get_client_ip
from pretix.helpers.i18n import get_format_without_seconds
from pretix.helpers.security import get_geoip
from pretix.presale.signals import question_form_fields
logger = logging.getLogger(__name__)
@@ -393,7 +393,7 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
def guess_country_from_request(request, event):
if settings.HAS_GEOIP:
g = GeoIP2()
g = get_geoip()
try:
res = g.country(get_client_ip(request))
if res['country_code'] and len(res['country_code']) == 2:

View File

@@ -36,8 +36,9 @@ from django.core.management.commands.makemigrations import Command as Parent
from ._migrations import monkeypatch_migrations
monkeypatch_migrations()
class Command(Parent):
pass
def handle(self, *args, **kwargs):
monkeypatch_migrations()
return super().handle(*args, **kwargs)

View File

@@ -64,7 +64,7 @@ class Command(BaseCommand):
if not periodic_task.receivers or periodic_task.sender_receivers_cache.get(self) is NO_RECEIVERS:
return
for receiver in periodic_task._live_receivers(self):
for receiver in periodic_task._live_receivers(self)[0]:
name = f'{receiver.__module__}.{receiver.__name__}'
if options['list_tasks']:
print(name)

View File

@@ -41,16 +41,20 @@ class Migration(migrations.Migration):
name='datetime',
field=models.DateTimeField(),
),
migrations.AlterIndexTogether(
name='logentry',
index_together={('datetime', 'id')},
migrations.AddIndex(
'logentry',
models.Index(fields=('datetime', 'id'), name="pretixbase__datetim_b1fe5a_idx"),
),
migrations.AlterIndexTogether(
name='order',
index_together={('datetime', 'id'), ('last_modified', 'id')},
migrations.AddIndex(
'order',
models.Index(fields=["datetime", "id"], name="pretixbase__datetim_66aff0_idx"),
),
migrations.AlterIndexTogether(
name='transaction',
index_together={('datetime', 'id')},
migrations.AddIndex(
'order',
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"),
),
]

View File

@@ -61,7 +61,10 @@ class Migration(migrations.Migration):
options={
'ordering': ('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),
),

View File

@@ -9,31 +9,6 @@ class Migration(migrations.Migration):
]
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(
model_name="attendeeprofile",
name="id",

View File

@@ -1,6 +1,6 @@
# 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):
@@ -10,8 +10,8 @@ class Migration(migrations.Migration):
]
operations = [
migrations.AlterIndexTogether(
name="reusablemedium",
index_together=set(),
migrations.RemoveIndex(
"reusablemedium",
'reusable_medium_organizer_index',
),
]

View File

@@ -88,7 +88,7 @@ class LogEntry(models.Model):
class Meta:
ordering = ('-datetime', '-id')
indexes = [models.Index(fields=["datetime", "id"])]
indexes = [models.Index(fields=["datetime", "id"], name="pretixbase__datetim_b1fe5a_idx")]
def display(self):
from pretix.base.logentrytype_registry import log_entry_types

View File

@@ -122,7 +122,7 @@ class ReusableMedium(LoggedModel):
class Meta:
unique_together = (("identifier", "type", "organizer"),)
indexes = [
models.Index(fields=("updated", "id")),
models.Index(fields=("updated", "id"), name="pretixbase__updated_093277_idx"),
]
ordering = "identifier", "type", "organizer"

View File

@@ -336,8 +336,8 @@ class Order(LockModel, LoggedModel):
verbose_name_plural = _("Orders")
ordering = ("-datetime", "-pk")
indexes = [
models.Index(fields=["datetime", "id"]),
models.Index(fields=["last_modified", "id"]),
models.Index(fields=["datetime", "id"], name="pretixbase__datetim_66aff0_idx"),
models.Index(fields=["last_modified", "id"], name="pretixbase__last_mo_4ebf8b_idx"),
]
constraints = [
models.UniqueConstraint(fields=["organizer", "code"], name="order_organizer_code_uniq"),
@@ -3080,7 +3080,7 @@ class Transaction(models.Model):
class Meta:
ordering = 'datetime', 'pk'
indexes = [
models.Index(fields=['datetime', 'id'])
models.Index(fields=['datetime', 'id'], name="pretixbase__datetim_b20405_idx")
]
def save(self, *args, **kwargs):

View File

@@ -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')
if payment_term_last and event.presale_end:

View File

@@ -32,6 +32,7 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import logging
import warnings
from typing import Any, Callable, Generic, List, Tuple, TypeVar
@@ -48,6 +49,8 @@ from .plugins import (
PLUGIN_LEVEL_ORGANIZER,
)
logger = logging.getLogger(__name__)
app_cache = {}
T = TypeVar('T')
@@ -60,23 +63,25 @@ def _populate_app_cache():
def get_defining_app(o):
# 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__
if hasattr(o, "__mocked_app"):
return o.__mocked_app
# Find the Django application this belongs to
searchpath = o.__module__
searchpath = module or getattr(o.__class__, "__module__", None) or ""
# 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'
if not app_cache:
_populate_app_cache()
while True:
app = None
while searchpath:
app = app_cache.get(searchpath)
if "." not in searchpath or app:
break
@@ -157,7 +162,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
if not 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):
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
@@ -179,7 +184,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
if not 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):
named[chain_kwarg_name] = response
response = receiver(signal=self, sender=sender, **named)
@@ -204,7 +209,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
if not 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):
try:
response = receiver(signal=self, sender=sender, **named)
@@ -214,17 +219,35 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
responses.append((receiver, response))
return responses
def _sorted_receivers(self, sender):
orig_list = self._live_receivers(sender)
def asend(self, sender: T, **named):
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(
orig_list,
key=lambda receiver: (
0 if any(receiver.__module__.startswith(m) for m in settings.CORE_MODULES) else 1,
receiver.__module__,
receiver.__name__,
0 if _is_core_module(receiver) else 1,
_getattr_fallback_to_class(receiver, "__module__"),
_getattr_fallback_to_class(receiver, "__name__"),
)
)
return sorted_list
return sorted_list, []
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:
return response
for receiver in self._live_receivers(sender):
for receiver in self._live_receivers(sender)[0]:
named[chain_kwarg_name] = response
response = receiver(signal=self, sender=sender, **named)
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):
# 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(
orig_list,
key=lambda receiver: (
0 if any(receiver.__module__.startswith(m) for m in settings.CORE_MODULES) else 1,
receiver.__module__,
receiver.__name__,
0 if _is_core_module(receiver) else 1,
_getattr_fallback_to_class(receiver, "__module__"),
_getattr_fallback_to_class(receiver, "__name__"),
)
)
return sorted_list
return sorted_list, []
class DeprecatedSignal(GlobalSignal):

View File

@@ -63,7 +63,7 @@ from pretix.base.forms import (
from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
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.services.placeholders import FormPlaceholderMixin
from pretix.base.settings import (
@@ -531,6 +531,13 @@ class EventUpdateForm(I18nModelForm):
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):
data = super().clean()
settings_dict = self.obj.settings.freeze()

View File

@@ -363,7 +363,7 @@ def get_global_navigation(request):
'icon': 'dashboard',
},
]
if request.user.is_in_any_teams:
if request.user.is_in_any_teams or request.user.is_staff:
nav += [
{
'label': _('Events'),

View File

@@ -6,38 +6,9 @@
<h1>{% trans "Add a two-factor authentication device" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
{% bootstrap_form_errors form %}
{% bootstrap_field form.name layout='horizontal' %}
<div class="form-group">
<label class="col-md-3 control-label">{% trans "Device type" %}</label>
<div class="col-md-9">
<div class="big-radio radio">
<label>
<input type="radio" value="totp" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "totp" %}checked{% endif %}>
<strong>{% trans "Smartphone with the Authenticator application" %}</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" 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 biometric authentication on iOS, macOS and Android.
{% endblocktrans %}
</div>
</label>
</div>
</div>
</div>
{% bootstrap_field form.devicetype layout='horizontal' %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Continue" %}

View File

@@ -28,6 +28,11 @@
{% trans "iOS (iTunes)" %}
</a>
</li>
<li>
<a href="https://m.google.com/authenticator">
{% trans "Blackberry (Link via Google)" %}
</a>
</li>
</ul>
</li>
<li>
@@ -69,11 +74,14 @@
{% trans "Enter the displayed code here:" %}
<form class="form form-inline" method="post" action="">
{% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
<input type="number" name="token" class="form-control" required="required">
<button class="btn btn-primary" type="submit">
{% trans "Continue" %}
</button><br>
<label>
<input type="checkbox" name="activate" checked="checked" value="on">
{% trans "Require second factor for future logins" %}
</label>
</form>
</li>
</ol>

View File

@@ -12,9 +12,13 @@
</p>
<form class="form form-inline" method="post" action="" id="webauthn-form">
{% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
<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>
</form>

View File

@@ -6,7 +6,6 @@
<h1>{% trans "Delete a two-factor authentication device" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
<p>{% blocktrans trimmed with device=device.name %}
Are you sure you want to delete the authentication device "{{ device }}"?
{% endblocktrans %}</p>

View File

@@ -6,7 +6,6 @@
<h1>{% trans "Disable two-factor authentication" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
<p>
{% trans "Do you really want to disable two-factor authentication?" %}
</p>

View File

@@ -1,58 +1,23 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load icon %}
{% block title %}{% trans "Enable two-factor authentication" %}{% endblock %}
{% block content %}
<h1>{% trans "Enable two-factor authentication" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
<p>
{% trans "Do you really want to enable two-factor authentication?" %}
</p>
<p>
{% 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>
{% 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">
<a href="{% url "control:user.settings.2fa" %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-primary btn-save">
<button type="submit" class="btn btn-danger btn-save">
{% trans "Enable" %}
</button>
</div>

View File

@@ -6,7 +6,6 @@
<h1>{% trans "Leave teams that require two-factor authentication" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
<p>
<strong>{% trans "Do you really want to leave the following teams?" %}</strong>
</p>

View File

@@ -1,6 +1,5 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load icon %}
{% load bootstrap3 %}
{% block title %}{% trans "Two-factor authentication" %}{% endblock %}
{% block content %}
@@ -121,7 +120,7 @@
Delete
</a>
{% if d.devicetype == "totp" %}
<span class="fa fa-mobile fa-lg"></span>
<span class="fa fa-mobile"></span>
{% elif d.devicetype == "webauthn" %}
<span class="fa fa-usb"></span>
{% elif d.devicetype == "u2f" %}
@@ -153,30 +152,19 @@
</p>
{% if static_tokens_device %}
<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>
<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>
{% 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>
{% trans "You don't have any emergency tokens yet." %}
</p>
{% 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>
{% endblock %}

View File

@@ -6,7 +6,6 @@
<h1>{% trans "Regenerate emergency codes" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
<p>
{% trans "Do you really want to regenerate your emergency codes?" %}
</p>

View File

@@ -8,7 +8,6 @@
{% trans "Change login email address" %}
</h1>
{% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
{% bootstrap_form_errors form %}
<p class="text-muted">
{% trans "This changes the email address used to login to your account, as well as where we send email notifications." %}

View File

@@ -9,7 +9,6 @@
</h1>
<br>
{% csrf_token %}
<input type="hidden" name="flow_token" value="{{ flow_token }}">
{% bootstrap_form_errors form %}
{% bootstrap_field form.email %}
{% bootstrap_field form.old_pw %}

View File

@@ -641,7 +641,7 @@ def user_index(request):
ctx = {
'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(
request,
annotated_event_query(request, lazy=True).filter(

View File

@@ -89,31 +89,13 @@ logger = logging.getLogger(__name__)
class RecentAuthenticationRequiredMixin:
max_time = 900
max_form_time = 900
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
auth_is_recent = time.time() - request.session.get('pretix_auth_login_time', 0) < 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:
tdelta = time.time() - request.session.get('pretix_auth_login_time', 0)
if tdelta > self.max_time:
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
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
return super().dispatch(request, *args, **kwargs)
class ReauthView(TemplateView):
@@ -301,7 +283,6 @@ class UserHistoryView(ListView):
class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
max_time = 7200
template_name = 'pretixcontrol/user/2fa_main.html'
def get_context_data(self, **kwargs):
@@ -484,15 +465,25 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
notices = [
_('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.update_session_token()
update_session_auth_hash(self.request, self.request.user)
messages.success(request, str(_('The device has been verified and can now be used.')))
if self.request.user.require_2fa:
return redirect(reverse('control:user.settings.2fa'))
else:
return redirect(reverse('control:user.settings.2fa.enable'))
note = ''
if not self.request.user.require_2fa:
note = ' ' + str(_('Please note that you still need to enable two-factor authentication for your '
'account using the buttons below to make a second factor required for logging '
'into your account.'))
messages.success(request, str(_('The device has been verified and can now be used.')) + note)
return redirect(reverse('control:user.settings.2fa'))
except Exception:
messages.error(request, _('The registration could not be completed. Please try again.'))
logger.exception('WebAuthn registration failed')
@@ -503,7 +494,6 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateView):
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
def device(self):
@@ -524,6 +514,7 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
def post(self, request, *args, **kwargs):
token = request.POST.get('token', '')
activate = request.POST.get('activate', '')
if self.device.verify_token(token):
self.device.confirmed = True
self.device.save()
@@ -535,15 +526,24 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
notices = [
_('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.update_session_token()
update_session_auth_hash(self.request, self.request.user)
messages.success(request, str(_('The device has been verified and can now be used.')))
if self.request.user.require_2fa:
return redirect(reverse('control:user.settings.2fa'))
else:
return redirect(reverse('control:user.settings.2fa.enable'))
note = ''
if not self.request.user.require_2fa:
note = ' ' + str(_('Please note that you still need to enable two-factor authentication for your '
'account using the buttons below to make a second factor required for logging '
'into your account.'))
messages.success(request, str(_('The device has been verified and can now be used.')) + note)
return redirect(reverse('control:user.settings.2fa'))
else:
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.'))
@@ -576,7 +576,6 @@ class User2FALeaveTeamsView(RecentAuthenticationRequiredMixin, TemplateView):
class User2FAEnableView(RecentAuthenticationRequiredMixin, TemplateView):
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):
if not any(dt.objects.filter(user=self.request.user, confirmed=True) for dt in REAL_DEVICE_TYPES):
@@ -585,39 +584,14 @@ class User2FAEnableView(RecentAuthenticationRequiredMixin, TemplateView):
return redirect(reverse('control:user.settings.2fa'))
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):
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.save()
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.'))
self.request.user.send_security_notice(notices)
self.request.user.send_security_notice([
_('Two-factor authentication has been enabled.')
])
self.request.user.update_session_token()
update_session_auth_hash(self.request, self.request.user)
return redirect(reverse('control:user.settings.2fa'))

View File

@@ -25,7 +25,7 @@ import time
from django.conf import settings
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.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -63,14 +63,20 @@ def get_user_agent_hash(request):
_geoip = None
def _get_country(request):
def get_geoip() -> geoip2.GeoIP2:
# See https://code.djangoproject.com/ticket/36988#ticket
global _geoip
if not _geoip:
_geoip = GeoIP2()
geoip2.SUPPORTED_DATABASE_TYPES.add("Geoacumen-Country")
if not _geoip:
_geoip = geoip2.GeoIP2()
return _geoip
def _get_country(request):
try:
res = _geoip.country(get_client_ip(request))
res = get_geoip().country(get_client_ip(request))
except AddressNotFoundError:
return None
return res['country_code']

View File

@@ -32,11 +32,8 @@
# 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.
from itertools import chain
from django import forms
from django.core.exceptions import ValidationError
from django.utils.encoding import force_str
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.safestring import mark_safe
@@ -168,46 +165,6 @@ class QuestionsForm(BaseQuestionsForm):
)
class AddOnRadioSelect(forms.RadioSelect):
option_template_name = 'pretixpresale/forms/addon_choice_option.html'
def optgroups(self, name, value, attrs=None):
attrs = attrs or {}
groups = []
has_selected = False
for index, (option_value, option_label, option_desc) in enumerate(chain(self.choices)):
if option_value is None:
option_value = ''
if isinstance(option_label, (list, tuple)):
raise TypeError('Choice groups are not supported here')
group_name = None
subgroup = []
groups.append((group_name, subgroup, index))
selected = (
force_str(option_value) in value and
(has_selected is False or self.allow_multiple_selected)
)
if selected is True and has_selected is False:
has_selected = True
attrs['description'] = option_desc
subgroup.append(self.create_option(
name, option_value, option_label, selected, index,
subindex=None, attrs=attrs,
))
return groups
class AddOnVariationField(forms.ChoiceField):
def valid_value(self, value):
text_value = force_str(value)
for k, v, d in self.choices:
if value == k or text_value == force_str(k):
return True
return False
class MembershipForm(forms.Form):
required_css_class = 'required'

View File

@@ -576,7 +576,7 @@ def filter_subevents_with_plugins(subevents, sales_channel=None):
if not 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)
event_state = {}

View File

@@ -49,7 +49,7 @@ from django.views.decorators.cache import cache_page
from django.views.decorators.gzip import gzip_page
from django.views.decorators.http import condition
from django.views.i18n import (
JavaScriptCatalog, get_formats, js_catalog_template,
JavaScriptCatalog, builtin_template_path, get_formats,
)
from lxml import html
@@ -170,7 +170,8 @@ def generate_widget_js(version, lang):
'September', 'October', 'November', 'December'
)
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({
'catalog_str': indent(json.dumps(
catalog, sort_keys=True, indent=2)) if catalog else None,

View File

@@ -534,6 +534,7 @@ X_FRAME_OPTIONS = 'DENY'
# URL settings
ROOT_URLCONF = 'pretix.multidomain.maindomain_urlconf'
FORMS_URLFIELD_ASSUME_HTTPS = True # transitional for django 6.0
WSGI_APPLICATION = 'pretix.wsgi.application'

View File

@@ -23,9 +23,7 @@ filterwarnings =
error
ignore:.*invalid escape sequence.*:
ignore:The 'warn' method is deprecated:DeprecationWarning
ignore::django.utils.deprecation.RemovedInDjango51Warning:django.core.files.storage
ignore:.*index_together.*:django.utils.deprecation.RemovedInDjango51Warning:
ignore:.*get_storage_class.*:django.utils.deprecation.RemovedInDjango51Warning:compressor
ignore::django.utils.deprecation.RemovedInDjango60Warning:
ignore:.*This signal will soon be only available for plugins that declare to be organizer-level.*:DeprecationWarning:
ignore::DeprecationWarning:mt940
ignore::DeprecationWarning:cbor2

View File

@@ -19,14 +19,13 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
import aiohttp
import pytest
import pytest_asyncio
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pytz import UTC
from pretix.base.models import (
Device, Event, Item, Organizer, Quota, SeatingPlan,
@@ -53,7 +52,7 @@ def organizer():
def event(organizer):
e = Event.objects.create(
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),
plugins='pretix.plugins.banktransfer,pretix.plugins.ticketoutputpdf',
is_public=True, live=True
@@ -109,8 +108,8 @@ def customer(event, membership_type):
def membership(event, membership_type, customer):
return customer.memberships.create(
membership_type=membership_type,
date_start=datetime(2017, 1, 1, 0, 0, tzinfo=UTC),
date_end=datetime(2099, 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=timezone.utc),
)