mirror of
https://github.com/pretix/pretix.git
synced 2026-07-07 05:31:55 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a47ba7ff5 | |||
| dd2a74557d | |||
| bb9c4fa18d | |||
| ac356acd29 | |||
| 54eadaffcc | |||
| 617a548b19 | |||
| fe0f7864e7 | |||
| fc4a2f5508 | |||
| 4cb32a753f | |||
| 3ae9aabcfb | |||
| 3270c4e583 | |||
| 3b285a89dd | |||
| c1683df1fd | |||
| 6fdcbcebd2 | |||
| ddbea7c32e | |||
| 3f8ed0f722 | |||
| 388eb7de96 | |||
| df4c83d849 | |||
| fa4cec8a2d | |||
| 943b319557 | |||
| 28b13667ce | |||
| b00d1c9156 | |||
| 120317a8f2 | |||
| 7d5b00a610 | |||
| 83612c7d65 | |||
| 7a5f96369a | |||
| 7fd6bf41f9 | |||
| 458c3d4b83 | |||
| d10d061e45 | |||
| d30bca50f7 | |||
| 3903aca7c9 | |||
| 493c920aba | |||
| 09b7bc00b0 | |||
| 2e195c0274 |
+6
-5
@@ -53,7 +53,8 @@ dependencies = [
|
||||
"django-oauth-toolkit==2.3.*",
|
||||
"django-otp==1.7.*",
|
||||
"django-phonenumber-field==8.4.*",
|
||||
"django-redis==6.0.*",
|
||||
"django-querytagger==0.0.3",
|
||||
"django-redis==7.0.*",
|
||||
"django-scopes==2.0.*",
|
||||
"django-statici18n==2.7.*",
|
||||
"djangorestframework==3.17.*",
|
||||
@@ -76,7 +77,7 @@ dependencies = [
|
||||
"paypal-checkout-serversdk==1.0.*",
|
||||
"PyJWT==2.13.*",
|
||||
"phonenumberslite==9.0.*",
|
||||
"Pillow==12.2.*",
|
||||
"Pillow==12.3.*",
|
||||
"pretix-plugin-build",
|
||||
"protobuf==7.35.*",
|
||||
"psycopg2-binary",
|
||||
@@ -91,9 +92,9 @@ dependencies = [
|
||||
"pyuca",
|
||||
"qrcode==8.2",
|
||||
"redis==7.4.*",
|
||||
"reportlab==4.5.*",
|
||||
"requests==2.32.*",
|
||||
"sentry-sdk==2.63.*",
|
||||
"reportlab==5.0.*",
|
||||
"requests==2.34.*",
|
||||
"sentry-sdk==2.64.*",
|
||||
"sepaxml==2.7.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
|
||||
@@ -118,6 +118,7 @@ ALL_LANGUAGES = [
|
||||
('sv', _('Swedish')),
|
||||
('es', _('Spanish')),
|
||||
('es-419', _('Spanish (Latin America)')),
|
||||
('th', _('Thai')),
|
||||
('tr', _('Turkish')),
|
||||
('uk', _('Ukrainian')),
|
||||
]
|
||||
|
||||
@@ -747,6 +747,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'max_items_per_order',
|
||||
'reservation_time',
|
||||
'contact_mail',
|
||||
'contact_url',
|
||||
'show_variations_expanded',
|
||||
'hide_sold_out',
|
||||
'meta_noindex',
|
||||
|
||||
@@ -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_lazy as _
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
@@ -492,7 +492,9 @@ class TeamInviteSerializer(serializers.ModelSerializer):
|
||||
def _send_invite(self, instance):
|
||||
mail(
|
||||
instance.email,
|
||||
_('Account invitation'),
|
||||
gettext('You\'ve been invited to join %(organizer)s') % {
|
||||
'organizer': self.context['organizer'].name,
|
||||
},
|
||||
'pretixcontrol/email/invitation.txt',
|
||||
{
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
@@ -574,6 +576,7 @@ 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',
|
||||
|
||||
@@ -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 = True
|
||||
self.instance.vat_id_validated = bool(normalized_id)
|
||||
self.instance.vat_id = data['vat_id'] = normalized_id
|
||||
except VATIDFinalError as e:
|
||||
if self.all_optional:
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# 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
|
||||
@@ -43,6 +45,8 @@ from pretix.multidomain.urlreverse import (
|
||||
)
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_supported = None
|
||||
|
||||
|
||||
@@ -223,7 +227,26 @@ 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)
|
||||
|
||||
|
||||
@@ -243,21 +266,7 @@ 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
|
||||
@@ -268,18 +277,15 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
# https://github.com/pretix/pretix/issues/765
|
||||
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
|
||||
|
||||
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}", "*"))
|
||||
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']
|
||||
|
||||
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))
|
||||
return resp
|
||||
|
||||
def _build_csp(self, request, resp):
|
||||
url = resolve(request.path_info)
|
||||
|
||||
h = {
|
||||
'default-src': ["{static}"],
|
||||
@@ -288,8 +294,8 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
'frame-src': ['{static}'],
|
||||
'style-src': ["{static}", "{media}"],
|
||||
'connect-src': ["{dynamic}", "{media}"],
|
||||
'img-src': ["{static}", "{media}", "data:"] + img_src,
|
||||
'font-src': ["{static}"] + list(font_src),
|
||||
'img-src': ["{static}", "{media}", "data:"],
|
||||
'font-src': ["{static}"],
|
||||
'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
|
||||
@@ -298,6 +304,13 @@ 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'"]
|
||||
@@ -309,6 +322,7 @@ 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
|
||||
@@ -317,27 +331,32 @@ 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))
|
||||
|
||||
staticdomain = "'self'"
|
||||
dynamicdomain = "'self'"
|
||||
mediadomain = "'self'"
|
||||
placeholders = {
|
||||
"{static}": ["'self'"],
|
||||
"{dynamic}": ["'self'"],
|
||||
"{media}": ["'self'"],
|
||||
}
|
||||
if settings.MEDIA_URL.startswith('http'):
|
||||
mediadomain += " " + settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)]
|
||||
placeholders["{media}"].append(settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)])
|
||||
if settings.STATIC_URL.startswith('http'):
|
||||
staticdomain += " " + settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)]
|
||||
placeholders["{static}"].append(settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)])
|
||||
if settings.SITE_URL.startswith('http'):
|
||||
if settings.SITE_URL.find('/', 9) > 0:
|
||||
staticdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)]
|
||||
dynamicdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)]
|
||||
placeholders["{static}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
|
||||
placeholders["{dynamic}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
|
||||
else:
|
||||
staticdomain += " " + settings.SITE_URL
|
||||
dynamicdomain += " " + settings.SITE_URL
|
||||
placeholders["{static}"].append(settings.SITE_URL)
|
||||
placeholders["{dynamic}"].append(settings.SITE_URL)
|
||||
|
||||
if hasattr(request, 'organizer') and request.organizer:
|
||||
if hasattr(request, 'event') and request.event:
|
||||
@@ -348,18 +367,29 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
siteurlsplit = urlsplit(settings.SITE_URL)
|
||||
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
|
||||
domain = '%s:%d' % (domain, siteurlsplit.port)
|
||||
dynamicdomain += " " + domain
|
||||
placeholders["{dynamic}"].append(domain)
|
||||
|
||||
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']
|
||||
for k, v in h.items():
|
||||
h[k] = sorted(set(result for part in v for result in placeholders.get(part, [part])))
|
||||
|
||||
return resp
|
||||
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
|
||||
|
||||
|
||||
class RejectInvalidInputMiddleware(MiddlewareMixin):
|
||||
|
||||
@@ -373,7 +373,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
|
||||
mail(
|
||||
email or self.email,
|
||||
_('Account information changed'),
|
||||
_('Changes to your account'),
|
||||
'pretixcontrol/email/security_notice.txt',
|
||||
{
|
||||
'user': self,
|
||||
@@ -400,12 +400,13 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
|
||||
with language(self.locale):
|
||||
if reason == 'email_change':
|
||||
msg = str(_('to confirm changing your email address from {old_email}\nto {new_email}, use the following code:').format(
|
||||
msg = str(_('To change your email address from {old_email} to {new_email}, use the following code:').format(
|
||||
old_email=self.email, new_email=email,
|
||||
))
|
||||
elif reason == 'email_verify':
|
||||
msg = str(_('to confirm that your email address {email} belongs to your pretix account, use the following code:').format(
|
||||
msg = str(_('To verify your email address {email} on {instance}, use the following code:').format(
|
||||
email=self.email,
|
||||
instance=settings.PRETIX_INSTANCE_NAME,
|
||||
))
|
||||
else:
|
||||
raise Exception('Invalid confirmation code reason')
|
||||
@@ -418,7 +419,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
}
|
||||
mail(
|
||||
email or self.email,
|
||||
_('pretix confirmation code'),
|
||||
_('Your confirmation code'),
|
||||
'pretixcontrol/email/confirmation_code.txt',
|
||||
{
|
||||
'user': self,
|
||||
@@ -462,7 +463,9 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
from pretix.base.services.mail import mail
|
||||
|
||||
mail(
|
||||
self.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt',
|
||||
self.email,
|
||||
_('Reset your password'),
|
||||
'pretixcontrol/email/forgot.txt',
|
||||
{
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
'user': self,
|
||||
@@ -647,25 +650,22 @@ 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=None):
|
||||
def has_active_staff_session(self, session_key):
|
||||
"""
|
||||
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=None):
|
||||
if not self.is_staff:
|
||||
def get_active_staff_session(self, session_key):
|
||||
if not self.is_staff or not session_key:
|
||||
return None
|
||||
if not hasattr(self, '_staff_session_cache'):
|
||||
self._staff_session_cache = {}
|
||||
if session_key not in self._staff_session_cache:
|
||||
qs = StaffSession.objects.filter(
|
||||
user=self, date_end__isnull=True
|
||||
)
|
||||
if session_key:
|
||||
qs = qs.filter(session_key=session_key)
|
||||
sess = qs.first()
|
||||
sess = StaffSession.objects.filter(
|
||||
user=self, date_end__isnull=True, session_key=session_key
|
||||
).first()
|
||||
if sess:
|
||||
if sess.date_start < now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE):
|
||||
sess.date_end = now()
|
||||
|
||||
@@ -834,7 +834,7 @@ class BasePaymentProvider:
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
|
||||
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str | None:
|
||||
"""
|
||||
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.
|
||||
|
||||
+10
-5
@@ -77,6 +77,7 @@ 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
|
||||
@@ -401,11 +402,7 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
"editor_sample": _("Event organizer info text"),
|
||||
"evaluate": lambda op, order, ev: str(order.event.settings.organizer_info_text)
|
||||
}),
|
||||
("event_info_text", {
|
||||
"label": _("Event info text"),
|
||||
"editor_sample": _("Event info text"),
|
||||
"evaluate": lambda op, order, ev: str(order.event.settings.event_info_text)
|
||||
}),
|
||||
("event_info_text", {}), # Placeholder to "reserve" position, defined later in `get_variables`
|
||||
("now_date", {
|
||||
"label": _("Printing date"),
|
||||
"editor_sample": _("2017-05-31"),
|
||||
@@ -670,6 +667,14 @@ 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"])
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
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
|
||||
@@ -377,12 +378,13 @@ 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('Bulk-refund confirmation'),
|
||||
subject=gettext('Confirm event cancellation and bulk refund'),
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -340,12 +340,13 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
|
||||
if schedule.owner.is_active:
|
||||
mail(
|
||||
email=schedule.owner.email,
|
||||
subject=gettext('Export failed'),
|
||||
subject=gettext('Scheduled 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,
|
||||
|
||||
@@ -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_lazy as _
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import CachedFile, Event, User, cachedfile_name
|
||||
@@ -171,15 +171,16 @@ 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,
|
||||
_('Data shredding completed'),
|
||||
gettext('Data shredding completed for %(event)s') % {'event': event_name},
|
||||
'pretixbase/email/shred_completed.txt',
|
||||
{
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
'user': user,
|
||||
'organizer': event.organizer.name,
|
||||
'event': str(event.name),
|
||||
'event': 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])
|
||||
},
|
||||
|
||||
@@ -1276,7 +1276,7 @@ DEFAULTS = {
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Allow to update existing invoices"),
|
||||
label=_("Allow updating 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,8 +1924,6 @@ 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': {
|
||||
@@ -2286,6 +2284,17 @@ 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,10 +1,14 @@
|
||||
{% load i18n %}
|
||||
{% trans "You have requested us to cancel an event which includes a larger bulk-refund:" %}
|
||||
{% trans "You requested to cancel an event that involves a large bulk refund:" %}
|
||||
|
||||
{% trans "Event" %}: {{ event }}
|
||||
- {% trans "Event" %}: {{ event }}
|
||||
- {% trans "Estimated refund" %}: **{{ amount }}**
|
||||
|
||||
{% trans "Estimated refund amount" %}: **{{ amount }}**
|
||||
{% trans "To confirm, paste the following code into the cancellation form:" %}
|
||||
|
||||
{% trans "Please confirm that you want to proceed by coping the following confirmation code into the cancellation form:" %}
|
||||
{{ confirmation_code }}
|
||||
|
||||
**{{ confirmation_code }}**
|
||||
{% 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 %}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{% load i18n %}
|
||||
{% trans "Your export failed." %}
|
||||
{% trans "Your scheduled export failed." %}
|
||||
|
||||
{% trans "Reason:" %} {{ reason }}
|
||||
- {% trans "Reason" %}: {{ reason }}
|
||||
|
||||
{% if not soft %}
|
||||
{% trans "If your export fails five times in a row, it will no longer be sent." %}
|
||||
{% endif %}
|
||||
{% if not soft %}{% trans "If an export fails five times in a row, we'll stop sending it." %}{% endif %}
|
||||
|
||||
{% trans "Configuration link:" %}
|
||||
{% trans "You can adjust or remove this export here:" %}
|
||||
|
||||
{{ 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 receive these emails based on your notification settings." %}<br>
|
||||
{% trans "You're receiving this email based on your notification settings." %}<br>
|
||||
<a href="{{ settings_url }}">
|
||||
{% trans "Click here to view and change your notification settings" %}
|
||||
{% trans "Manage settings" %}
|
||||
</a>
|
||||
{% if disable_url %}<br>
|
||||
<a href="{{ disable_url }}">
|
||||
{% trans "Click here disable all notifications immediately." %}
|
||||
{% trans "Disable all notifications" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
{% load i18n %}
|
||||
{{ notification.title }}{% if notification.detail %}
|
||||
|
||||
{{ notification.detail }}
|
||||
{% endif %}{% if notification.url %}
|
||||
{{ notification.detail }}{% endif %}{% if notification.url %}
|
||||
|
||||
{{ notification.url }}{% endif %}{% for attr in notification.attributes %}
|
||||
{{ notification.url }}{% endif %}{% if notification.attributes %}
|
||||
|
||||
{{ attr.title }}: {{ attr.value }}{% endfor %}{% for action in notification.actions %}
|
||||
{% for attr in notification.attributes %}- {{ attr.title }}: {{ attr.value }}
|
||||
{% endfor %}{% endif %}{% for action in notification.actions %}
|
||||
|
||||
{{ action.label }}
|
||||
{{ action.url }}{% endfor %}
|
||||
{{ action.label }}:
|
||||
|
||||
{% 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 }}
|
||||
{{ 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 }}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
{% load i18n %}
|
||||
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
|
||||
{% load i18n %}{% blocktrans %}Hello,
|
||||
|
||||
we hereby confirm that the following data shredding job has been completed:
|
||||
The following data shredding job has been completed:
|
||||
|
||||
Organizer: {{ organizer }}
|
||||
- Organizer: {{ organizer }}
|
||||
- Event: {{ event }}
|
||||
- Data selection: {{ shredders }}
|
||||
- Start time: {{ start_time }}
|
||||
|
||||
Event: {{ event }}
|
||||
Any data added to the event after the start time may not have been deleted.
|
||||
|
||||
Data selection: {{ shredders }}
|
||||
|
||||
Start time: {{ start_time }} (new data added after this time might not have been deleted)
|
||||
|
||||
Best regards,
|
||||
|
||||
Your {{ instance }} team
|
||||
Thanks,
|
||||
The {{ instance }} Team
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -625,6 +625,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
|
||||
'max_items_per_order',
|
||||
'reservation_time',
|
||||
'contact_mail',
|
||||
'contact_url',
|
||||
'show_variations_expanded',
|
||||
'hide_sold_out',
|
||||
'meta_noindex',
|
||||
@@ -671,6 +672,11 @@ 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=''):
|
||||
@@ -1899,6 +1905,12 @@ 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,
|
||||
|
||||
@@ -604,6 +604,7 @@ 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',
|
||||
|
||||
@@ -106,6 +106,11 @@ 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(
|
||||
|
||||
@@ -36,6 +36,7 @@ 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
|
||||
@@ -214,12 +215,15 @@ 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 ss:
|
||||
ss.logs.create(
|
||||
url=request.path,
|
||||
method=request.method,
|
||||
impersonating=request.user
|
||||
)
|
||||
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
|
||||
)
|
||||
else:
|
||||
ss = request.user.get_active_staff_session(request.session.session_key)
|
||||
if ss:
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
|
||||
{% load i18n %}{% blocktrans with code=code reason=reason instance=instance %}Hello,
|
||||
|
||||
{{ reason }}
|
||||
|
||||
{{ code }}
|
||||
|
||||
Please do never give this code to another person. Our support team will never ask for this code.
|
||||
Don't share this code with anyone. The {{ instance }} team will never ask you for it.
|
||||
|
||||
If this code was not requested by you, please contact us immediately.
|
||||
If you didn't request this code, please contact us immediately.
|
||||
|
||||
Best regards,
|
||||
Your {{ instance }} team
|
||||
Thanks,
|
||||
The {{ instance }} Team
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
{% load i18n %}{% blocktrans with code=code instance=instance %}Hello,
|
||||
{% load i18n %}{% blocktrans with code=code address=address instance=instance %}Hello,
|
||||
|
||||
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:
|
||||
Someone requested to use {{ address }} as a sender address on {{ instance }}. Once verified, emails sent from {{ instance }} can show this address as the sender.
|
||||
|
||||
{{ code }}
|
||||
If this was you, enter the following code in the setup form:
|
||||
|
||||
If this was not requested by you, you can safely ignore this email.
|
||||
{{ code }}
|
||||
|
||||
Best regards,
|
||||
Don't share this code with anyone. The {{ instance }} team will never ask you for it.
|
||||
|
||||
Your {{ instance }} team
|
||||
{% endblocktrans %}
|
||||
If you didn't request this, you can safely ignore this email.
|
||||
|
||||
Thanks,
|
||||
The {{ instance }} Team
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
|
||||
|
||||
you requested a new password. Please go to the following page to reset your password:
|
||||
We received a request to reset the password for your {{ instance }} account. To choose a new password, follow the link below:
|
||||
|
||||
{{ url }}
|
||||
|
||||
Best regards,
|
||||
Your {{ instance }} team
|
||||
If you didn't request this, you can safely ignore this email — your password won't change.
|
||||
|
||||
Thanks,
|
||||
The {{ instance }} Team
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
|
||||
|
||||
you have been invited to a team on {{ instance }}, a platform to perform event
|
||||
ticket sales.
|
||||
You've been invited to join a team on {{ instance }}, an event ticket sales platform.
|
||||
|
||||
Organizer: {{ organizer }}
|
||||
Team: {{ team }}
|
||||
- Organizer: {{ organizer }}
|
||||
- Team: {{ team }}
|
||||
|
||||
To accept, follow the link below:
|
||||
|
||||
If you want to join that team, just click on the following link:
|
||||
{{ url }}
|
||||
|
||||
If you do not want to join, you can safely ignore or delete this email.
|
||||
If you don't want to join, you can safely ignore this email.
|
||||
|
||||
Best regards,
|
||||
|
||||
Your {{ instance }} team
|
||||
Thanks,
|
||||
The {{ instance }} Team
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
{% load i18n %}{% blocktrans with url=url|safe os=source.os_type agent=source.agent_type %}Hello,
|
||||
{% load i18n %}{% blocktrans %}Hello,
|
||||
|
||||
a login to your {{ instance }} account from an unusual or new location was detected. The login was performed using {{ agent }} on {{ os }} from {{ country }}.
|
||||
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.
|
||||
|
||||
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:
|
||||
If you don't recognise this sign-in, please change your password immediately:
|
||||
|
||||
{{ url }}
|
||||
|
||||
Best regards,
|
||||
Your {{ instance }} team
|
||||
Thanks,
|
||||
The {{ instance }} Team
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
|
||||
|
||||
this is to inform you that the account information of your {{ instance }} account has been
|
||||
changed. In particular, the following changes have been performed:
|
||||
The following changes were made to your {{ instance }} account:
|
||||
|
||||
{{ messages }}
|
||||
|
||||
If this change was not performed by you, please contact us immediately.
|
||||
If you didn't make these changes, please contact the {{ instance }} support team immediately.
|
||||
|
||||
You can review and change your account settings here:
|
||||
You can review your account settings here:
|
||||
|
||||
{{ url }}
|
||||
|
||||
Best regards,
|
||||
Your {{ instance }} team
|
||||
Thanks,
|
||||
The {{ instance }} Team
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
{% 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,6 +30,7 @@
|
||||
{% 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" %}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
{% 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 %}
|
||||
|
||||
@@ -370,6 +370,7 @@ 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'),
|
||||
|
||||
@@ -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_lazy as _
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from pretix.base import email
|
||||
@@ -216,13 +216,14 @@ 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(
|
||||
self.simple_form.cleaned_data.get('mail_from'),
|
||||
_('Sender address verification'),
|
||||
sender_address,
|
||||
gettext('Confirm %(address)s as a sender address') % {'address': sender_address},
|
||||
'pretixcontrol/email/email_setup.txt',
|
||||
{
|
||||
'code': self.request.session[session_key],
|
||||
'address': self.simple_form.cleaned_data.get('mail_from'),
|
||||
'address': sender_address,
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
},
|
||||
None,
|
||||
|
||||
@@ -1032,7 +1032,9 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
||||
def _send_invite(self, instance):
|
||||
mail(
|
||||
instance.email,
|
||||
_('Account invitation'),
|
||||
gettext('You\'ve been invited to join %(organizer)s') % {
|
||||
'organizer': self.request.organizer.name,
|
||||
},
|
||||
'pretixcontrol/email/invitation.txt',
|
||||
{
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
|
||||
@@ -975,6 +975,21 @@ 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')
|
||||
|
||||
@@ -31,6 +31,7 @@ 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
|
||||
@@ -221,11 +222,13 @@ 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
|
||||
'other_email': self.object.email,
|
||||
'staff_session': staff_session.pk,
|
||||
})
|
||||
oldkey = request.session.session_key
|
||||
|
||||
@@ -249,6 +252,12 @@ 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(
|
||||
@@ -265,13 +274,15 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
|
||||
class UserImpersonateStopView(LoginRequiredMixin, View):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
impersonated = request.user
|
||||
|
||||
hijs = request.session['hijacker_session']
|
||||
staff_session_key = request.session['hijacker_session']
|
||||
prev_session_key = request.session.session_key
|
||||
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",
|
||||
@@ -299,17 +310,22 @@ class UserImpersonateStopView(LoginRequiredMixin, View):
|
||||
hijacked=hijacked,
|
||||
)
|
||||
|
||||
ss = request.user.get_active_staff_session(hijs)
|
||||
if ss:
|
||||
request.session.save()
|
||||
ss.session_key = request.session.session_key
|
||||
ss.save()
|
||||
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]})',
|
||||
)
|
||||
|
||||
request.user.log_action('pretix.control.auth.user.impersonate_stopped',
|
||||
user=request.user,
|
||||
data={
|
||||
'other': impersonated.pk,
|
||||
'other_email': impersonated.email
|
||||
'other': hijacked.pk,
|
||||
'other_email': hijacked.email,
|
||||
'staff_session': staff_session.pk,
|
||||
})
|
||||
return redirect(reverse('control:index'))
|
||||
|
||||
|
||||
@@ -29,36 +29,61 @@ 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}'
|
||||
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}"
|
||||
|
||||
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._envkey(section, '')) for k in os.environ):
|
||||
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):
|
||||
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)
|
||||
|
||||
@@ -25,6 +25,19 @@ 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(",", ".")
|
||||
@@ -77,7 +90,7 @@ def euro_epc_qr(
|
||||
return {
|
||||
"id": "girocode",
|
||||
"label": "EPC-QR",
|
||||
"qr_data": "\n".join(text_unidecode.unidecode(str(d or '')) for d in [
|
||||
"qr_data": "\n".join(epc_qr_field(d) for d in [
|
||||
"BCD", # Service Tag: ‘BCD’
|
||||
"002", # Version: V2
|
||||
"2", # Character set: ISO 8859-1
|
||||
|
||||
@@ -27,6 +27,7 @@ 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
|
||||
@@ -169,11 +170,15 @@ def handle_login_source(user, request):
|
||||
with language(user.locale):
|
||||
mail(
|
||||
user.email,
|
||||
_('Login from new source detected'),
|
||||
_('New sign-in to your account'),
|
||||
'pretixcontrol/email/login_notice.txt',
|
||||
{
|
||||
'source': src,
|
||||
'country': Country(str(country)).name if country else _('Unknown country'),
|
||||
'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,
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
'url': mainreverse_absolute('control:user.settings')
|
||||
},
|
||||
|
||||
+8707
-9999
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8841
-10040
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8707
-9999
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8832
-10049
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8850
-10034
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8727
-10005
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8871
-10037
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8966
-10041
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
+8707
-9999
File diff suppressed because it is too large
Load Diff
+595
-216
File diff suppressed because it is too large
Load Diff
+8845
-10048
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8707
-9999
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8968
-10041
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8745
-10018
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8707
-9999
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8812
-10029
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8809
-10023
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8729
-10006
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8969
-10043
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8829
-10044
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8899
-10040
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8811
-10023
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8775
-10027
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8857
-10039
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8840
-10041
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8961
-10036
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8848
-10043
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8766
-10020
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8708
-9999
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8845
-10049
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+8707
-9999
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
Reference in New Issue
Block a user