mirror of
https://github.com/pretix/pretix.git
synced 2026-09-13 16:14:40 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eb6932eef |
@@ -566,7 +566,7 @@ organizer level.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"region": "DE",
|
||||
"imprint_url": "https://pretix.eu",
|
||||
…
|
||||
}
|
||||
|
||||
@@ -579,14 +579,12 @@ organizer level.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"region":
|
||||
"imprint_url":
|
||||
{
|
||||
"value": "DE",
|
||||
"label": "Region",
|
||||
"value": "https://pretix.eu",
|
||||
"label": "Imprint URL",
|
||||
"readonly": false,
|
||||
"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)."
|
||||
"help_text": "This should point e.g. to a part of your website that has your contact details and legal information."
|
||||
}
|
||||
},
|
||||
…
|
||||
@@ -622,7 +620,7 @@ organizer level.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"region": "DE"
|
||||
"imprint_url": "https://example.org/imprint/"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -634,7 +632,7 @@ organizer level.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"region": "DE",
|
||||
"imprint_url": "https://example.org/imprint/",
|
||||
…
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -56,8 +56,8 @@ dependencies = [
|
||||
"django-querytagger==0.0.3",
|
||||
"django-redis==7.0.*",
|
||||
"django-scopes==2.1.*",
|
||||
"django-statici18n==2.8.*",
|
||||
"djangorestframework==3.18.*",
|
||||
"django-statici18n==2.7.*",
|
||||
"djangorestframework==3.17.*",
|
||||
"dnspython==2.8.*",
|
||||
"drf_ujson2==1.7.*",
|
||||
"geoip2==5.*",
|
||||
|
||||
@@ -2314,27 +2314,25 @@ DEFAULTS = {
|
||||
},
|
||||
'contact_url': {
|
||||
'default': None,
|
||||
'type': LazyI18nString,
|
||||
'form_class': I18nURLFormField,
|
||||
'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."),
|
||||
widget=I18nTextInput,
|
||||
),
|
||||
'serializer_class': I18nURLField,
|
||||
"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': LazyI18nString,
|
||||
'form_class': I18nURLFormField,
|
||||
'type': str,
|
||||
'form_class': forms.URLField,
|
||||
'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': I18nURLField,
|
||||
'serializer_class': serializers.URLField,
|
||||
},
|
||||
'privacy_url': {
|
||||
'default': None,
|
||||
|
||||
@@ -172,9 +172,7 @@ 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
|
||||
v = CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
|
||||
if not v.allowed_for_session(self.request):
|
||||
v = None
|
||||
return CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
|
||||
return v
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
@@ -246,11 +244,6 @@ 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
|
||||
|
||||
@@ -278,8 +271,6 @@ 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
|
||||
@@ -303,8 +294,6 @@ 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,7 +87,6 @@ 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
|
||||
@@ -135,7 +134,6 @@ 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,7 +64,6 @@ 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
|
||||
@@ -396,17 +395,15 @@ class Recover(TemplateView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if self.form.is_valid():
|
||||
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()
|
||||
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()
|
||||
messages.success(request, _('You can now login using your new password.'))
|
||||
user.log_action('pretix.control.auth.user.forgot_password.recovered')
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ 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
|
||||
@@ -194,7 +193,6 @@ 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()))
|
||||
@@ -220,7 +218,6 @@ 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()
|
||||
@@ -306,7 +303,5 @@ 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,11 +56,18 @@ 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'),
|
||||
@@ -72,16 +79,6 @@ 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,7 +157,6 @@ 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,28 +873,6 @@ 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
|
||||
|
||||
@@ -247,14 +247,15 @@ def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, v
|
||||
continue
|
||||
|
||||
if item.hidden_if_item_available:
|
||||
time_available = item.hidden_if_item_available.is_available()
|
||||
if item.hidden_if_item_available.has_variations:
|
||||
item._dependency_available = any(
|
||||
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)[0] == Quota.AVAILABILITY_OK
|
||||
# is_available on variant is evaluated called by available_variations
|
||||
for var in item.hidden_if_item_available.available_variations
|
||||
)
|
||||
) and time_available
|
||||
else:
|
||||
q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
||||
time_available = item.hidden_if_item_available.is_available()
|
||||
item._dependency_available = (q[0] == Quota.AVAILABILITY_OK) and time_available
|
||||
if item._dependency_available and item.hidden_if_item_available_mode == Item.UNAVAIL_MODE_HIDDEN:
|
||||
item._remove = True
|
||||
|
||||
@@ -54,7 +54,6 @@ 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
|
||||
@@ -281,11 +280,6 @@ 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,14 +170,13 @@ body.has-modal-dialog .container, body.has-modal-dialog #wrapper {
|
||||
#lightbox-dialog {
|
||||
width: fit-content;
|
||||
max-width: 80%;
|
||||
min-width: calc(min(24em, 90%));
|
||||
min-width: 24em;
|
||||
.modal-card-content {
|
||||
padding: 2.5em;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: calc(100dvh - 60px - 5em - 5em);
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
@@ -1434,12 +1434,8 @@ 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'] == {
|
||||
"en": "https://example.org",
|
||||
}
|
||||
assert resp.data['contact_url'] == {
|
||||
"en": "https://example.org/contact",
|
||||
}
|
||||
assert resp.data['imprint_url'] == "https://example.org"
|
||||
assert resp.data['contact_url'] == "https://example.org/contact"
|
||||
assert resp.data['seating_allow_blocked_seats_for_channel'] == []
|
||||
|
||||
resp = token_client.get(
|
||||
@@ -1447,9 +1443,7 @@ def test_get_event_settings(token_client, organizer, event):
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['imprint_url'] == {
|
||||
"value": {
|
||||
"en": "https://example.org",
|
||||
},
|
||||
"value": "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.",
|
||||
@@ -1484,12 +1478,8 @@ def test_patch_event_settings(token_client, organizer, event, team):
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['contact_url'] == {
|
||||
"en": "https://example.com/contact",
|
||||
}
|
||||
assert resp.data['imprint_url'] == {
|
||||
"en": "https://example.com",
|
||||
}
|
||||
assert resp.data['contact_url'] == "https://example.com/contact"
|
||||
assert resp.data['imprint_url'] == "https://example.com"
|
||||
assert resp.data['seating_allow_blocked_seats_for_channel'] == ['web']
|
||||
assert not resp.data['reusable_media_active']
|
||||
event.settings.flush()
|
||||
@@ -1552,12 +1542,8 @@ def test_patch_event_settings(token_client, organizer, event, team):
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['contact_url'] == {
|
||||
"en": "https://example.org/contact",
|
||||
}
|
||||
assert resp.data['imprint_url'] == {
|
||||
"en": "https://example.org",
|
||||
}
|
||||
assert resp.data['contact_url'] == "https://example.org/contact"
|
||||
assert resp.data['imprint_url'] == "https://example.org"
|
||||
event.settings.flush()
|
||||
assert event.settings.contact_url == 'https://example.org/contact'
|
||||
assert event.settings.imprint_url == 'https://example.org'
|
||||
|
||||
@@ -25,7 +25,6 @@ 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 = {
|
||||
@@ -166,11 +165,9 @@ def test_patch_settings(token_client, organizer):
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['contact_url'] == {
|
||||
'en': 'https://example.org/contact',
|
||||
}
|
||||
assert resp.data['contact_url'] == 'https://example.org/contact'
|
||||
organizer.settings.flush()
|
||||
assert organizer.settings.contact_url == LazyI18nString('https://example.org/contact')
|
||||
assert organizer.settings.contact_url == 'https://example.org/contact'
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/settings/'.format(organizer.slug),
|
||||
|
||||
@@ -744,6 +744,35 @@ class ItemDisplayTest(EventTestMixin, SoupTest):
|
||||
self.assertNotIn("SOLD OUT", doc.select("section:nth-of-type(1)")[0].text)
|
||||
self.assertIn("Late-bird", doc.select("section:nth-of-type(1)")[0].text)
|
||||
|
||||
def test_hidden_if_item_available_variation_unavailable_by_time(self):
|
||||
with scopes_disabled():
|
||||
q = Quota.objects.create(event=self.event, name='Early-bird', size=10)
|
||||
q2 = Quota.objects.create(event=self.event, name='Late-bird', size=10)
|
||||
item_with_vars = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=12)
|
||||
v = item_with_vars.variations.create(
|
||||
value='Regular', active=True,
|
||||
)
|
||||
item2 = Item.objects.create(event=self.event, name='Late-bird ticket', default_price=12,
|
||||
hidden_if_item_available=item_with_vars)
|
||||
q.items.add(item_with_vars)
|
||||
q.variations.add(v)
|
||||
q2.items.add(item2)
|
||||
|
||||
self.event.settings.hide_sold_out = True
|
||||
doc = self.get_doc('/%s/%s/' % (self.orga.slug, self.event.slug))
|
||||
self.assertIn("Early-bird", doc.select("section:nth-of-type(1)")[0].text)
|
||||
self.assertNotIn("SOLD OUT", doc.select("section:nth-of-type(1)")[0].text)
|
||||
self.assertNotIn("Late-bird", doc.select("section:nth-of-type(1)")[0].text)
|
||||
|
||||
item_with_vars.available_until = now() - datetime.timedelta(days=3)
|
||||
item_with_vars.available_until_mode = "hide"
|
||||
item_with_vars.save()
|
||||
|
||||
doc = self.get_doc('/%s/%s/' % (self.orga.slug, self.event.slug))
|
||||
self.assertNotIn("Early-bird", doc.select("section:nth-of-type(1)")[0].text)
|
||||
self.assertNotIn("SOLD OUT", doc.select("section:nth-of-type(1)")[0].text)
|
||||
self.assertIn("Late-bird", doc.select("section:nth-of-type(1)")[0].text)
|
||||
|
||||
def test_bundle_sold_out(self):
|
||||
with scopes_disabled():
|
||||
q = Quota.objects.create(event=self.event, name='Quota', size=2)
|
||||
|
||||
Reference in New Issue
Block a user