Compare commits

..
Author SHA1 Message Date
Raphael Michel 2bd0a341d6 Bump to 2026.6.1 2026-07-28 12:25:38 +02:00
Raphael Michel 6bec15d12a [SECURITY] Add missing permission check for view (CVE-2026-57532) 2026-07-28 12:25:31 +02:00
186 changed files with 561982 additions and 496781 deletions
-6
View File
@@ -864,9 +864,6 @@ Generating new secrets
Triggers generation of new ``secret`` and ``web_secret`` attributes for both the order and all order positions.
Ticket secrets of order positions that have been used to issue a gift card can not
be changed. Only the link (``web_secret``) will be changed in this case.
**Example request**:
.. sourcecode:: http
@@ -898,9 +895,6 @@ Generating new secrets
Triggers generation of a new ``secret`` and ``web_secret`` attribute for a single order position.
Ticket secrets of order positions that have been used to issue a gift card can not
be changed. Only the link (``web_secret``) will be changed in this case.
**Example request**:
.. sourcecode:: http
+5 -6
View File
@@ -53,8 +53,7 @@ dependencies = [
"django-oauth-toolkit==2.3.*",
"django-otp==1.7.*",
"django-phonenumber-field==8.4.*",
"django-querytagger==0.0.3",
"django-redis==7.0.*",
"django-redis==6.0.*",
"django-scopes==2.0.*",
"django-statici18n==2.7.*",
"djangorestframework==3.17.*",
@@ -77,7 +76,7 @@ dependencies = [
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.13.*",
"phonenumberslite==9.0.*",
"Pillow==12.3.*",
"Pillow==12.2.*",
"pretix-plugin-build",
"protobuf==7.35.*",
"psycopg2-binary",
@@ -92,9 +91,9 @@ dependencies = [
"pyuca",
"qrcode==8.2",
"redis==7.4.*",
"reportlab==5.0.*",
"requests==2.34.*",
"sentry-sdk==2.64.*",
"reportlab==4.5.*",
"requests==2.32.*",
"sentry-sdk==2.63.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
+2 -2
View File
@@ -6,8 +6,8 @@ localecompile:
./manage.py compilemessages
localegen:
./manage.py makemessages --keep-pot --add-location file --ignore "pretix/static/npm_dir/*" $(LNGS)
./manage.py makemessages --keep-pot --add-location file -e js,ts,vue -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
./manage.py makemessages --keep-pot --ignore "pretix/static/npm_dir/*" $(LNGS)
./manage.py makemessages --keep-pot -e js,ts,vue -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
staticfiles: npminstall npmbuild jsi18n
./manage.py collectstatic --noinput
+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
# <https://www.gnu.org/licenses/>.
#
__version__ = "2026.7.0.dev0"
__version__ = "2026.6.1"
-1
View File
@@ -118,7 +118,6 @@ ALL_LANGUAGES = [
('sv', _('Swedish')),
('es', _('Spanish')),
('es-419', _('Spanish (Latin America)')),
('th', _('Thai')),
('tr', _('Turkish')),
('uk', _('Ukrainian')),
]
-1
View File
@@ -747,7 +747,6 @@ class EventSettingsSerializer(SettingsSerializer):
'max_items_per_order',
'reservation_time',
'contact_mail',
'contact_url',
'show_variations_expanded',
'hide_sold_out',
'meta_noindex',
+2 -5
View File
@@ -27,7 +27,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.translation import gettext, gettext_lazy as _
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
@@ -492,9 +492,7 @@ class TeamInviteSerializer(serializers.ModelSerializer):
def _send_invite(self, instance):
mail(
instance.email,
gettext('You\'ve been invited to join %(organizer)s') % {
'organizer': self.context['organizer'].name,
},
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
@@ -576,7 +574,6 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'customer_accounts_require_login_for_order_access',
'invoice_regenerate_allowed',
'contact_mail',
'contact_url',
'imprint_url',
'organizer_info_text',
'event_list_type',
+1 -1
View File
@@ -1398,7 +1398,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
elif self.validate_vat_id and vat_id_applicable:
try:
normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country')))
self.instance.vat_id_validated = bool(normalized_id)
self.instance.vat_id_validated = True
self.instance.vat_id = data['vat_id'] = normalized_id
except VATIDFinalError as e:
if self.all_optional:
+46 -76
View File
@@ -19,8 +19,6 @@
# 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/>.
#
import logging
import re
from collections import OrderedDict
from urllib.parse import urlparse, urlsplit
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
@@ -45,8 +43,6 @@ from pretix.multidomain.urlreverse import (
)
from pretix.presale.style import get_fonts
logger = logging.getLogger(__name__)
_supported = None
@@ -227,26 +223,7 @@ def _parse_csp(header):
return h
VALID_CSP_DIRECTIVES = [
"child-src", "connect-src", "default-src", "fenced-frame-src", "font-src", "form-action", "frame-src", "img-src",
"manifest-src", "media-src", "object-src", "prefetch-src", "report-uri", "script-src", "script-src-elem",
"script-src-attr", "style-src", "style-src-elem", "style-src-attr", "worker-src",
]
CSP_ILLEGAL_CHARS = re.compile(r'[\s,;]')
def _sanitize_csp(h):
for k, v in h.items():
if k not in VALID_CSP_DIRECTIVES:
raise ValueError("Invalid CSP directive " + k)
if any(CSP_ILLEGAL_CHARS.search(el) for el in v):
logger.warning("Stripping invalid component from CSP: %r", h)
h[k] = [el for el in v if not CSP_ILLEGAL_CHARS.search(el)]
def _render_csp(h):
_sanitize_csp(h)
return "; ".join(k + ' ' + ' '.join(v) for k, v in h.items() if v)
@@ -266,7 +243,21 @@ def _merge_csp(a, b):
class SecurityMiddleware(MiddlewareMixin):
CSP_EXEMPT = (
'/api/v1/docs/',
)
def process_response(self, request, resp):
def nested_dict_values(d):
for v in d.values():
if isinstance(v, dict):
yield from nested_dict_values(v)
else:
if isinstance(v, str):
yield v
url = resolve(request.path_info)
if settings.DEBUG and resp.status_code >= 400:
# Don't use CSP on debug error page as it breaks of Django's fancy error
# pages
@@ -277,15 +268,18 @@ class SecurityMiddleware(MiddlewareMixin):
# https://github.com/pretix/pretix/issues/765
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
if not getattr(resp, '_csp_ignore', False):
resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp))
elif 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']
img_src = []
gs = global_settings_object(request)
if gs.settings.leaflet_tiles:
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
return resp
def _build_csp(self, request, resp):
url = resolve(request.path_info)
font_src = set()
if hasattr(request, 'event'):
for font in get_fonts(request.event, pdf_support_required=False).values():
for path in list(nested_dict_values(font)):
font_location = urlparse(path)
if font_location.scheme and font_location.netloc:
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
h = {
'default-src': ["{static}"],
@@ -294,8 +288,8 @@ class SecurityMiddleware(MiddlewareMixin):
'frame-src': ['{static}'],
'style-src': ["{static}", "{media}"],
'connect-src': ["{dynamic}", "{media}"],
'img-src': ["{static}", "{media}", "data:"],
'font-src': ["{static}"],
'img-src': ["{static}", "{media}", "data:"] + img_src,
'font-src': ["{static}"] + list(font_src),
'media-src': ["{static}", "data:"],
# form-action is not only used to match on form actions, but also on URLs
# form-actions redirect to. In the context of e.g. payment providers or
@@ -304,13 +298,6 @@ class SecurityMiddleware(MiddlewareMixin):
'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []),
}
gs = global_settings_object(request)
if gs.settings.leaflet_tiles:
h['img-src'].append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
if hasattr(request, 'event'):
h['font-src'] += list(self._get_font_origins(request.event))
if settings.VITE_DEV_MODE:
h['script-src'] += ["http://localhost:5173", "ws://localhost:5173"]
h['style-src'] += ["'unsafe-inline'"]
@@ -322,7 +309,6 @@ class SecurityMiddleware(MiddlewareMixin):
if not settings.VITE_DEV_MODE:
# can't have 'unsafe-inline' and nonce at the same time
h['style-src'].append(nonce)
# Only include pay.google.com for wallet detection purposes on the Payment selection page
if (
url.url_name == "event.order.pay.change" or
@@ -331,32 +317,27 @@ class SecurityMiddleware(MiddlewareMixin):
h['script-src'].append('https://pay.google.com')
h['frame-src'].append('https://pay.google.com')
h['connect-src'].append('https://google.com/pay')
if settings.LOG_CSP:
h['report-uri'] = ["/csp_report/"]
if 'Content-Security-Policy' in resp:
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
if settings.CSP_ADDITIONAL_HEADER:
_merge_csp(h, _parse_csp(settings.CSP_ADDITIONAL_HEADER))
placeholders = {
"{static}": ["'self'"],
"{dynamic}": ["'self'"],
"{media}": ["'self'"],
}
staticdomain = "'self'"
dynamicdomain = "'self'"
mediadomain = "'self'"
if settings.MEDIA_URL.startswith('http'):
placeholders["{media}"].append(settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)])
mediadomain += " " + settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)]
if settings.STATIC_URL.startswith('http'):
placeholders["{static}"].append(settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)])
staticdomain += " " + settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)]
if settings.SITE_URL.startswith('http'):
if settings.SITE_URL.find('/', 9) > 0:
placeholders["{static}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
placeholders["{dynamic}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
staticdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)]
dynamicdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)]
else:
placeholders["{static}"].append(settings.SITE_URL)
placeholders["{dynamic}"].append(settings.SITE_URL)
staticdomain += " " + settings.SITE_URL
dynamicdomain += " " + settings.SITE_URL
if hasattr(request, 'organizer') and request.organizer:
if hasattr(request, 'event') and request.event:
@@ -367,29 +348,18 @@ class SecurityMiddleware(MiddlewareMixin):
siteurlsplit = urlsplit(settings.SITE_URL)
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
domain = '%s:%d' % (domain, siteurlsplit.port)
placeholders["{dynamic}"].append(domain)
dynamicdomain += " " + domain
for k, v in h.items():
h[k] = sorted(set(result for part in v for result in placeholders.get(part, [part])))
if request.path not in self.CSP_EXEMPT and not getattr(resp, '_csp_ignore', False):
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain,
media=mediadomain)
for k, v in h.items():
h[k] = sorted(set(' '.join(v).format(static=staticdomain, dynamic=dynamicdomain, media=mediadomain).split(' ')))
resp['Content-Security-Policy'] = _render_csp(h)
elif 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']
return h
def _get_font_origins(self, event):
def nested_dict_values(d):
for v in d.values():
if isinstance(v, dict):
yield from nested_dict_values(v)
else:
if isinstance(v, str):
yield v
font_src = set()
for font in get_fonts(event, pdf_support_required=False).values():
for path in list(nested_dict_values(font)):
font_location = urlparse(path)
if font_location.scheme and font_location.netloc:
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
return font_src
return resp
class RejectInvalidInputMiddleware(MiddlewareMixin):
+14 -14
View File
@@ -373,7 +373,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
mail(
email or self.email,
_('Changes to your account'),
_('Account information changed'),
'pretixcontrol/email/security_notice.txt',
{
'user': self,
@@ -400,13 +400,12 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
with language(self.locale):
if reason == 'email_change':
msg = str(_('To change your email address from {old_email} to {new_email}, use the following code:').format(
msg = str(_('to confirm changing your email address from {old_email}\nto {new_email}, use the following code:').format(
old_email=self.email, new_email=email,
))
elif reason == 'email_verify':
msg = str(_('To verify your email address {email} on {instance}, use the following code:').format(
msg = str(_('to confirm that your email address {email} belongs to your pretix account, use the following code:').format(
email=self.email,
instance=settings.PRETIX_INSTANCE_NAME,
))
else:
raise Exception('Invalid confirmation code reason')
@@ -419,7 +418,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
}
mail(
email or self.email,
_('Your confirmation code'),
_('pretix confirmation code'),
'pretixcontrol/email/confirmation_code.txt',
{
'user': self,
@@ -463,9 +462,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
from pretix.base.services.mail import mail
mail(
self.email,
_('Reset your password'),
'pretixcontrol/email/forgot.txt',
self.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
@@ -650,22 +647,25 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
id__in=self.teams.filter(TeamQuerySet.organizer_permission_q(permission)).values_list('organizer', flat=True)
)
def has_active_staff_session(self, session_key):
def has_active_staff_session(self, session_key=None):
"""
Returns whether or not a user has an active staff session (formerly known as superuser session)
with the given session key.
"""
return self.get_active_staff_session(session_key) is not None
def get_active_staff_session(self, session_key):
if not self.is_staff or not session_key:
def get_active_staff_session(self, session_key=None):
if not self.is_staff:
return None
if not hasattr(self, '_staff_session_cache'):
self._staff_session_cache = {}
if session_key not in self._staff_session_cache:
sess = StaffSession.objects.filter(
user=self, date_end__isnull=True, session_key=session_key
).first()
qs = StaffSession.objects.filter(
user=self, date_end__isnull=True
)
if session_key:
qs = qs.filter(session_key=session_key)
sess = qs.first()
if sess:
if sess.date_start < now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE):
sess.date_end = now()
+1 -1
View File
@@ -834,7 +834,7 @@ class BasePaymentProvider:
"""
raise NotImplementedError() # NOQA
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str | None:
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
"""
After the user has confirmed their purchase, this method will be called to complete
the payment process. This is the place to actually move the money if applicable.
+5 -10
View File
@@ -77,7 +77,6 @@ from reportlab.platypus import Paragraph
from pretix.base.i18n import language
from pretix.base.models import Checkin, Event, Order, OrderPosition, Question
from pretix.base.services.placeholders import PlaceholderContext
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.templatetags.money import money_filter
@@ -402,7 +401,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Event organizer info text"),
"evaluate": lambda op, order, ev: str(order.event.settings.organizer_info_text)
}),
("event_info_text", {}), # Placeholder to "reserve" position, defined later in `get_variables`
("event_info_text", {
"label": _("Event info text"),
"editor_sample": _("Event info text"),
"evaluate": lambda op, order, ev: str(order.event.settings.event_info_text)
}),
("now_date", {
"label": _("Printing date"),
"editor_sample": _("2017-05-31"),
@@ -667,14 +670,6 @@ def get_images(event):
def get_variables(event):
v = copy.copy(DEFAULT_VARIABLES)
templating_context = PlaceholderContext(event=event)
v['event_info_text'] = {
"label": _("Event info text"),
"editor_sample": _("Event info text"),
"evaluate": lambda op, order, ev:
templating_context.format(str(order.event.settings.event_info_text))
}
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
concatenation_for_salutation = scheme.get("concatenation_for_salutation", scheme["concatenation"])
-3
View File
@@ -245,9 +245,6 @@ def recv_classic(sender, **kwargs):
def assign_ticket_secret(event, position, force_invalidate_if_revokation_list_used=False, force_invalidate=False, save=True):
if position.pk and position.issued_gift_cards.exists():
return
gen = event.ticket_secret_generator
if gen.use_revocation_list and force_invalidate_if_revokation_list_used:
force_invalidate = True
+1 -3
View File
@@ -22,7 +22,6 @@
import logging
from decimal import Decimal
from django.conf import settings
from django.db import transaction
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Subquery
from django.utils.crypto import get_random_string
@@ -378,13 +377,12 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
confirmation_code = get_random_string(8, allowed_chars="01234567890")
mail(
user.email,
subject=gettext('Confirm event cancellation and bulk refund'),
subject=gettext('Bulk-refund confirmation'),
template='pretixbase/email/cancel_confirm.txt',
context={
"event": str(event),
"amount": money_filter(refund_total, event.currency),
"confirmation_code": confirmation_code,
"instance": settings.PRETIX_INSTANCE_NAME,
},
locale=user.locale,
)
+1 -2
View File
@@ -340,13 +340,12 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
if schedule.owner.is_active:
mail(
email=schedule.owner.email,
subject=gettext('Scheduled export failed'),
subject=gettext('Export failed'),
template='pretixbase/email/export_failed.txt',
context={
'configuration_url': config_url,
'reason': msg,
'soft': soft,
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=context if isinstance(context, Event) else None,
organizer=context.organizer if isinstance(context, Event) else context,
-4
View File
@@ -1599,7 +1599,6 @@ class OrderChangeManager:
'seat_forbidden': gettext_lazy('The selected product does not allow to select a seat.'),
'tax_rule_country_blocked': gettext_lazy('The selected country is blocked by your tax rule.'),
'gift_card_change': gettext_lazy('You cannot change the price of a position that has been used to issue a gift card.'),
'gift_card_secret': gettext_lazy('You cannot change the ticket secret of a position that has been used to issue a gift card.'),
'max_items_per_product': ngettext_lazy(
"You cannot select more than %(max)s item of the product %(product)s.",
"You cannot select more than %(max)s items of the product %(product)s.",
@@ -1757,9 +1756,6 @@ class OrderChangeManager:
self._operations.append(self.RegenerateSecretOperation(position))
def change_ticket_secret(self, position: OrderPosition, new_secret: str):
if position.issued_gift_cards.exists():
raise OrderError(self.error_messages['gift_card_secret'])
self._operations.append(self.ChangeSecretOperation(position, new_secret))
def change_valid_from(self, position: OrderPosition, new_value: datetime):
+3 -4
View File
@@ -44,7 +44,7 @@ from django.conf import settings
from django.utils.crypto import get_random_string
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _
from django.utils.translation import gettext_lazy as _
from pretix.base.i18n import language
from pretix.base.models import CachedFile, Event, User, cachedfile_name
@@ -171,16 +171,15 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo
if user:
with language(user.locale):
event_name = str(event.name)
mail(
user.email,
gettext('Data shredding completed for %(event)s') % {'event': event_name},
_('Data shredding completed'),
'pretixbase/email/shred_completed.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': user,
'organizer': event.organizer.name,
'event': event_name,
'event': str(event.name),
'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'),
'shredders': ', '.join([str(s.verbose_name) for s in shredders])
},
+3 -12
View File
@@ -1276,7 +1276,7 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict(
label=_("Allow updating existing invoices"),
label=_("Allow to update existing invoices"),
help_text=_("By default, invoices can never again be changed once they are issued. In most countries, we "
"recommend to leave this option turned off and always issue a new invoice if a change needs "
"to be made."),
@@ -1924,6 +1924,8 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Hide all past dates from calendar"),
help_text=_("This option currently only affects the calendar of this event series, not the organizer-wide "
"calendar.")
)
},
'allow_modifications': {
@@ -2284,17 +2286,6 @@ DEFAULTS = {
help_text=_("We'll show this publicly to allow attendees to contact you.")
)
},
'contact_url': {
'default': None,
'type': str,
'serializer_class': serializers.URLField,
'form_class': forms.URLField,
'form_kwargs': dict(
label=_("Contact URL"),
help_text=_("If you set this, the footer contact link will point here instead of using the email address above. "
"Please note that you still need to add a contact email address that will be shared with all emails you send.")
)
},
'imprint_url': {
'default': None,
'type': str,
@@ -1,14 +1,10 @@
{% load i18n %}
{% trans "You requested to cancel an event that involves a large bulk refund:" %}
{% trans "You have requested us to cancel an event which includes a larger bulk-refund:" %}
- {% trans "Event" %}: {{ event }}
- {% trans "Estimated refund" %}: **{{ amount }}**
{% trans "Event" %}: {{ event }}
{% trans "To confirm, paste the following code into the cancellation form:" %}
{% trans "Estimated refund amount" %}: **{{ amount }}**
{{ confirmation_code }}
{% trans "Please confirm that you want to proceed by coping the following confirmation code into the cancellation form:" %}
{% blocktrans with instance=instance %}Don't share this code with anyone. The {{ instance }} team will never ask you for it.{% endblocktrans %}
{% blocktrans with instance=instance %}Thanks,
The {{ instance }} Team{% endblocktrans %}
**{{ confirmation_code }}**
@@ -1,13 +1,12 @@
{% load i18n %}
{% trans "Your scheduled export failed." %}
{% trans "Your export failed." %}
- {% trans "Reason" %}: {{ reason }}
{% trans "Reason:" %} {{ reason }}
{% if not soft %}{% trans "If an export fails five times in a row, we'll stop sending it." %}{% endif %}
{% if not soft %}
{% trans "If your export fails five times in a row, it will no longer be sent." %}
{% endif %}
{% trans "You can adjust or remove this export here:" %}
{% trans "Configuration link:" %}
{{ configuration_url }}
{% blocktrans with instance=instance %}Thanks,
The {{ instance }} Team{% endblocktrans %}
@@ -52,13 +52,13 @@
<table cellpadding="20"><tr><td>
<![endif]-->
<div class="content">
{% trans "You're receiving this email based on your notification settings." %}<br>
{% trans "You receive these emails based on your notification settings." %}<br>
<a href="{{ settings_url }}">
{% trans "Manage settings" %}
{% trans "Click here to view and change your notification settings" %}
</a>
{% if disable_url %}<br>
<a href="{{ disable_url }}">
{% trans "Disable all notifications" %}
{% trans "Click here disable all notifications immediately." %}
</a>
{% endif %}
</div>
@@ -1,21 +1,19 @@
{% load i18n %}
{{ notification.title }}{% if notification.detail %}
{{ notification.detail }}{% endif %}{% if notification.url %}
{{ notification.detail }}
{% endif %}{% if notification.url %}
{{ notification.url }}{% endif %}{% if notification.attributes %}
{{ notification.url }}{% endif %}{% for attr in notification.attributes %}
{% for attr in notification.attributes %}- {{ attr.title }}: {{ attr.value }}
{% endfor %}{% endif %}{% for action in notification.actions %}
{{ attr.title }}: {{ attr.value }}{% endfor %}{% for action in notification.actions %}
{{ action.label }}:
{{ action.label }}
{{ action.url }}{% endfor %}
{{ action.url }}{% endfor %}
---
{% trans "You're receiving this email based on your notification settings." %}
- {% trans "Manage settings" %}: {{ settings_url }}
{% if disable_url %}- {% trans "Disable all notifications" %}: {{ disable_url }}
{% trans "You receive these emails based on your notification settings." %}
{% trans "Click here to view and change your notification settings:" %}
{{ settings_url }}
{% if disable_url %}{% trans "Click here disable all notifications immediately:" %}
{{ disable_url }}
{% endif %}
@@ -1,14 +1,17 @@
{% load i18n %}{% blocktrans %}Hello,
{% load i18n %}
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
The following data shredding job has been completed:
we hereby confirm that the following data shredding job has been completed:
- Organizer: {{ organizer }}
- Event: {{ event }}
- Data selection: {{ shredders }}
- Start time: {{ start_time }}
Organizer: {{ organizer }}
Any data added to the event after the start time may not have been deleted.
Event: {{ event }}
Thanks,
The {{ instance }} Team
Data selection: {{ shredders }}
Start time: {{ start_time }} (new data added after this time might not have been deleted)
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -42,6 +42,8 @@ from bleach import DEFAULT_CALLBACKS, html5lib_shim
from bleach.linkifier import build_email_re
from django import template
from django.conf import settings
from django.core import signing
from django.urls import reverse
from django.utils.functional import SimpleLazyObject
from django.utils.html import escape
from django.utils.http import url_has_allowed_host_and_scheme
-12
View File
@@ -625,7 +625,6 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
'max_items_per_order',
'reservation_time',
'contact_mail',
'contact_url',
'show_variations_expanded',
'hide_sold_out',
'meta_noindex',
@@ -672,11 +671,6 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
base_context = {
'frontpage_text': ['event'],
'presale_has_ended_text': ['event'],
'voucher_explanation_text': ['event'],
'banner_text': ['event'],
'banner_text_bottom': ['event'],
'event_info_text': ['event'],
}
def _resolve_virtual_keys_input(self, data, prefix=''):
@@ -1905,12 +1899,6 @@ class QuickSetupForm(I18nForm):
required=False,
help_text=_("We'll show this publicly to allow attendees to contact you.")
)
contact_url = forms.URLField(
label=_("Contact URL"),
required=False,
help_text=_("If you set this, the footer contact link will point here instead of using the email address above. "
"Please note that you still need to add a contact email address that will be shared with all emails you send.")
)
total_quota = forms.IntegerField(
label=_("Total capacity"),
min_value=0,
-1
View File
@@ -604,7 +604,6 @@ class OrganizerSettingsForm(SettingsForm):
'customer_accounts_require_login_for_order_access',
'invoice_regenerate_allowed',
'contact_mail',
'contact_url',
'imprint_url',
'organizer_info_text',
'event_list_type',
-5
View File
@@ -106,11 +106,6 @@ class VoucherForm(I18nModelForm):
pass
super().__init__(*args, **kwargs)
self.fields['tag'].widget.attrs['data-typeahead-url'] = reverse('control:event.vouchers.tags.typeahead', kwargs={
'event': instance.event.slug,
'organizer': instance.event.organizer.slug,
})
if instance.event.has_subevents:
self.fields['subevent'].queryset = instance.event.subevents.all()
self.fields['subevent'].widget = Select2(
+6 -10
View File
@@ -36,7 +36,6 @@ from urllib.parse import quote, urljoin, urlparse
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
from django.contrib.auth.views import redirect_to_login
from django.http import Http404
from django.shortcuts import get_object_or_404, resolve_url
from django.template.response import TemplateResponse
@@ -215,15 +214,12 @@ class AuditLogMiddleware:
hijack_history = request.session.get('hijack_history', False)
hijacker = get_object_or_404(User, pk=hijack_history[0]["user"])
ss = hijacker.get_active_staff_session(request.session.get('hijacker_session'))
if not ss:
# Staff session expired or not found
logout(request)
return redirect_to_login(request.get_full_path())
ss.logs.create(
url=request.path,
method=request.method,
impersonating=request.user
)
if ss:
ss.logs.create(
url=request.path,
method=request.method,
impersonating=request.user
)
else:
ss = request.user.get_active_staff_session(request.session.session_key)
if ss:
@@ -1,13 +1,13 @@
{% load i18n %}{% blocktrans with code=code reason=reason instance=instance %}Hello,
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
{{ reason }}
{{ code }}
Don't share this code with anyone. The {{ instance }} team will never ask you for it.
Please do never give this code to another person. Our support team will never ask for this code.
If you didn't request this code, please contact us immediately.
If this code was not requested by you, please contact us immediately.
Thanks,
The {{ instance }} Team
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -1,15 +1,14 @@
{% load i18n %}{% blocktrans with code=code address=address instance=instance %}Hello,
{% load i18n %}{% blocktrans with code=code instance=instance %}Hello,
Someone requested to use {{ address }} as a sender address on {{ instance }}. Once verified, emails sent from {{ instance }} can show this address as the sender.
someone requested to use {{ address }} as a sender address on {{ instance }}.
This will allow them to send emails that are shown to originate from this email address.
If that was you, please enter the following confirmation code:
If this was you, enter the following code in the setup form:
{{ code }}
{{ code }}
If this was not requested by you, you can safely ignore this email.
Don't share this code with anyone. The {{ instance }} team will never ask you for it.
Best regards,
If you didn't request this, you can safely ignore this email.
Thanks,
The {{ instance }} Team
{% endblocktrans %}
Your {{ instance }} team
{% endblocktrans %}
@@ -1,11 +1,9 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
We received a request to reset the password for your {{ instance }} account. To choose a new password, follow the link below:
you requested a new password. Please go to the following page to reset your password:
{{ url }}
If you didn't request this, you can safely ignore this email — your password won't change.
Thanks,
The {{ instance }} Team
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -1,16 +1,17 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
You've been invited to join a team on {{ instance }}, an event ticket sales platform.
you have been invited to a team on {{ instance }}, a platform to perform event
ticket sales.
- Organizer: {{ organizer }}
- Team: {{ team }}
To accept, follow the link below:
Organizer: {{ organizer }}
Team: {{ team }}
If you want to join that team, just click on the following link:
{{ url }}
If you don't want to join, you can safely ignore this email.
If you do not want to join, you can safely ignore or delete this email.
Thanks,
The {{ instance }} Team
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -1,19 +1,13 @@
{% load i18n %}{% blocktrans %}Hello,
{% load i18n %}{% blocktrans with url=url|safe os=source.os_type agent=source.agent_type %}Hello,
We noticed a new sign-in to your {{ instance }} account:
{% endblocktrans %}
- {% trans "Time" %}: {{ when }}
- {% trans "Browser" %}: {{ agent }}
- {% trans "Operating system" %}: {{ os }}
{% if device %}- {% trans "Device" %}: {{ device }}
{% endif %}{% if country %}- {% trans "Country" %}: {{ country }}
{% endif %}
{% blocktrans with url=url|safe %}If it was you, no action is needed.
a login to your {{ instance }} account from an unusual or new location was detected. The login was performed using {{ agent }} on {{ os }} from {{ country }}.
If you don't recognise this sign-in, please change your password immediately:
If this was you, you can safely ignore this email.
If this was not you, we recommend that you change your password in your account settings:
{{ url }}
Thanks,
The {{ instance }} Team
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -1,15 +1,16 @@
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
The following changes were made to your {{ instance }} account:
this is to inform you that the account information of your {{ instance }} account has been
changed. In particular, the following changes have been performed:
{{ messages }}
If you didn't make these changes, please contact the {{ instance }} support team immediately.
If this change was not performed by you, please contact us immediately.
You can review your account settings here:
You can review and change your account settings here:
{{ url }}
Thanks,
The {{ instance }} Team
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -193,7 +193,6 @@
{% endblocktrans %}
</p>
{% bootstrap_field form.contact_mail layout="control" %}
{% bootstrap_field form.contact_url layout="control" %}
{% bootstrap_field form.imprint_url layout="control" %}
</div>
</fieldset>
@@ -30,7 +30,6 @@
{% bootstrap_field form.date_admission layout="control" %}
{% bootstrap_field form.currency layout="control" %}
{% bootstrap_field sform.contact_mail layout="control" %}
{% bootstrap_field sform.contact_url layout="control" %}
{% bootstrap_field sform.imprint_url layout="control" %}
{% bootstrap_field form.is_public layout="control" %}
{% bootstrap_field form.all_sales_channels layout="control" %}
@@ -284,14 +284,6 @@
</div>
<div class="col-sm-4">
{% bootstrap_field position.form.operation_secret layout='inline' %}
{% if position.issued_gift_cards.exists %}
<div class="alert alert-info">
{% blocktrans trimmed %}
Ticket secrets of order positions that have been used to issue a gift card can not
be changed. Only the link will be changed in this case.
{% endblocktrans %}
</div>
{% endif %}
</div>
</div>
@@ -34,7 +34,6 @@
{% endif %}
{% bootstrap_field sform.imprint_url layout="control" %}
{% bootstrap_field sform.contact_mail layout="control" %}
{% bootstrap_field sform.contact_url layout="control" %}
{% bootstrap_field sform.organizer_info_text layout="control" %}
{% bootstrap_field sform.event_team_provisioning layout="control" %}
{% if sform.allowed_restricted_plugins %}
-1
View File
@@ -370,7 +370,6 @@ urlpatterns = [
re_path(r'^discounts/add$', discounts.DiscountCreate.as_view(), name='event.items.discounts.add'),
re_path(r'^vouchers/$', vouchers.VoucherList.as_view(), name='event.vouchers'),
re_path(r'^vouchers/tags/$', vouchers.VoucherTags.as_view(), name='event.vouchers.tags'),
re_path(r'^vouchers/tags/typeahead$', typeahead.voucher_tag_typeahead, name='event.vouchers.tags.typeahead'),
re_path(r'^vouchers/rng$', vouchers.VoucherRNG.as_view(), name='event.vouchers.rng'),
re_path(r'^vouchers/item_select$', typeahead.itemvarquota_select2, name='event.vouchers.itemselect2'),
re_path(r'^vouchers/(?P<voucher>\d+)/$', vouchers.VoucherUpdate.as_view(), name='event.voucher'),
+1 -1
View File
@@ -1558,7 +1558,7 @@ class WidgetSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormV
return ctx
class QuickSetupView(FormView):
class QuickSetupView(EventPermissionRequiredMixin, FormView):
template_name = 'pretixcontrol/event/quick_setup.html'
permission = 'event.settings.general:write'
form_class = QuickSetupForm
+4 -5
View File
@@ -28,7 +28,7 @@ from django.core.mail import get_connection
from django.shortcuts import redirect
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import gettext, gettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView
from pretix.base import email
@@ -216,14 +216,13 @@ class MailSettingsSetupView(TemplateView):
messages.error(request, _('The verification code was incorrect, please try again.'))
else:
self.request.session[session_key] = get_random_string(length=6, allowed_chars='1234567890')
sender_address = self.simple_form.cleaned_data.get('mail_from')
mail(
sender_address,
gettext('Confirm %(address)s as a sender address') % {'address': sender_address},
self.simple_form.cleaned_data.get('mail_from'),
_('Sender address verification'),
'pretixcontrol/email/email_setup.txt',
{
'code': self.request.session[session_key],
'address': sender_address,
'address': self.simple_form.cleaned_data.get('mail_from'),
'instance': settings.PRETIX_INSTANCE_NAME,
},
None,
+3 -9
View File
@@ -64,7 +64,7 @@ from django.urls import reverse
from django.utils import formats
from django.utils.formats import date_format, get_format
from django.utils.functional import cached_property
from django.utils.html import conditional_escape, escape, format_html
from django.utils.html import conditional_escape, escape
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe
from django.utils.timezone import make_aware, now
@@ -79,7 +79,7 @@ from pretix.base.email import get_email_context
from pretix.base.exporter import MultiSheetListExporter
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, CachedTicket, Checkin, GiftCard, Invoice, InvoiceAddress, Item,
CachedFile, CachedTicket, Checkin, Invoice, InvoiceAddress, Item,
ItemVariation, LogEntry, Order, QuestionAnswer, Quota,
ScheduledEventExport, generate_secret,
)
@@ -1994,7 +1994,7 @@ class OrderChange(OrderView):
positions = list(self.order.positions.select_related(
'item', 'item__tax_rule', 'used_membership', 'used_membership__membership_type', 'tax_rule',
'seat', 'subevent',
).prefetch_related('granted_memberships', 'issued_gift_cards'))
).prefetch_related('granted_memberships'))
for p in positions:
p.form = OrderPositionChangeForm(prefix='op-{}'.format(p.pk), instance=p, items=self.items,
initial={'seat': p.seat.seat_guid if p.seat else None},
@@ -2261,12 +2261,6 @@ class OrderContactChange(OrderView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['form'] = self.form
if self.order.all_positions.filter(Exists(GiftCard.objects.filter(issued_in=OuterRef('pk')))).exists():
self.form.fields['regenerate_secrets'].help_text = format_html(
'{}<br><br><strong><span class="fa fa-warning"></span> {}</strong>',
self.form.fields['regenerate_secrets'].help_text,
_("Ticket secrets of order positions that have been used to issue a gift card can not be changed. Only the link will be changed in this case."),
)
return ctx
@cached_property
+1 -3
View File
@@ -1032,9 +1032,7 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
def _send_invite(self, instance):
mail(
instance.email,
gettext('You\'ve been invited to join %(organizer)s') % {
'organizer': self.request.organizer.name,
},
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
-15
View File
@@ -975,21 +975,6 @@ def subevent_meta_values(request, organizer, event):
})
@event_permission_required('event.vouchers:read')
def voucher_tag_typeahead(request, **kwargs):
q = request.GET.get('q', '')
tags = request.event.vouchers.filter(
tag__isnull=False,
waitinglistentries__isnull=True,
).filter(
tag__icontains=q,
).values_list('tag', flat=True).distinct().order_by('tag')[:10]
return JsonResponse({
'results': [{'name': t} for t in tags]
})
def item_meta_values(request, organizer, event):
q = request.GET.get('q')
propname = request.GET.get('property')
+11 -27
View File
@@ -31,7 +31,6 @@ from django.contrib.auth import (
)
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import redirect_to_login
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
@@ -222,13 +221,11 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
def post(self, request, *args, **kwargs):
self.object = get_object_or_404(User, pk=self.kwargs.get("id"))
staff_session = request.user.get_active_staff_session(request.session.session_key)
self.request.user.log_action('pretix.control.auth.user.impersonated',
user=request.user,
data={
'other': self.kwargs.get("id"),
'other_email': self.object.email,
'staff_session': staff_session.pk,
'other_email': self.object.email
})
oldkey = request.session.session_key
@@ -252,12 +249,6 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
with signals.no_update_last_login(), keep_session_age(request.session):
login(request, hijacked, backend=backend)
request.session.save()
staff_session.logs.create(
method='(NOTE)',
url=f'Begin impersonating user #{hijacked.pk} (request session {oldkey[:8]} -> {request.session.session_key[:8]})',
)
request.session["hijack_history"] = hijack_history
signals.hijack_started.send(
@@ -274,15 +265,13 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
class UserImpersonateStopView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
staff_session_key = request.session['hijacker_session']
prev_session_key = request.session.session_key
impersonated = request.user
hijs = request.session['hijacker_session']
hijack_history = request.session.get("hijack_history", [])
hijacked = request.user
prev_session = hijack_history.pop()
hijacker = get_object_or_404(get_user_model(), pk=prev_session["user"])
staff_session = hijacker.get_active_staff_session(staff_session_key)
if not staff_session:
raise PermissionDenied
expected_hash = salted_hmac(
key_salt=b"hijack-history-hash",
@@ -310,22 +299,17 @@ class UserImpersonateStopView(LoginRequiredMixin, View):
hijacked=hijacked,
)
request.session.save()
staff_session.session_key = request.session.session_key
staff_session.save()
staff_session.logs.create(
method='(NOTE)',
url=f'Stop impersonating user #{hijacked.pk} (request session {prev_session_key[:8]}, '
f'staff session {staff_session_key[:8]} -> {request.session.session_key[:8]})',
)
ss = request.user.get_active_staff_session(hijs)
if ss:
request.session.save()
ss.session_key = request.session.session_key
ss.save()
request.user.log_action('pretix.control.auth.user.impersonate_stopped',
user=request.user,
data={
'other': hijacked.pk,
'other_email': hijacked.email,
'staff_session': staff_session.pk,
'other': impersonated.pk,
'other_email': impersonated.email
})
return redirect(reverse('control:index'))
+6 -31
View File
@@ -29,61 +29,36 @@ class EnvOrParserConfig:
self.cp = configparser
def _envkey(self, section, option):
section = re.sub("[^a-zA-Z0-9]", "_", section.upper())
option = re.sub("[^a-zA-Z0-9]", "_", option.upper())
return f"PRETIX_{section}_{option}"
def _file_envkey(self, section, option):
section = re.sub("[^a-zA-Z0-9]", "_", section.upper())
option = re.sub("[^a-zA-Z0-9]", "_", option.upper())
return f"FILE__PRETIX_{section}_{option}"
section = re.sub('[^a-zA-Z0-9]', '_', section.upper())
option = re.sub('[^a-zA-Z0-9]', '_', option.upper())
return f'PRETIX_{section}_{option}'
def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
if self._file_envkey(section, option) in os.environ:
with open(os.environ[self._file_envkey(section, option)], "r") as f:
return f.read().strip()
if self._envkey(section, option) in os.environ:
return os.environ[self._envkey(section, option)]
return self.cp.get(section, option, raw=raw, vars=vars, fallback=fallback)
def getint(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
if self._file_envkey(section, option) in os.environ:
with open(os.environ[self._file_envkey(section, option)], "r") as f:
return int(f.read().strip())
if self._envkey(section, option) in os.environ:
return int(os.environ[self._envkey(section, option)])
return self.cp.getint(section, option, raw=raw, vars=vars, fallback=fallback)
def getfloat(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
if self._file_envkey(section, option) in os.environ:
with open(os.environ[self._file_envkey(section, option)], "r") as f:
return float(f.read().strip())
if self._envkey(section, option) in os.environ:
return float(os.environ[self._envkey(section, option)])
return self.cp.getfloat(section, option, raw=raw, vars=vars, fallback=fallback)
def getboolean(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
if self._file_envkey(section, option) in os.environ:
with open(os.environ[self._file_envkey(section, option)], "r") as f:
return self.cp._convert_to_boolean(f.read().strip())
if self._envkey(section, option) in os.environ:
return self.cp._convert_to_boolean(
os.environ[self._envkey(section, option)]
)
return self.cp.getboolean(
section, option, raw=raw, vars=vars, fallback=fallback
)
return self.cp._convert_to_boolean(os.environ[self._envkey(section, option)])
return self.cp.getboolean(section, option, raw=raw, vars=vars, fallback=fallback)
def has_section(self, section):
if any(k.startswith(self._file_envkey(section, "")) for k in os.environ):
return True
if any(k.startswith(self._envkey(section, "")) for k in os.environ):
if any(k.startswith(self._envkey(section, '')) for k in os.environ):
return True
return self.cp.has_section(section)
def has_option(self, section, option):
if self._file_envkey(section, option) in os.environ:
return True
if self._envkey(section, option) in os.environ:
return True
return self.cp.has_option(section, option)
+1 -14
View File
@@ -25,19 +25,6 @@ import text_unidecode
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
EPC_QR_ALLOWED_CHARS = set(
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789/-?:().,'+ "
)
def epc_qr_field(value):
return ''.join(
char for char in text_unidecode.unidecode(str(value or ''))
if char in EPC_QR_ALLOWED_CHARS
)
def dotdecimal(value):
return str(value).replace(",", ".")
@@ -90,7 +77,7 @@ def euro_epc_qr(
return {
"id": "girocode",
"label": "EPC-QR",
"qr_data": "\n".join(epc_qr_field(d) for d in [
"qr_data": "\n".join(text_unidecode.unidecode(str(d or '')) for d in [
"BCD", # Service Tag: BCD
"002", # Version: V2
"2", # Character set: ISO 8859-1
+3 -8
View File
@@ -27,7 +27,6 @@ from django.conf import settings
from django.contrib.auth import login as auth_login
from django.contrib.gis import geoip2
from django.core.cache import cache
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_countries.fields import Country
@@ -170,15 +169,11 @@ def handle_login_source(user, request):
with language(user.locale):
mail(
user.email,
_('New sign-in to your account'),
_('Login from new source detected'),
'pretixcontrol/email/login_notice.txt',
{
'when': date_format(src.last_seen, 'DATETIME_FORMAT'),
'agent': src.agent_type,
'os': src.os_type,
# ua-parser returns "Other" for unidentified desktop devices.
'device': src.device_type if src.device_type and src.device_type != 'Other' else None,
'country': Country(str(country)).name if country else None,
'source': src,
'country': Country(str(country)).name if country else _('Unknown country'),
'instance': settings.PRETIX_INSTANCE_NAME,
'url': mainreverse_absolute('control:user.settings')
},
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
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
+9999 -8707
View File
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
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
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

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