mirror of
https://github.com/pretix/pretix.git
synced 2026-09-15 16:34:42 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e57596b1c | ||
|
|
fbaa5e9cbb | ||
|
|
6025c7942f | ||
|
|
23a3412154 | ||
|
|
3635f6fb0c | ||
|
|
2aadfa2ce4 | ||
|
|
dd0d78242e | ||
|
|
42c5de895c | ||
|
|
e91718e73b | ||
|
|
2c15d8b074 |
@@ -566,7 +566,7 @@ organizer level.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"imprint_url": "https://pretix.eu",
|
||||
"region": "DE",
|
||||
…
|
||||
}
|
||||
|
||||
@@ -579,12 +579,14 @@ organizer level.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"imprint_url":
|
||||
"region":
|
||||
{
|
||||
"value": "https://pretix.eu",
|
||||
"label": "Imprint URL",
|
||||
"value": "DE",
|
||||
"label": "Region",
|
||||
"readonly": false,
|
||||
"help_text": "This should point e.g. to a part of your website that has your contact details and legal information."
|
||||
"help_text": "Will be used to determine date and time formatting as well as default country for customer
|
||||
addresses and phone numbers. For formatting, this takes less priority than the language and
|
||||
is therefore mostly relevant for languages used in different regions globally (like English)."
|
||||
}
|
||||
},
|
||||
…
|
||||
@@ -620,7 +622,7 @@ organizer level.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"imprint_url": "https://example.org/imprint/"
|
||||
"region": "DE"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -632,7 +634,7 @@ organizer level.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"imprint_url": "https://example.org/imprint/",
|
||||
"region": "DE",
|
||||
…
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ dependencies = [
|
||||
"django-querytagger==0.0.3",
|
||||
"django-redis==7.0.*",
|
||||
"django-scopes==2.1.*",
|
||||
"django-statici18n==2.7.*",
|
||||
"django-statici18n==2.8.*",
|
||||
"djangorestframework==3.17.*",
|
||||
"dnspython==2.8.*",
|
||||
"drf_ujson2==1.7.*",
|
||||
|
||||
@@ -2314,25 +2314,27 @@ DEFAULTS = {
|
||||
},
|
||||
'contact_url': {
|
||||
'default': None,
|
||||
'type': str,
|
||||
'serializer_class': serializers.URLField,
|
||||
'form_class': forms.URLField,
|
||||
'type': LazyI18nString,
|
||||
'form_class': I18nURLFormField,
|
||||
'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.")
|
||||
)
|
||||
"Please note that you still need to add a contact email address that will be shared with all emails you send."),
|
||||
widget=I18nTextInput,
|
||||
),
|
||||
'serializer_class': I18nURLField,
|
||||
},
|
||||
'imprint_url': {
|
||||
'default': None,
|
||||
'type': str,
|
||||
'form_class': forms.URLField,
|
||||
'type': LazyI18nString,
|
||||
'form_class': I18nURLFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Imprint URL"),
|
||||
help_text=_("This should point e.g. to a part of your website that has your contact details and legal "
|
||||
"information."),
|
||||
widget=I18nTextInput,
|
||||
),
|
||||
'serializer_class': serializers.URLField,
|
||||
'serializer_class': I18nURLField,
|
||||
},
|
||||
'privacy_url': {
|
||||
'default': None,
|
||||
|
||||
@@ -172,7 +172,9 @@ class CachedFileInput(forms.ClearableFileInput):
|
||||
from ...base.models import CachedFile
|
||||
v = super().value_from_datadict(data, files, name)
|
||||
if v is None and data.get(name + '-cachedfile'): # An explicit "[x] clear" would be False, not None
|
||||
return CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
|
||||
v = CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
|
||||
if not v.allowed_for_session(self.request):
|
||||
v = None
|
||||
return v
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
@@ -244,6 +246,11 @@ class ExtFileField(ExtValidationMixin, SizeFileField):
|
||||
class CachedFileField(ExtFileField):
|
||||
widget = CachedFileInput
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop("request", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.widget.request = self.request
|
||||
|
||||
def to_python(self, data):
|
||||
from ...base.models import CachedFile
|
||||
|
||||
@@ -271,6 +278,8 @@ class CachedFileField(ExtFileField):
|
||||
filename=data.name,
|
||||
type=data.content_type,
|
||||
)
|
||||
if self.request:
|
||||
cf.bind_to_session(self.request) # no salt because we want direct web access
|
||||
cf.file.save(data.name, data.file)
|
||||
cf.save()
|
||||
data._uploaded_to = cf
|
||||
@@ -294,6 +303,8 @@ class CachedFileField(ExtFileField):
|
||||
filename=data.name,
|
||||
type=data.content_type,
|
||||
)
|
||||
if self.request:
|
||||
cf.bind_to_session(self.request) # no salt because we want direct web access
|
||||
cf.file.save(data.name, data.file)
|
||||
cf.save()
|
||||
data._uploaded_to = cf
|
||||
|
||||
@@ -87,6 +87,7 @@ class RRuleForm(forms.Form):
|
||||
('1', pgettext_lazy('rrule', 'first')),
|
||||
('2', pgettext_lazy('rrule', 'second')),
|
||||
('3', pgettext_lazy('rrule', 'third')),
|
||||
('4', pgettext_lazy('rrule', 'fourth')),
|
||||
('-1', pgettext_lazy('rrule', 'last')),
|
||||
],
|
||||
required=False
|
||||
@@ -134,6 +135,7 @@ class RRuleForm(forms.Form):
|
||||
('1', pgettext_lazy('rrule', 'first')),
|
||||
('2', pgettext_lazy('rrule', 'second')),
|
||||
('3', pgettext_lazy('rrule', 'third')),
|
||||
('4', pgettext_lazy('rrule', 'fourth')),
|
||||
('-1', pgettext_lazy('rrule', 'last')),
|
||||
],
|
||||
required=False
|
||||
|
||||
@@ -64,6 +64,7 @@ from pretix.base.forms.auth import (
|
||||
)
|
||||
from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins
|
||||
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.http import get_client_ip, redirect_to_url
|
||||
from pretix.helpers.ratelimit import rate_limit, rate_limit_reset
|
||||
from pretix.helpers.security import handle_login_source, session_login
|
||||
@@ -395,15 +396,17 @@ class Recover(TemplateView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if self.form.is_valid():
|
||||
try:
|
||||
user = User.objects.get(id=self.request.GET.get('id'), auth_backend='native')
|
||||
except User.DoesNotExist:
|
||||
return self.invalid('unknownuser')
|
||||
if not default_token_generator.check_token(user, self.request.GET.get('token')):
|
||||
return self.invalid('invalid')
|
||||
user.set_password(self.form.cleaned_data['password'])
|
||||
user.needs_password_change = False
|
||||
user.save()
|
||||
with transaction.atomic():
|
||||
# Check token in transaction to prevent race condition
|
||||
try:
|
||||
user = User.objects.select_for_update(of=OF_SELF).get(id=self.request.GET.get('id'), auth_backend='native')
|
||||
except User.DoesNotExist:
|
||||
return self.invalid('unknownuser')
|
||||
if not default_token_generator.check_token(user, self.request.GET.get('token')):
|
||||
return self.invalid('invalid')
|
||||
user.set_password(self.form.cleaned_data['password'])
|
||||
user.needs_password_change = False
|
||||
user.save()
|
||||
messages.success(request, _('You can now login using your new password.'))
|
||||
user.log_action('pretix.control.auth.user.forgot_password.recovered')
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ from decimal import Decimal
|
||||
from io import BytesIO
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.files import File
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
@@ -193,6 +194,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
c.expires = now() + timedelta(days=7)
|
||||
c.date = now()
|
||||
c.filename = 'background_preview.pdf'
|
||||
c.bind_to_session(request, "ticketoutput-pdf-background")
|
||||
c.type = 'application/pdf'
|
||||
c.save()
|
||||
c.file.save('empty.pdf', ContentFile(buffer.read()))
|
||||
@@ -218,6 +220,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
c.expires = now() + timedelta(days=7)
|
||||
c.date = now()
|
||||
c.filename = 'background_preview.pdf'
|
||||
c.bind_to_session(request, "ticketoutput-pdf-background")
|
||||
c.type = 'application/pdf'
|
||||
c.file = fileobj
|
||||
c.save()
|
||||
@@ -303,5 +306,7 @@ class FontsCSSView(TemplateView):
|
||||
class PdfView(TemplateView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf")
|
||||
if not cf.allowed_for_session(request, "ticketoutput-pdf-background"):
|
||||
raise PermissionDenied()
|
||||
resp = FileResponse(cf.file, filename=cf.filename, content_type='application/pdf')
|
||||
return resp
|
||||
|
||||
@@ -56,18 +56,11 @@ from pretix.base.services.placeholders import FormPlaceholderMixin # noqa
|
||||
class BaseMailForm(FormPlaceholderMixin, forms.Form):
|
||||
subject = forms.CharField(label=_("Subject"))
|
||||
message = forms.CharField(label=_("Message"))
|
||||
attachment = CachedFileField(
|
||||
label=_("Attachment"),
|
||||
required=False,
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT,
|
||||
help_text=_('Sending an attachment increases the chance of your email not arriving or being sorted into spam folders. We recommend only using PDFs '
|
||||
'of no more than 2 MB in size.'),
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
event = self.event = kwargs.pop('event')
|
||||
context_parameters = kwargs.pop('context_parameters')
|
||||
request = kwargs.pop('request')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['subject'] = I18nFormField(
|
||||
label=_('Subject'),
|
||||
@@ -79,6 +72,16 @@ class BaseMailForm(FormPlaceholderMixin, forms.Form):
|
||||
widget=I18nMarkdownTextarea, required=True,
|
||||
locales=event.settings.get('locales'),
|
||||
)
|
||||
self.fields['attachment'] = CachedFileField(
|
||||
label=_("Attachment"),
|
||||
required=False,
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT,
|
||||
help_text=_(
|
||||
'Sending an attachment increases the chance of your email not arriving or being sorted into spam folders. We recommend only using PDFs '
|
||||
'of no more than 2 MB in size.'),
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT,
|
||||
request=request,
|
||||
)
|
||||
self._set_field_placeholders('subject', context_parameters, rich=False)
|
||||
self._set_field_placeholders('message', context_parameters, rich=True)
|
||||
|
||||
|
||||
@@ -157,6 +157,7 @@ class BaseSenderView(EventPermissionRequiredMixin, FormView):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['event'] = self.request.event
|
||||
kwargs['context_parameters'] = self.context_parameters
|
||||
kwargs['request'] = self.request
|
||||
if 'from_log' in self.request.GET:
|
||||
try:
|
||||
from_log_id = self.request.GET.get('from_log')
|
||||
|
||||
@@ -873,6 +873,28 @@ class QuestionsStep(CartQuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
'attendee_name_parts': d
|
||||
})
|
||||
|
||||
wd = self.cart_session.get('widget_data', {})
|
||||
if wd.get('attendee-fix', '') == 'true':
|
||||
for k, v in wd.items():
|
||||
if v and k.startswith('attendee-name'):
|
||||
o.append({
|
||||
'attendee_name_parts': {
|
||||
'disabled': True,
|
||||
}
|
||||
})
|
||||
elif v and k.startswith('email'):
|
||||
o.append({
|
||||
'attendee_email': {
|
||||
'disabled': True,
|
||||
}
|
||||
})
|
||||
elif v and k.startswith('question-'):
|
||||
o.append({
|
||||
k[9:].upper(): {
|
||||
'disabled': True,
|
||||
}
|
||||
})
|
||||
|
||||
return o
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -23,7 +23,11 @@
|
||||
{% endblocktrans %} ::
|
||||
{% endif %}
|
||||
{% elif subevent %}
|
||||
{{ subevent.get_date_range_display }} ::
|
||||
{% if subevent.name|upper != event.name|upper %}
|
||||
{# The |upper is a trick to force LazyI18nString→str conversion before comparison #}
|
||||
{{ subevent.name }} ::
|
||||
{% endif %}
|
||||
{{ subevent.get_date_range_display_with_times }} ::
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ from pretix.base.models import Customer, InvoiceAddress, Order, OrderPosition
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import customer_created, customer_signed_in
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.compat import CompatDeleteView
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
from pretix.multidomain.models import KnownDomain
|
||||
@@ -280,6 +281,11 @@ class SetPasswordView(FormView):
|
||||
|
||||
def form_valid(self, form):
|
||||
with transaction.atomic():
|
||||
# Re-check token in transaction to prevent race condition
|
||||
self.customer = Customer.objects.select_for_update(of=OF_SELF).get(pk=self.customer.pk)
|
||||
if not TokenGenerator().check_token(self.customer, self.request.GET.get('token', '')):
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
self.customer.set_password(form.cleaned_data['password'])
|
||||
self.customer.is_verified = True
|
||||
self.customer.save()
|
||||
|
||||
@@ -170,13 +170,14 @@ body.has-modal-dialog .container, body.has-modal-dialog #wrapper {
|
||||
#lightbox-dialog {
|
||||
width: fit-content;
|
||||
max-width: 80%;
|
||||
min-width: 24em;
|
||||
min-width: calc(min(24em, 90%));
|
||||
.modal-card-content {
|
||||
padding: 2.5em;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: calc(100dvh - 60px - 5em - 5em);
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
@@ -1434,8 +1434,12 @@ def test_get_event_settings(token_client, organizer, event):
|
||||
'/api/v1/organizers/{}/events/{}/settings/'.format(organizer.slug, event.slug),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['imprint_url'] == "https://example.org"
|
||||
assert resp.data['contact_url'] == "https://example.org/contact"
|
||||
assert resp.data['imprint_url'] == {
|
||||
"en": "https://example.org",
|
||||
}
|
||||
assert resp.data['contact_url'] == {
|
||||
"en": "https://example.org/contact",
|
||||
}
|
||||
assert resp.data['seating_allow_blocked_seats_for_channel'] == []
|
||||
|
||||
resp = token_client.get(
|
||||
@@ -1443,7 +1447,9 @@ def test_get_event_settings(token_client, organizer, event):
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['imprint_url'] == {
|
||||
"value": "https://example.org",
|
||||
"value": {
|
||||
"en": "https://example.org",
|
||||
},
|
||||
"label": "Imprint URL",
|
||||
"help_text": "This should point e.g. to a part of your website that has your contact details and legal "
|
||||
"information.",
|
||||
@@ -1478,8 +1484,12 @@ def test_patch_event_settings(token_client, organizer, event, team):
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['contact_url'] == "https://example.com/contact"
|
||||
assert resp.data['imprint_url'] == "https://example.com"
|
||||
assert resp.data['contact_url'] == {
|
||||
"en": "https://example.com/contact",
|
||||
}
|
||||
assert resp.data['imprint_url'] == {
|
||||
"en": "https://example.com",
|
||||
}
|
||||
assert resp.data['seating_allow_blocked_seats_for_channel'] == ['web']
|
||||
assert not resp.data['reusable_media_active']
|
||||
event.settings.flush()
|
||||
@@ -1542,8 +1552,12 @@ def test_patch_event_settings(token_client, organizer, event, team):
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['contact_url'] == "https://example.org/contact"
|
||||
assert resp.data['imprint_url'] == "https://example.org"
|
||||
assert resp.data['contact_url'] == {
|
||||
"en": "https://example.org/contact",
|
||||
}
|
||||
assert resp.data['imprint_url'] == {
|
||||
"en": "https://example.org",
|
||||
}
|
||||
event.settings.flush()
|
||||
assert event.settings.contact_url == 'https://example.org/contact'
|
||||
assert event.settings.imprint_url == 'https://example.org'
|
||||
|
||||
@@ -25,6 +25,7 @@ from datetime import datetime
|
||||
import pytest
|
||||
from django.core.files.base import ContentFile
|
||||
from django_scopes import scopes_disabled
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from tests.const import SAMPLE_PNG
|
||||
|
||||
TEST_ORGANIZER_RES = {
|
||||
@@ -165,9 +166,11 @@ def test_patch_settings(token_client, organizer):
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['contact_url'] == 'https://example.org/contact'
|
||||
assert resp.data['contact_url'] == {
|
||||
'en': 'https://example.org/contact',
|
||||
}
|
||||
organizer.settings.flush()
|
||||
assert organizer.settings.contact_url == 'https://example.org/contact'
|
||||
assert organizer.settings.contact_url == LazyI18nString('https://example.org/contact')
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/settings/'.format(organizer.slug),
|
||||
|
||||
Reference in New Issue
Block a user