mirror of
https://github.com/pretix/pretix.git
synced 2025-12-19 16:22:26 +00:00
Compare commits
4 Commits
widget-dia
...
mail-setti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22d6426c3e | ||
|
|
61b398949e | ||
|
|
d0a1fcfb12 | ||
|
|
a9edf2ef8a |
@@ -74,11 +74,6 @@ class ExportersMixin:
|
|||||||
@action(detail=True, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
|
@action(detail=True, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
|
||||||
def download(self, *args, **kwargs):
|
def download(self, *args, **kwargs):
|
||||||
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
|
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
|
||||||
if not cf.allowed_for_session(self.request, "exporters-api"):
|
|
||||||
return Response(
|
|
||||||
{'status': 'failed', 'message': 'Unknown file ID or export failed'},
|
|
||||||
status=status.HTTP_410_GONE
|
|
||||||
)
|
|
||||||
if cf.file:
|
if cf.file:
|
||||||
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
|
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
|
||||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
|
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
|
||||||
@@ -114,8 +109,7 @@ class ExportersMixin:
|
|||||||
serializer = JobRunSerializer(exporter=instance, data=self.request.data, **self.get_serializer_kwargs())
|
serializer = JobRunSerializer(exporter=instance, data=self.request.data, **self.get_serializer_kwargs())
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
cf = CachedFile(web_download=True)
|
cf = CachedFile(web_download=False)
|
||||||
cf.bind_to_session(self.request, "exporters-api")
|
|
||||||
cf.date = now()
|
cf.date = now()
|
||||||
cf.expires = now() + timedelta(hours=24)
|
cf.expires = now() + timedelta(hours=24)
|
||||||
cf.save()
|
cf.save()
|
||||||
|
|||||||
@@ -59,37 +59,6 @@ class CachedFile(models.Model):
|
|||||||
web_download = models.BooleanField(default=True) # allow web download, True for backwards compatibility in plugins
|
web_download = models.BooleanField(default=True) # allow web download, True for backwards compatibility in plugins
|
||||||
session_key = models.TextField(null=True, blank=True) # only allow download in this session
|
session_key = models.TextField(null=True, blank=True) # only allow download in this session
|
||||||
|
|
||||||
def session_key_for_request(self, request, salt=None):
|
|
||||||
from ...api.models import OAuthAccessToken, OAuthApplication
|
|
||||||
from .devices import Device
|
|
||||||
from .organizer import TeamAPIToken
|
|
||||||
|
|
||||||
if hasattr(request, "auth") and isinstance(request.auth, OAuthAccessToken):
|
|
||||||
k = f'app:{request.auth.application.pk}'
|
|
||||||
elif hasattr(request, "auth") and isinstance(request.auth, OAuthApplication):
|
|
||||||
k = f'app:{request.auth.pk}'
|
|
||||||
elif hasattr(request, "auth") and isinstance(request.auth, TeamAPIToken):
|
|
||||||
k = f'token:{request.auth.pk}'
|
|
||||||
elif hasattr(request, "auth") and isinstance(request.auth, Device):
|
|
||||||
k = f'device:{request.auth.pk}'
|
|
||||||
elif request.session.session_key:
|
|
||||||
k = request.session.session_key
|
|
||||||
else:
|
|
||||||
raise ValueError("No auth method found to bind to")
|
|
||||||
|
|
||||||
if salt:
|
|
||||||
k = f"{k}!{salt}"
|
|
||||||
return k
|
|
||||||
|
|
||||||
def allowed_for_session(self, request, salt=None):
|
|
||||||
return (
|
|
||||||
not self.session_key or
|
|
||||||
self.session_key_for_request(request, salt) == self.session_key
|
|
||||||
)
|
|
||||||
|
|
||||||
def bind_to_session(self, request, salt=None):
|
|
||||||
self.session_key = self.session_key_for_request(request, salt)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=CachedFile)
|
@receiver(post_delete, sender=CachedFile)
|
||||||
def cached_file_delete(sender, instance, **kwargs):
|
def cached_file_delete(sender, instance, **kwargs):
|
||||||
|
|||||||
@@ -801,7 +801,11 @@ def get_sample_context(event, context_parameters, rich=True):
|
|||||||
sample = v.render_sample(event)
|
sample = v.render_sample(event)
|
||||||
if isinstance(sample, PlainHtmlAlternativeString):
|
if isinstance(sample, PlainHtmlAlternativeString):
|
||||||
context_dict[k] = PlainHtmlAlternativeString(
|
context_dict[k] = PlainHtmlAlternativeString(
|
||||||
sample.plain,
|
'<{el} class="placeholder" title="{title}">{plain}</{el}>'.format(
|
||||||
|
el='span',
|
||||||
|
title=lbl,
|
||||||
|
plain=escape(sample.plain),
|
||||||
|
),
|
||||||
'<{el} class="placeholder placeholder-html" title="{title}">{html}</{el}>'.format(
|
'<{el} class="placeholder placeholder-html" title="{title}">{html}</{el}>'.format(
|
||||||
el='div' if sample.is_block else 'span',
|
el='div' if sample.is_block else 'span',
|
||||||
title=lbl,
|
title=lbl,
|
||||||
|
|||||||
@@ -36,8 +36,9 @@ class DownloadView(TemplateView):
|
|||||||
def object(self) -> CachedFile:
|
def object(self) -> CachedFile:
|
||||||
try:
|
try:
|
||||||
o = get_object_or_404(CachedFile, id=self.kwargs['id'], web_download=True)
|
o = get_object_or_404(CachedFile, id=self.kwargs['id'], web_download=True)
|
||||||
if not o.allowed_for_session(self.request):
|
if o.session_key:
|
||||||
raise Http404()
|
if o.session_key != self.request.session.session_key:
|
||||||
|
raise Http404()
|
||||||
return o
|
return o
|
||||||
except (ValueError, ValidationError): # Invalid URLs
|
except (ValueError, ValidationError): # Invalid URLs
|
||||||
raise Http404()
|
raise Http404()
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
|
|||||||
from django.db.models import Prefetch, Q, prefetch_related_objects
|
from django.db.models import Prefetch, Q, prefetch_related_objects
|
||||||
from django.forms import formset_factory, inlineformset_factory
|
from django.forms import formset_factory, inlineformset_factory
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property, lazy
|
||||||
from django.utils.html import escape, format_html
|
from django.utils.html import escape, format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.timezone import get_current_timezone_name
|
from django.utils.timezone import get_current_timezone_name
|
||||||
@@ -53,7 +53,7 @@ from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
|
|||||||
from django_countries.fields import LazyTypedChoiceField
|
from django_countries.fields import LazyTypedChoiceField
|
||||||
from django_scopes.forms import SafeModelMultipleChoiceField
|
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||||
from i18nfield.forms import (
|
from i18nfield.forms import (
|
||||||
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
|
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
|
||||||
)
|
)
|
||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
|
|
||||||
@@ -1311,9 +1311,17 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
|||||||
mail_text_order_invoice = I18nFormField(
|
mail_text_order_invoice = I18nFormField(
|
||||||
label=_("Text"),
|
label=_("Text"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=I18nMarkdownTextarea,
|
widget=I18nTextarea, # no Markdown supported
|
||||||
help_text=_("This will only be used if the invoice is sent to a different email address or at a different time "
|
help_text=lazy(
|
||||||
"than the order confirmation."),
|
lambda: str(_(
|
||||||
|
"This will only be used if the invoice is sent to a different email address or at a different time "
|
||||||
|
"than the order confirmation."
|
||||||
|
)) + " " + str(_(
|
||||||
|
"Formatting is not supported, as some accounting departments process mail automatically and do not "
|
||||||
|
"handle formatted emails properly."
|
||||||
|
)),
|
||||||
|
str
|
||||||
|
)()
|
||||||
)
|
)
|
||||||
mail_subject_download_reminder = I18nFormField(
|
mail_subject_download_reminder = I18nFormField(
|
||||||
label=_("Subject sent to order contact address"),
|
label=_("Subject sent to order contact address"),
|
||||||
@@ -1481,6 +1489,9 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
|||||||
'mail_subject_resend_all_links': ['event', 'orders'],
|
'mail_subject_resend_all_links': ['event', 'orders'],
|
||||||
'mail_attach_ical_description': ['event', 'event_or_subevent'],
|
'mail_attach_ical_description': ['event', 'event_or_subevent'],
|
||||||
}
|
}
|
||||||
|
plain_rendering = {
|
||||||
|
'mail_text_order_invoice',
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.event = event = kwargs.get('obj')
|
self.event = event = kwargs.get('obj')
|
||||||
@@ -1499,7 +1510,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
|||||||
self.event.meta_values_cached = self.event.meta_values.select_related('property').all()
|
self.event.meta_values_cached = self.event.meta_values.select_related('property').all()
|
||||||
|
|
||||||
for k, v in self.base_context.items():
|
for k, v in self.base_context.items():
|
||||||
self._set_field_placeholders(k, v, rich=k.startswith('mail_text_'))
|
self._set_field_placeholders(k, v, rich=k.startswith('mail_text_') and k not in self.plain_rendering)
|
||||||
|
|
||||||
for k, v in list(self.fields.items()):
|
for k, v in list(self.fields.items()):
|
||||||
if k.endswith('_attendee') and not event.settings.attendee_emails_asked:
|
if k.endswith('_attendee') and not event.settings.attendee_emails_asked:
|
||||||
|
|||||||
@@ -829,8 +829,8 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
|||||||
return locales
|
return locales
|
||||||
|
|
||||||
# get all supported placeholders with dummy values
|
# get all supported placeholders with dummy values
|
||||||
def placeholders(self, item):
|
def placeholders(self, item, rich=True):
|
||||||
return get_sample_context(self.request.event, MailSettingsForm.base_context[item])
|
return get_sample_context(self.request.event, MailSettingsForm.base_context[item], rich=rich)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
preview_item = request.POST.get('item', '')
|
preview_item = request.POST.get('item', '')
|
||||||
@@ -851,6 +851,14 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
|||||||
msgs[self.supported_locale[idx]] = prefix_subject(self.request.event, format_map(
|
msgs[self.supported_locale[idx]] = prefix_subject(self.request.event, format_map(
|
||||||
bleach.clean(v), self.placeholders(preview_item), raise_on_missing=True
|
bleach.clean(v), self.placeholders(preview_item), raise_on_missing=True
|
||||||
), highlight=True)
|
), highlight=True)
|
||||||
|
elif preview_item in MailSettingsForm.plain_rendering:
|
||||||
|
msgs[self.supported_locale[idx]] = mark_safe(
|
||||||
|
format_map(
|
||||||
|
conditional_escape(v),
|
||||||
|
self.placeholders(preview_item, rich=False),
|
||||||
|
raise_on_missing=True
|
||||||
|
).replace("\n", "<br />")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
placeholders = self.placeholders(preview_item)
|
placeholders = self.placeholders(preview_item)
|
||||||
msgs[self.supported_locale[idx]] = format_map(
|
msgs[self.supported_locale[idx]] = format_map(
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import Http404
|
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -86,7 +85,6 @@ class BaseImportView(TemplateView):
|
|||||||
filename='import.csv',
|
filename='import.csv',
|
||||||
type='text/csv',
|
type='text/csv',
|
||||||
)
|
)
|
||||||
cf.bind_to_session(request, "modelimport")
|
|
||||||
cf.file.save('import.csv', request.FILES['file'])
|
cf.file.save('import.csv', request.FILES['file'])
|
||||||
|
|
||||||
if self.request.POST.get("charset") in ENCODINGS:
|
if self.request.POST.get("charset") in ENCODINGS:
|
||||||
@@ -139,10 +137,7 @@ class BaseProcessView(AsyncAction, FormView):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def file(self):
|
def file(self):
|
||||||
cf = get_object_or_404(CachedFile, pk=self.kwargs.get("file"), filename="import.csv")
|
return get_object_or_404(CachedFile, pk=self.kwargs.get("file"), filename="import.csv")
|
||||||
if not cf.allowed_for_session(self.request, "modelimport"):
|
|
||||||
raise Http404()
|
|
||||||
return cf
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def parsed(self):
|
def parsed(self):
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
|||||||
cf = None
|
cf = None
|
||||||
if request.POST.get("background", "").strip():
|
if request.POST.get("background", "").strip():
|
||||||
try:
|
try:
|
||||||
cf = CachedFile.objects.get(id=request.POST.get("background"), web_download=True)
|
cf = CachedFile.objects.get(id=request.POST.get("background"))
|
||||||
except CachedFile.DoesNotExist:
|
except CachedFile.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,7 @@ from collections import OrderedDict
|
|||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import Http404
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import get_language, gettext_lazy as _
|
from django.utils.translation import get_language, gettext_lazy as _
|
||||||
@@ -95,8 +94,6 @@ class ShredDownloadView(RecentAuthenticationRequiredMixin, EventPermissionRequir
|
|||||||
cf = CachedFile.objects.get(pk=kwargs['file'])
|
cf = CachedFile.objects.get(pk=kwargs['file'])
|
||||||
except CachedFile.DoesNotExist:
|
except CachedFile.DoesNotExist:
|
||||||
raise ShredError(_("The download file could no longer be found on the server, please try to start again."))
|
raise ShredError(_("The download file could no longer be found on the server, please try to start again."))
|
||||||
if not cf.allowed_for_session(self.request):
|
|
||||||
raise Http404()
|
|
||||||
|
|
||||||
with ZipFile(cf.file.file, 'r') as zipfile:
|
with ZipFile(cf.file.file, 'r') as zipfile:
|
||||||
indexdata = json.loads(zipfile.read('index.json').decode())
|
indexdata = json.loads(zipfile.read('index.json').decode())
|
||||||
@@ -114,7 +111,7 @@ class ShredDownloadView(RecentAuthenticationRequiredMixin, EventPermissionRequir
|
|||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['shredders'] = self.shredders
|
ctx['shredders'] = self.shredders
|
||||||
ctx['download_on_shred'] = any(shredder.require_download_confirmation for shredder in shredders)
|
ctx['download_on_shred'] = any(shredder.require_download_confirmation for shredder in shredders)
|
||||||
ctx['file'] = cf
|
ctx['file'] = get_object_or_404(CachedFile, pk=kwargs.get("file"))
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<br>
|
<br>
|
||||||
<span data-time="{{ ev.date_from.isoformat }}" data-timezone="{{ request.event.timezone }}">
|
<span data-time="{{ ev.date_from.isoformat }}" data-timezone="{{ request.event.timezone }}">
|
||||||
{% html_time ev.date_from "TIME_FORMAT" attr_fmt="H:i" as time%}
|
{% html_time ev.date_from "TIME_FORMAT" attr_fmt="H:i" as time%}
|
||||||
{% blocktrans trimmed with time=time %}
|
{% blocktrans with time=time %}
|
||||||
Begin: {{ time }}
|
Begin: {{ time }}
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</span>
|
</span>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<br>
|
<br>
|
||||||
<span data-time="{{ ev.date_to.isoformat }}" data-timezone="{{ request.event.timezone }}">
|
<span data-time="{{ ev.date_to.isoformat }}" data-timezone="{{ request.event.timezone }}">
|
||||||
{% html_time ev.date_to "TIME_FORMAT" attr_fmt="H:i" as time%}
|
{% html_time ev.date_to "TIME_FORMAT" attr_fmt="H:i" as time%}
|
||||||
{% blocktrans trimmed with time=time %}
|
{% blocktrans with time=time %}
|
||||||
End: {{ time }}
|
End: {{ time }}
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
{% if event.settings.presale_start_show_date %}
|
{% if event.settings.presale_start_show_date %}
|
||||||
<br><span class="text-muted">
|
<br><span class="text-muted">
|
||||||
{% html_time event.event.effective_presale_start "SHORT_DATE_FORMAT" as date %}
|
{% html_time event.event.effective_presale_start "SHORT_DATE_FORMAT" as date %}
|
||||||
{% blocktrans trimmed with date=date %}
|
{% blocktrans with date=date %}
|
||||||
Sale starts {{ date }}
|
Sale starts {{ date }}
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -848,9 +848,6 @@ $table-bg-accent: rgba(128, 128, 128, 0.05);
|
|||||||
outline: 2px solid $brand-primary;
|
outline: 2px solid $brand-primary;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
&:not([open]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.pretix-widget-frame-isloading:focus {
|
.pretix-widget-frame-isloading:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user