Compare commits

..
Author SHA1 Message Date
f74de683e2 Fix typo
Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-04-08 15:56:44 +02:00
Kara Engelhardt a7418a75a5 Remove unneccesary csrf_protect decorators 2026-03-30 10:46:26 +02:00
Kara Engelhardt 31a1e35071 Fix customer views using wrong csrf middleware
This lead to persistent csrf validation errors if the token from the cookie expired, which could only be solved by clearing cookies.
2026-03-26 11:15:03 +01:00
Kara Engelhardt 77eb6eb23b Fix tests 2026-03-25 13:34:46 +01:00
Richard SchreiberandKara Engelhardt 58d6d7f71b fix typo 2026-03-25 12:21:50 +01:00
Richard SchreiberandKara Engelhardt 43917c62a8 fix isort 2026-03-25 12:21:50 +01:00
Richard SchreiberandKara Engelhardt 44e010a207 fix debug typo 2026-03-25 12:21:50 +01:00
Richard SchreiberandKara Engelhardt 38e69d1e32 change to helper delete_cookie_without_samesite 2026-03-25 12:21:50 +01:00
Richard SchreiberandKara Engelhardt 777b504bbe Fix delete_cookie for partitioned legacy CSRF cookie 2026-03-25 12:21:50 +01:00
Raphael MichelandGitHub a71efa6747 Event settings: Workaround for Django 5.2 change (#6025) 2026-03-24 22:00:05 +01:00
Richard Schreiber 4fed47fb9b Fix live_receivers for django 5 2026-03-24 17:14:05 +01:00
Phin WolkwitzandGitHub c143d50290 Update django to 5.2 2026-03-24 16:33:28 +01:00
luelistaandGitHub 88cd715ece Always show Organizers and Events menu entries for staff (#6011) 2026-03-24 11:26:54 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3513de6a45 Update importlib-metadata requirement from ==8.* to ==9.* (#6016)
Updates the requirements on [importlib-metadata](https://github.com/python/importlib_metadata) to permit the latest version.
- [Release notes](https://github.com/python/importlib_metadata/releases)
- [Changelog](https://github.com/python/importlib_metadata/blob/main/NEWS.rst)
- [Commits](https://github.com/python/importlib_metadata/compare/v8.0.0...v9.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-24 09:09:07 +01:00
Richard SchreiberandGitHub fd6d3934c0 Remove invoice_address_from_vat_id on save if it is not used 2026-03-23 14:33:17 +01:00
32 changed files with 239 additions and 169 deletions
+1 -1
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 }}
+4 -4
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
+6 -6
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
+4 -2
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'
+7 -6
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.*",
+2 -2
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:
@@ -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)
@@ -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)
@@ -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"),
),
]
@@ -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),
),
-25
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",
@@ -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',
),
]
+1 -1
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
+1 -1
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"
+3 -3
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):
+8
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:
+60 -19
View File
@@ -32,6 +32,7 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# 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):
+1 -1
View File
@@ -27,11 +27,11 @@ from django.template import TemplateDoesNotExist, loader
from django.template.loader import get_template
from django.utils.functional import Promise
from django.utils.translation import gettext as _
from django.views.decorators.csrf import requires_csrf_token
from sentry_sdk import last_event_id
from pretix.base.i18n import language
from pretix.base.middleware import get_language_from_request
from pretix.multidomain.middlewares import requires_csrf_token
def csrf_failure(request, reason=""):
+16 -2
View File
@@ -34,6 +34,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.
from dataclasses import dataclass
from decimal import Decimal
from urllib.parse import urlencode
from zoneinfo import ZoneInfo
@@ -63,7 +64,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 +532,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()
@@ -571,6 +579,12 @@ class EventSettingsValidationMixin:
del self.cleaned_data[field]
@dataclass
class FontOption:
title: str
data: str
class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, SettingsForm):
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
@@ -722,7 +736,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
del self.fields['event_list_filters']
del self.fields['event_calendar_future_only']
self.fields['primary_font'].choices = [('Open Sans', 'Open Sans')] + sorted([
(a, {"title": a, "data": v}) for a, v in get_fonts(self.event, pdf_support_required=False).items()
(a, FontOption(title=a, data=v)) for a, v in get_fonts(self.event, pdf_support_required=False).items()
], key=lambda a: a[0])
# 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',
},
]
if request.user.is_in_any_teams:
if request.user.is_in_any_teams or request.user.is_staff:
nav += [
{
'label': _('Events'),
@@ -8,45 +8,7 @@
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.name layout='horizontal' %}
<div class="form-group{% if form.devicetype.errors %} has-error{% endif %}">
<label class="col-md-3 control-label">{% trans "Device type" %}</label>
<div class="col-md-9">
<div>
<div class="big-radio radio">
<label>
<input type="radio" required value="totp" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "totp" %}checked{% endif %}>
<strong>{% trans "Smartphone with Authenticator app" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
Use your smartphone with any Time-based One-Time-Password app like freeOTP, Google Authenticator or Proton Authenticator.
{% endblocktrans %}
</div>
</label>
</div>
<div class="big-radio radio">
<label>
<input type="radio" required value="webauthn" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "webauthn" %}checked{% endif %}>
<strong>{% trans "WebAuthn-compatible hardware token" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
Use a hardware token like the Yubikey, or other biometric authentication like fingerprint or face recognition.
{% endblocktrans %}
</div>
</label>
</div>
</div>
{% if form.devicetype.errors %}
<div class="help-block">
{% for error in form.devicetype.errors %}
<p>{{ error|escape }}</p>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% bootstrap_field form.devicetype layout='horizontal' %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Continue" %}
@@ -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>
+1 -1
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(
+5
View File
@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
import re
from datetime import datetime
from django.conf import settings
@@ -48,6 +49,10 @@ def set_cookie_without_samesite(request, response, key, *args, **kwargs):
response.cookies[key]['Partitioned'] = True
def delete_cookie_without_samesite(request, response, key, *args, **kwargs):
kwargs['expires'] = datetime.fromtimestamp(0).strftime("%a, %d %b %Y %H:%M:%S GMT")
set_cookie_without_samesite(request, response, key, *args, **kwargs)
# Based on https://www.chromium.org/updates/same-site/incompatible-clients
# Copyright 2019 Google LLC.
# SPDX-License-Identifier: Apache-2.0
+11 -5
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']
+65 -12
View File
@@ -50,12 +50,15 @@ from django.middleware.csrf import (
from django.shortcuts import render
from django.urls import set_urlconf
from django.utils.cache import patch_vary_headers
from django.utils.decorators import decorator_from_middleware
from django.utils.deprecation import MiddlewareMixin
from django.utils.http import http_date
from django_scopes import scopes_disabled
from pretix.base.models import Event, Organizer
from pretix.helpers.cookies import set_cookie_without_samesite
from pretix.helpers.cookies import (
delete_cookie_without_samesite, set_cookie_without_samesite,
)
from pretix.multidomain.models import KnownDomain
LOCAL_HOST_NAMES = ('testserver', 'localhost')
@@ -176,9 +179,23 @@ class SessionMiddleware(BaseSessionMiddleware):
# The session should be deleted only if the session is entirely empty
is_secure = request.scheme == 'https'
if '__Host-' + settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
response.delete_cookie('__Host-' + settings.SESSION_COOKIE_NAME)
# response.delete_cookie does not work as we might have set a partitioned cookie
delete_cookie_without_samesite(
request, response,
'__Host-' + settings.SESSION_COOKIE_NAME,
path=settings.SESSION_COOKIE_PATH,
secure=is_secure,
httponly=settings.SESSION_COOKIE_HTTPONLY or None
)
elif settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
response.delete_cookie(settings.SESSION_COOKIE_NAME)
# response.delete_cookie does not work as we might have set a partitioned cookie
delete_cookie_without_samesite(
request, response,
settings.SESSION_COOKIE_NAME,
path=settings.SESSION_COOKIE_PATH,
secure=is_secure,
httponly=settings.SESSION_COOKIE_HTTPONLY or None
)
else:
if accessed:
patch_vary_headers(response, ('Cookie',))
@@ -195,15 +212,21 @@ class SessionMiddleware(BaseSessionMiddleware):
if response.status_code != 500:
request.session.save()
if is_secure and settings.SESSION_COOKIE_NAME in request.COOKIES: # remove legacy cookie
response.delete_cookie(settings.SESSION_COOKIE_NAME)
response.delete_cookie(settings.SESSION_COOKIE_NAME, samesite="None")
# response.delete_cookie does not work as we might have set a partitioned cookie
delete_cookie_without_samesite(
request, response,
settings.SESSION_COOKIE_NAME,
path=settings.SESSION_COOKIE_PATH,
secure=is_secure,
httponly=settings.SESSION_COOKIE_HTTPONLY or None
)
set_cookie_without_samesite(
request, response,
'__Host-' + settings.SESSION_COOKIE_NAME if is_secure else settings.SESSION_COOKIE_NAME,
request.session.session_key, max_age=max_age,
expires=expires,
path=settings.SESSION_COOKIE_PATH,
secure=request.scheme == 'https',
secure=is_secure,
httponly=settings.SESSION_COOKIE_HTTPONLY or None
)
return response
@@ -248,20 +271,50 @@ class CsrfViewMiddleware(BaseCsrfMiddleware):
if request.session.get(CSRF_SESSION_KEY) != request.META["CSRF_COOKIE"]:
request.session[CSRF_SESSION_KEY] = request.META["CSRF_COOKIE"]
else:
is_secure = request.scheme == 'https'
# Set the CSRF cookie even if it's already set, so we renew
# the expiry timer.
if is_secure and settings.CSRF_COOKIE_NAME in request.COOKIES: # remove legacy cookie
response.delete_cookie(settings.CSRF_COOKIE_NAME)
response.delete_cookie(settings.CSRF_COOKIE_NAME, samesite="None")
if request.is_secure() and settings.CSRF_COOKIE_NAME in request.COOKIES: # remove legacy cookie
# response.delete_cookie does not work as we might have set a partitioned cookie
delete_cookie_without_samesite(
request, response,
settings.CSRF_COOKIE_NAME,
path=settings.CSRF_COOKIE_PATH,
secure=request.is_secure(),
httponly=settings.CSRF_COOKIE_HTTPONLY
)
set_cookie_without_samesite(
request, response,
'__Host-' + settings.CSRF_COOKIE_NAME if is_secure else settings.CSRF_COOKIE_NAME,
'__Host-' + settings.CSRF_COOKIE_NAME if request.is_secure() else settings.CSRF_COOKIE_NAME,
request.META["CSRF_COOKIE"],
max_age=settings.CSRF_COOKIE_AGE,
path=settings.CSRF_COOKIE_PATH,
secure=is_secure,
secure=request.is_secure(),
httponly=settings.CSRF_COOKIE_HTTPONLY
)
# Content varies with the CSRF cookie, so set the Vary header.
patch_vary_headers(response, ('Cookie',))
def process_response(self, request, response):
if (
not settings.CSRF_USE_SESSIONS
and request.is_secure()
and settings.CSRF_COOKIE_NAME in response.cookies
and response.cookies[settings.CSRF_COOKIE_NAME].value
):
raise ValueError("Usage of djangos CsrfViewMiddleware detected (legacy cookie found in response). "
"This may be caused by using csrf_project or requires_csrf_token from django.views.decorators.csrf. "
"Use the pretix.multidomain.middlewares equivalent instead.")
return super().process_response(request, response)
csrf_protect = decorator_from_middleware(CsrfViewMiddleware)
class _EnsureCsrfToken(CsrfViewMiddleware):
# Behave like CsrfViewMiddleware but don't reject requests or log warnings.
def _reject(self, request, reason):
return None
requires_csrf_token = decorator_from_middleware(_EnsureCsrfToken)
-9
View File
@@ -42,7 +42,6 @@ from django.utils.functional import cached_property
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, ListView, View
@@ -99,7 +98,6 @@ class LoginView(RedirectBackMixin, FormView):
redirect_authenticated_user = True
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
@@ -211,7 +209,6 @@ class RegistrationView(RedirectBackMixin, FormView):
redirect_authenticated_user = True
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
@@ -255,7 +252,6 @@ class SetPasswordView(FormView):
template_name = 'pretixpresale/organizers/customer_setpassword.html'
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
@@ -299,7 +295,6 @@ class ResetPasswordView(FormView):
template_name = 'pretixpresale/organizers/customer_resetpw.html'
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
@@ -523,7 +518,6 @@ class ChangePasswordView(CustomerAccountBaseMixin, FormView):
form_class = ChangePasswordForm
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
@@ -557,7 +551,6 @@ class ChangeInformationView(CustomerAccountBaseMixin, FormView):
form_class = ChangeInfoForm
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
@@ -665,7 +658,6 @@ class SSOLoginView(RedirectBackMixin, View):
redirect_authenticated_user = True
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
@@ -728,7 +720,6 @@ class SSOLoginReturnView(RedirectBackMixin, View):
redirect_authenticated_user = True
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
+1 -1
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 = {}
+3 -2
View File
@@ -49,7 +49,7 @@ from django.views.decorators.cache import cache_page
from django.views.decorators.gzip import gzip_page
from django.views.decorators.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,
+1
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'
+1 -3
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
+4 -5
View File
@@ -19,14 +19,13 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <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),
)