Compare commits

...
Author SHA1 Message Date
Mira Weller bb8e3feb3d Fix formatting 2026-06-23 18:22:56 +02:00
Mira Weller eb5ff1f34a EventMetaProperties: Restrict changing "allowed values" if already in use (Z#23235265) 2026-05-27 13:18:55 +02:00
rashandGitHub 94aec6f511 fixes bug in checkin rules editor where jquery select2 does not get removed correctly from DOM (#6206) 2026-05-26 19:33:31 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ed25a8b073 Update protobuf requirement from ==7.34.* to ==7.35.* (#6197)
Updates the requirements on [protobuf](https://github.com/protocolbuffers/protobuf) to permit the latest version.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: protobuf
  dependency-version: 7.35.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-05-26 11:58:09 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c9eb936d45 Update pyjwt requirement from ==2.12.* to ==2.13.* (#6213)
Updates the requirements on [pyjwt](https://github.com/jpadilla/pyjwt) to permit the latest version.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/compare/2.12.0...2.13.0)

---
updated-dependencies:
- dependency-name: pyjwt
  dependency-version: 2.13.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-05-26 11:57:58 +02:00
luelistaandGitHub 7237ece1ca Remove duplicated csrftoken cookies (#6212)
Due to a Safari bug, in some browser, two csrftoken cookies with different values
exist: one unpartitioned, one partitioned ("CHIPS"). This function generates an
additional Set-Cookie header to get rid of the unpartitioned one.

As Django usually only allows one Set-Cookie header per cookie name, we
need to manually create a cookie 'Morsel' for the deletion and store it
in the HttpResponse's cookie dictionary under a different name, so it is
not overwritten by the actual, correct Set-Cookie header. This works
because the code in django.core.handlers.wsgi/asgi, that generates the
actual Set-Cookie headers, only iterates over cookie.values(), ignoring
the keys.
2026-05-22 17:22:56 +02:00
luelistaandGitHub 18485f5d95 Only show "valid VAT ID" checkmark if VAT ID was entered (Z#23235033) (#6200) 2026-05-22 15:57:21 +02:00
6 changed files with 136 additions and 8 deletions
+2 -2
View File
@@ -74,11 +74,11 @@ dependencies = [
"packaging",
"paypalrestsdk==1.13.*",
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.12.*",
"PyJWT==2.13.*",
"phonenumberslite==9.0.*",
"Pillow==12.2.*",
"pretix-plugin-build",
"protobuf==7.34.*",
"protobuf==7.35.*",
"psycopg2-binary",
"pycountry",
"pycparser==3.0",
@@ -1078,7 +1078,7 @@
<dt>{% trans "VAT ID" %}</dt>
<dd>
{{ order.invoice_address.vat_id }}
{% if order.invoice_address.vat_id_validated %}
{% if order.invoice_address.vat_id and order.invoice_address.vat_id_validated %}
<span class="fa fa-check" data-toggle="tooltip" title="{% blocktrans trimmed %}Valid EU VAT ID{% endblocktrans %}"></span>
{% elif order.invoice_address.vat_id %}
<form class="form-inline helper-display-inline" method="post"
@@ -72,6 +72,9 @@
</div>
<div class="col-sm-6 col-md-4 col-lg-5">
{% bootstrap_field form.key layout='inline' form_group_class="" %}
{% if form.key.help_text %}
<span class="help-block">{{ form.key.help_text|safe }}</span>
{% endif %}
</div>
<div class="col-sm-6 col-md-4 col-lg-5">
{% bootstrap_field form.label layout='inline' form_group_class="" %}
+64 -4
View File
@@ -69,7 +69,7 @@ from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext, gettext_lazy as _
from django.utils.translation import gettext, gettext_lazy as _, ngettext
from django.views import View
from django.views.decorators.http import require_http_methods
from django.views.generic import (
@@ -91,7 +91,9 @@ from pretix.base.models import (
ReusableMedium, ScheduledOrganizerExport, Team, TeamInvite, User,
)
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue
from pretix.base.models.event import (
Event, EventMetaProperty, EventMetaValue, SubEvent, SubEventMetaValue,
)
from pretix.base.models.giftcards import (
GiftCardAcceptance, GiftCardTransaction, gen_giftcard_secret,
)
@@ -2466,11 +2468,51 @@ class EventMetaPropertyEditorMixin:
@cached_property
def formset(self):
return EventMetaPropertyAllowedValueFormSet(
formset = EventMetaPropertyAllowedValueFormSet(
data=self.request.POST if self.request.method == "POST" else None,
organizer=self.request.organizer,
initial=(self.object.choices or []) if self.object else [],
)
if self.event_value_counts or self.subevent_value_counts:
for form in formset.initial_forms:
uses = []
key = form.initial['key']
if key in self.event_value_counts:
count = self.event_value_counts[key]
uses += [ngettext("%d event", "%d events", count) % count]
if key in self.subevent_value_counts:
count = self.subevent_value_counts[key]
uses += [ngettext("%d subevent", "%d subevents", count) % count]
if uses:
form.fields['key'].help_text = _("Value can not be changed because it is in use (%s).") % (", ".join(uses))
form.fields['key'].widget.attrs['readonly'] = True
return formset
@cached_property
def event_value_counts(self):
if self.object:
return {
d['attr_value']: d['count']
for d in self.request.organizer.events.annotate(
attr_value=Subquery(EventMetaValue.objects.filter(
event=OuterRef('pk'),
property__name=self.object.name
).values('value')), count=Count('attr_value')
).values('attr_value', 'count')
}
@cached_property
def subevent_value_counts(self):
if self.object:
return {
d['attr_value']: d['count']
for d in SubEvent.objects.filter(event__organizer=self.request.organizer).annotate(
attr_value=Subquery(SubEventMetaValue.objects.filter(
subevent=OuterRef('pk'),
property__name=self.object.name,
).values('value')), count=Count('attr_value')
).values('attr_value', 'count')
}
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
@@ -2493,10 +2535,28 @@ class EventMetaPropertyEditorMixin:
return False
return True
def all_existing_values_valid(self):
if not self.event_value_counts and not self.subevent_value_counts:
return True
choice_keys = set(
f.cleaned_data.get("key") for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
)
if not choice_keys:
return True
existing_values = (self.event_value_counts.keys() | self.subevent_value_counts.keys()) - {None}
missing_choices = existing_values - choice_keys
if missing_choices:
messages.error(self.request, _(
"When restricting the allowed values, you need to allow all values that already exist "
"on your events. Missing values: %s"
) % (", ".join(missing_choices)))
return False
return True
def post(self, request, *args, **kwargs):
self.object = self.get_object(self.get_queryset())
self.form = self.get_form()
if self.form.is_valid() and self.formset.is_valid() and self.is_default_valid():
if self.form.is_valid() and self.formset.is_valid() and self.is_default_valid() and self.all_existing_values_valid():
return self.form_valid(self.form)
else:
return self.form_invalid(self.form)
+60
View File
@@ -32,7 +32,10 @@
# 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 time
from datetime import datetime
from http.cookies import Morsel
from urllib.parse import urlparse
from django.conf import settings
@@ -58,6 +61,8 @@ from pretix.base.models import Event, Organizer
from pretix.helpers.cookies import set_cookie_without_samesite
from pretix.multidomain.models import KnownDomain
logger = logging.getLogger(__name__)
LOCAL_HOST_NAMES = ('testserver', 'localhost')
@@ -254,6 +259,9 @@ class CsrfViewMiddleware(BaseCsrfMiddleware):
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")
handle_duplicated_csrftoken(request, response)
set_cookie_without_samesite(
request, response,
'__Host-' + settings.CSRF_COOKIE_NAME if is_secure else settings.CSRF_COOKIE_NAME,
@@ -265,3 +273,55 @@ class CsrfViewMiddleware(BaseCsrfMiddleware):
)
# Content varies with the CSRF cookie, so set the Vary header.
patch_vary_headers(response, ('Cookie',))
def handle_duplicated_csrftoken(request, response):
# Due to a Safari bug, in some browser, two csrftoken cookies with different values
# exist: one unpartitioned, one partitioned. This function generates an additional
# Set-Cookie header to get rid of the unpartitioned one.
cookie_name = '__Host-' + settings.CSRF_COOKIE_NAME
if request.scheme == 'https' and cookie_name in request.COOKIES:
values = get_all_values_of_cookie(request.headers.get('Cookie'), cookie_name)
if len(values) > 1:
logger.info('Trying to remove duplicated %s cookies: %r', cookie_name, values)
# Make sure the set_cookie_without_samesite below will add a new item in the dictionary, placing
# it below our deletion header.
response.cookies.pop(cookie_name, None)
# Add the deletion Set-Cookie header to the cookie dict under a wrong name, so it doesn't get
# overwritten by the set_cookie_without_samesite call below. This works because the code in
# django.core.handlers.wsgi/asgi, that generates the actual Set-Cookie headers, only iterates
# over cookie.values(), ignoring the keys.
response.cookies['___DELETECOOKIE___' + cookie_name] = make_delete_morsel(cookie_name)
def get_all_values_of_cookie(cookie_header, cookie_name):
# like django.http.cookie.parse_cookie, but returns all values of duplicated cookies instead of only the last
values = list()
if not cookie_header:
return values
for chunk in cookie_header.split(";"):
if "=" in chunk:
key, val = chunk.split("=", 1)
else:
# Assume an empty name per
# https://bugzilla.mozilla.org/show_bug.cgi?id=169091
key, val = "", chunk
key, val = key.strip(), val.strip()
if key == cookie_name:
values.append(val)
return values
def make_delete_morsel(name):
m = Morsel()
m.set(name, '', '')
m['expires'] = datetime.utcfromtimestamp(0).strftime("%a, %d %b %Y %H:%M:%S GMT")
m['samesite'] = 'None'
m['secure'] = True
m['path'] = settings.CSRF_COOKIE_PATH
m['httponly'] = settings.CSRF_COOKIE_HTTPONLY
return m
@@ -100,14 +100,19 @@ watch(() => props.value, (newval, oldval) => {
}
})
let rawSelectEl: HTMLSelectElement | null = null
onMounted(() => {
rawSelectEl = select.value
build()
})
onUnmounted(() => {
$(select.value)
if (!rawSelectEl) return
$(rawSelectEl)
.off()
.select2('destroy')
rawSelectEl = null
})
</script>
<template lang="pug">