Compare commits

..

38 Commits

Author SHA1 Message Date
Richard Schreiber
60e7a81c05 fix display of attendee_name in list of memberships 2024-11-27 12:40:46 +01:00
Richard Schreiber
dd5b57d4a2 fix contrast for $text-muted on $table-bg-accent 2024-11-25 10:25:53 +01:00
Richard Schreiber
297971e81a unify label and title for view customer account 2024-11-25 09:54:24 +01:00
Richard Schreiber
faf64f0973 fix test 2024-11-22 13:29:16 +01:00
Richard Schreiber
1a39f209e9 fix code style 2024-11-22 13:24:24 +01:00
Richard Schreiber
6fc62dcaf5 use more icon-helper 2024-11-22 13:17:34 +01:00
Richard Schreiber
6df3c121b4 fix flake8 2024-11-22 12:57:22 +01:00
Richard Schreiber
d1a6ab89fe rename text-blob to textbubble 2024-11-22 12:56:22 +01:00
Richard Schreiber
3ec0fdb4d2 improve membership validity display 2024-11-22 12:41:39 +01:00
Richard Schreiber
20c14b8b24 remove unused code 2024-11-22 11:13:40 +01:00
Richard Schreiber
a2100c9295 checnge users to user 2024-11-22 11:13:27 +01:00
Richard Schreiber
094c04df73 remove impractical title attribute from icon templatetag 2024-11-22 11:13:18 +01:00
Richard Schreiber
de6f6025e2 update templates 2024-11-22 11:12:57 +01:00
Richard Schreiber
3a1f19fa51 add icon templatetag 2024-11-22 10:12:26 +01:00
Richard Schreiber
4bc1adc5e2 make icon optional in textblob 2024-11-21 15:35:07 +01:00
Richard Schreiber
2ca22ef663 use predefined border-radius instead of always fully round 2024-11-21 15:32:30 +01:00
Richard Schreiber
1fdf8cb01e make icons in text-bubble more vibrant (although less contrast) 2024-11-21 15:29:17 +01:00
Richard Schreiber
83b8cb3b4b change order-status to text-blob 2024-11-21 14:02:14 +01:00
Richard Schreiber
2fe5b65f94 change to status to textblob 2024-11-21 14:02:14 +01:00
Richard Schreiber
87d54ae068 improve addresses 2024-11-21 14:02:14 +01:00
Richard Schreiber
e3cd5af1d7 remove padding top/bottom if full-width-list is only element in panel 2024-11-21 14:02:14 +01:00
Richard Schreiber
b1fbf4d5b7 change ol to ul 2024-11-21 14:02:14 +01:00
Richard Schreiber
ce2e94b8d5 improve memberships list view 2024-11-21 14:02:14 +01:00
Richard Schreiber
ccf32ed2c1 remove unused path 2024-11-21 14:02:14 +01:00
Richard Schreiber
57bed6e6db improve subnav 2024-11-21 14:02:14 +01:00
Richard Schreiber
5a85ed49e8 add customer to context in CustomerRequiredMixin 2024-11-21 14:02:14 +01:00
Richard Schreiber
9e2aeaa400 move to individual pages with own paged querysets 2024-11-21 14:02:14 +01:00
Richard Schreiber
2442f2bfb5 rename to OrderView instead of IndexView 2024-11-21 14:02:14 +01:00
Richard Schreiber
2c03468ef5 use new tinted/shaded colors for text-blob 2024-11-21 14:01:56 +01:00
Richard Schreiber
53d80e56e6 improve status icon 2024-11-21 14:01:56 +01:00
Richard Schreiber
5fede841d7 move css from theme to main 2024-11-21 14:01:56 +01:00
Richard Schreiber
3c5ccaa1ba improve styling 2024-11-21 14:01:56 +01:00
Richard Schreiber
59dccaf680 change to pages instead of tabs 2024-11-21 14:01:34 +01:00
Richard Schreiber
3f499447da change table of order to list of cards 2024-11-21 14:00:51 +01:00
Richard Schreiber
4765dd5c9a change order-status info to text-blob 2024-11-21 14:00:51 +01:00
Richard Schreiber
8f7fca42e5 simplify account info panel 2024-11-21 14:00:51 +01:00
Richard Schreiber
5437bad1c1 add actions icons to login page 2024-11-21 13:59:33 +01:00
Richard Schreiber
3c16c5f66a add count of 2024-11-21 13:59:33 +01:00
71 changed files with 759 additions and 1882 deletions

View File

@@ -288,7 +288,6 @@ Example::
[django]
secret=j1kjps5a5&4ilpn912s7a1!e2h!duz^i3&idu@_907s$wrz@x-
debug=off
passwords_argon2=on
``secret``
The secret to be used by Django for signing and verification purposes. If this
@@ -304,10 +303,6 @@ Example::
.. WARNING:: Never set this to ``True`` in production!
``passwords_argon``
Use the ``argon2`` algorithm for password hashing. Disable on systems with a small number of CPU cores (currently
less than 8).
``profile``
Enable code profiling for a random subset of requests. Disabled by default, see
:ref:`perf-monitoring` for details.

View File

@@ -249,7 +249,7 @@ Endpoints
"orderposition": null,
"cartposition": null,
"voucher": null
}
},
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
@@ -260,114 +260,3 @@ Endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/seats/bulk_block/
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/seats/bulk_block/
Set the ``blocked`` attribute to ``true`` for a large number of seats at once.
You can pass either a list of ``id`` values or a list of ``seat_guid`` values.
You can pass up to 10,000 seats in one request.
The endpoint will return an error if you pass a seat ID that does not exist.
However, it will not return an error if one of the passed seats is already blocked or sold.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_block/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"ids": [12, 45, 56]
}
or
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_block/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"seat_guids": ["6c0e29e5-05d6-421f-99f3-afd01478ecad", "c2899340-e2e7-4d05-8100-000a4b6d7cf4"]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param subevent_id: The ``id`` field of the subevent to modify
:statuscode 200: no error
:statuscode 400: The seat could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/seats/bulk_unblock/
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/seats/bulk_unblock/
Set the ``blocked`` attribute to ``false`` for a large number of seats at once.
You can pass either a list of ``id`` values or a list of ``seat_guid`` values.
You can pass up to 10,000 seats in one request.
The endpoint will return an error if you pass a seat ID that does not exist.
However, it will not return an error if one of the passed seat is already unblocked or is sold.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_unblock/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"ids": [12, 45, 56]
}
or
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_unblock/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"seat_guids": ["6c0e29e5-05d6-421f-99f3-afd01478ecad", "c2899340-e2e7-4d05-8100-000a4b6d7cf4"]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param subevent_id: The ``id`` field of the subevent to modify
:statuscode 200: no error
:statuscode 400: The seat could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.

View File

@@ -53,7 +53,7 @@ dependencies = [
"django-phonenumber-field==7.3.*",
"django-redis==5.4.*",
"django-scopes==2.0.*",
"django-statici18n==2.6.*",
"django-statici18n==2.5.*",
"djangorestframework==3.15.*",
"dnspython==2.7.*",
"drf_ujson2==1.7.*",
@@ -76,7 +76,7 @@ dependencies = [
"phonenumberslite==8.13.*",
"Pillow==11.0.*",
"pretix-plugin-build",
"protobuf==5.29.*",
"protobuf==5.28.*",
"psycopg2-binary",
"pycountry",
"pycparser==2.22",
@@ -97,10 +97,10 @@ dependencies = [
"text-unidecode==1.*",
"tlds>=2020041600",
"tqdm==4.*",
"ua-parser==1.0.*",
"ua-parser==0.18.*",
"vat_moss_forked==2020.3.20.0.11.0",
"vobject==0.9.*",
"webauthn==2.3.*",
"webauthn==2.2.*",
"zeep==4.3.*"
]

View File

@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2024.12.0.dev0"
__version__ = "2024.11.0.dev0"

View File

@@ -989,40 +989,6 @@ def prefetch_by_id(items, qs, id_attr, target_attr):
setattr(item, target_attr, result.get(getattr(item, id_attr)))
class SeatBulkBlockInputSerializer(serializers.Serializer):
ids = serializers.ListField(child=serializers.IntegerField(), required=False, allow_empty=True)
seat_guids = serializers.ListField(child=serializers.CharField(), required=False, allow_empty=True)
def to_internal_value(self, data):
data = super().to_internal_value(data)
if data.get("seat_guids") and data.get("ids"):
raise ValidationError("Please pass either seat_guids or ids.")
if data.get("seat_guids"):
seat_ids = data["seat_guids"]
if len(seat_ids) > 10000:
raise ValidationError({"seat_guids": ["Please do not pass over 10000 seats."]})
seats = {s.seat_guid: s for s in self.context["queryset"].filter(seat_guid__in=seat_ids)}
for s in seat_ids:
if s not in seats:
raise ValidationError({"seat_guids": [f"The seat '{s}' does not exist."]})
elif data.get("ids"):
seat_ids = data["ids"]
if len(seat_ids) > 10000:
raise ValidationError({"ids": ["Please do not pass over 10000 seats."]})
seats = self.context["queryset"].in_bulk(seat_ids)
for s in seat_ids:
if s not in seats:
raise ValidationError({"ids": [f"The seat '{s}' does not exist."]})
else:
raise ValidationError("Please pass either seat_guids or ids.")
return {"seats": seats.values()}
class SeatSerializer(I18nAwareModelSerializer):
orderposition = serializers.IntegerField(source='orderposition_id')
cartposition = serializers.IntegerField(source='cartposition_id')

View File

@@ -40,7 +40,6 @@ from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import serializers, views, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import (
NotFound, PermissionDenied, ValidationError,
)
@@ -51,9 +50,8 @@ from pretix.api.auth.permission import EventCRUDPermission
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.event import (
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
EventSettingsSerializer, ItemMetaPropertiesSerializer,
SeatBulkBlockInputSerializer, SeatSerializer, SubEventSerializer,
TaxRuleSerializer,
EventSettingsSerializer, ItemMetaPropertiesSerializer, SeatSerializer,
SubEventSerializer, TaxRuleSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
@@ -239,9 +237,9 @@ class EventViewSet(viewsets.ModelViewSet):
disabled = {m: 'disabled' for m in current_plugins_value if m not in updated_plugins_value}
changed = merge_dicts(enabled, disabled)
for module, operation in changed.items():
for module, action in changed.items():
serializer.instance.log_action(
'pretix.event.plugins.' + operation,
'pretix.event.plugins.' + action,
user=self.request.user,
auth=self.request.auth,
data={'plugin': module}
@@ -746,24 +744,3 @@ class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
auth=self.request.auth,
data={"seats": [serializer.instance.pk]},
)
def bulk_change_blocked(self, blocked):
s = SeatBulkBlockInputSerializer(
data=self.request.data,
context={"event": self.request.event, "queryset": self.get_queryset()},
)
s.is_valid(raise_exception=True)
seats = s.validated_data["seats"]
for seat in seats:
seat.blocked = blocked
Seat.objects.bulk_update(seats, ["blocked"], batch_size=1000)
return Response({})
@action(methods=["POST"], detail=False)
def bulk_block(self, request, *args, **kwargs):
return self.bulk_change_blocked(True)
@action(methods=["POST"], detail=False)
def bulk_unblock(self, request, *args, **kwargs):
return self.bulk_change_blocked(False)

View File

@@ -35,7 +35,6 @@ from django.utils.translation import get_language, gettext_lazy as _
from pretix.base.models import Event
from pretix.base.signals import register_html_mail_renderers
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.helpers.format import SafeFormatter, format_map
from pretix.base.services.placeholders import ( # noqa
get_available_placeholders, PlaceholderContext
@@ -80,7 +79,7 @@ class BaseHTMLMailRenderer:
return self.identifier
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
position=None, context=None) -> str:
position=None) -> str:
"""
This method should generate the HTML part of the email.
@@ -89,7 +88,6 @@ class BaseHTMLMailRenderer:
:param subject: The email subject.
:param order: The order if this email is connected to one, otherwise ``None``.
:param position: The order position if this email is connected to one, otherwise ``None``.
:param context: Context to use to render placeholders in the plain body
:return: An HTML string
"""
raise NotImplementedError()
@@ -136,10 +134,8 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
def compile_markdown(self, plaintext):
return markdown_compile_email(plaintext)
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
def render(self, plain_body: str, plain_signature: str, subject: str, order, position) -> str:
body_md = self.compile_markdown(plain_body)
if context:
body_md = format_map(body_md, context=context, mode=SafeFormatter.MODE_RICH_TO_HTML)
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL,

View File

@@ -54,7 +54,6 @@ from django.core.validators import (
from django.db.models import QuerySet
from django.forms import Select, widgets
from django.forms.widgets import FILE_INPUT_CONTRADICTION
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.safestring import mark_safe
@@ -78,7 +77,7 @@ from pretix.base.i18n import (
get_babel_locale, get_language_without_region, language,
)
from pretix.base.models import InvoiceAddress, Item, Question, QuestionOption
from pretix.base.models.tax import ask_for_vat_id
from pretix.base.models.tax import VAT_ID_COUNTRIES, ask_for_vat_id
from pretix.base.services.tax import (
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
)
@@ -603,7 +602,6 @@ class BaseQuestionsForm(forms.Form):
questions = pos.item.questions_to_ask
event = kwargs.pop('event')
self.all_optional = kwargs.pop('all_optional', False)
self.attendee_addresses_required = event.settings.attendee_addresses_required and not self.all_optional
super().__init__(*args, **kwargs)
@@ -678,7 +676,7 @@ class BaseQuestionsForm(forms.Form):
if item.ask_attendee_data and event.settings.attendee_addresses_asked:
add_fields['street'] = forms.CharField(
required=self.attendee_addresses_required,
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('Address'),
widget=forms.Textarea(attrs={
'rows': 2,
@@ -688,7 +686,7 @@ class BaseQuestionsForm(forms.Form):
initial=(cartpos.street if cartpos else orderpos.street),
)
add_fields['zipcode'] = forms.CharField(
required=False,
required=event.settings.attendee_addresses_required and not self.all_optional,
max_length=30,
label=_('ZIP code'),
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
@@ -697,7 +695,7 @@ class BaseQuestionsForm(forms.Form):
}),
)
add_fields['city'] = forms.CharField(
required=False,
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('City'),
max_length=255,
initial=(cartpos.city if cartpos else orderpos.city),
@@ -709,12 +707,11 @@ class BaseQuestionsForm(forms.Form):
add_fields['country'] = CountryField(
countries=CachedCountries
).formfield(
required=self.attendee_addresses_required,
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('Country'),
initial=country,
widget=forms.Select(attrs={
'autocomplete': 'country',
'data-country-information-url': reverse('js_helpers.states'),
}),
)
c = [('', pgettext_lazy('address', 'Select state'))]
@@ -949,9 +946,9 @@ class BaseQuestionsForm(forms.Form):
d = super().clean()
if self.address_validation:
self.cleaned_data = d = validate_address(d, all_optional=not self.attendee_addresses_required)
self.cleaned_data = d = validate_address(d, True)
if d.get('street') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if not d.get('state'):
self.add_error('state', _('This field is required.'))
@@ -1008,7 +1005,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
'street': forms.Textarea(attrs={
'rows': 2,
'placeholder': _('Street and Number'),
'autocomplete': 'street-address',
'autocomplete': 'street-address'
}),
'beneficiary': forms.Textarea(attrs={'rows': 3}),
'country': forms.Select(attrs={
@@ -1024,7 +1021,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
'data-display-dependency': '#id_is_business_1',
'autocomplete': 'organization',
}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1', 'data-countries-with-vat-id': ','.join(VAT_ID_COUNTRIES)}),
'internal_reference': forms.TextInput,
}
labels = {
@@ -1058,7 +1055,6 @@ class BaseInvoiceAddressForm(forms.ModelForm):
])
self.fields['country'].choices = CachedCountries()
self.fields['country'].widget.attrs['data-country-information-url'] = reverse('js_helpers.states')
c = [('', pgettext_lazy('address', 'Select state'))]
fprefix = self.prefix + '-' if self.prefix else ''
@@ -1087,10 +1083,6 @@ class BaseInvoiceAddressForm(forms.ModelForm):
)
self.fields['state'].widget.is_required = True
self.fields['street'].required = False
self.fields['zipcode'].required = False
self.fields['city'].required = False
# Without JavaScript the VAT ID field is not hidden, so we empty the field if a country outside the EU is selected.
if cc and not ask_for_vat_id(cc) and fprefix + 'vat_id' in self.data:
self.data = self.data.copy()
@@ -1143,7 +1135,6 @@ class BaseInvoiceAddressForm(forms.ModelForm):
validate_address # local import to prevent impact on startup time
data = self.cleaned_data
if not data.get('is_business'):
data['company'] = ''
data['vat_id'] = ''
@@ -1151,11 +1142,9 @@ class BaseInvoiceAddressForm(forms.ModelForm):
data['vat_id'] = ''
if self.event.settings.invoice_address_required:
if data.get('is_business') and not data.get('company'):
raise ValidationError({"company": _('You need to provide a company name.')})
raise ValidationError(_('You need to provide a company name.'))
if not data.get('is_business') and not data.get('name_parts'):
raise ValidationError(_('You need to provide your name.'))
if not self.all_optional and 'street' in self.fields and not data.get('street') and not data.get('zipcode') and not data.get('city'):
raise ValidationError({"street": _('This field is required.')})
if 'vat_id' in self.changed_data or not data.get('vat_id'):
self.instance.vat_id_validated = False

View File

@@ -9,7 +9,6 @@ from decimal import Decimal
import django.core.validators
import django.db.models.deletion
import i18nfield.fields
from argon2.exceptions import HashingError
from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.db import migrations, models
@@ -26,14 +25,7 @@ def initial_user(apps, schema_editor):
user = User(email='admin@localhost')
user.is_staff = True
user.is_superuser = True
try:
user.password = make_password('admin')
except HashingError:
raise Exception(
"Could not hash password of initial user with argon2id. If this is a system with less than 8 CPU cores, "
"you might need to disable argon2id by setting `passwords_argon2=off` in the `[django]` section of the "
"pretix.cfg configuration file."
)
user.password = make_password('admin')
user.save()

View File

@@ -823,9 +823,6 @@ class Event(EventMixin, LoggedModel):
self.save()
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
if hasattr(other, 'alternative_domain_assignment'):
other.alternative_domain_assignment.domain.event_assignments.create(event=self)
if not self.all_sales_channels:
self.limit_sales_channels.set(
self.organizer.sales_channels.filter(

View File

@@ -2275,7 +2275,6 @@ class OrderFee(models.Model):
FEE_TYPE_SERVICE = "service"
FEE_TYPE_CANCELLATION = "cancellation"
FEE_TYPE_INSURANCE = "insurance"
FEE_TYPE_LATE = "late"
FEE_TYPE_OTHER = "other"
FEE_TYPE_GIFTCARD = "giftcard"
FEE_TYPES = (
@@ -2284,7 +2283,6 @@ class OrderFee(models.Model):
(FEE_TYPE_SERVICE, _("Service fee")),
(FEE_TYPE_CANCELLATION, _("Cancellation fee")),
(FEE_TYPE_INSURANCE, _("Insurance fee")),
(FEE_TYPE_LATE, _("Late fee")),
(FEE_TYPE_OTHER, _("Other fees")),
(FEE_TYPE_GIFTCARD, _("Gift card")),
)
@@ -3206,9 +3204,9 @@ class InvoiceAddress(models.Model):
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
name_parts = models.JSONField(default=dict)
street = models.TextField(verbose_name=_('Address'), blank=True)
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=True)
street = models.TextField(verbose_name=_('Address'), blank=False)
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
country = FastCountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
countries=CachedCountries)

View File

@@ -76,7 +76,7 @@ from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.services.tickets import get_tickets_for_order
from pretix.base.signals import email_filter, global_email_filter
from pretix.celery_app import app
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.format import format_map
from pretix.helpers.hierarkey import clean_filename
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.ical import get_private_icals
@@ -311,13 +311,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
try:
if plain_text_only:
body_html = None
elif 'context' in inspect.signature(renderer.render).parameters:
body_html = renderer.render(content_plain, signature, raw_subject, order, position, context)
elif 'position' in inspect.signature(renderer.render).parameters:
# Backwards compatibility
warnings.warn('Email renderer called without context argument because context argument is not '
'supported.',
DeprecationWarning)
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
else:
# Backwards compatibility
@@ -329,8 +323,6 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
logger.exception('Could not render HTML body')
body_html = None
body_plain = format_map(body_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
send_task = mail_send_task.si(
to=[email] if isinstance(email, str) else list(email),
cc=cc,
@@ -663,7 +655,7 @@ def render_mail(template, context):
if isinstance(template, LazyI18nString):
body = str(template)
if context:
body = format_map(body, context, mode=SafeFormatter.MODE_IGNORE_RICH)
body = format_map(body, context)
else:
tpl = get_template(template)
body = tpl.render(context)

View File

@@ -26,7 +26,6 @@ from decimal import Decimal
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -40,8 +39,7 @@ from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
from pretix.base.signals import (
register_mail_placeholders, register_text_placeholders,
)
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.helpers.format import PlainHtmlAlternativeString, SafeFormatter
from pretix.helpers.format import SafeFormatter
logger = logging.getLogger('pretix.base.services.placeholders')
@@ -109,91 +107,6 @@ class SimpleFunctionalTextPlaceholder(BaseTextPlaceholder):
return self._sample
class BaseRichTextPlaceholder(BaseTextPlaceholder):
"""
This is the base class for all placeholders which can render either to plain text
or to a rich HTML element.
"""
def __init__(self, identifier, args):
self._identifier = identifier
self._args = args
@property
def identifier(self):
return self._identifier
@property
def required_context(self):
return self._args
@property
def is_block(self):
return False
def render(self, context):
return PlainHtmlAlternativeString(
self.render_plain(**{k: context[k] for k in self._args}),
self.render_html(**{k: context[k] for k in self._args}),
self.is_block,
)
def render_html(self, **kwargs):
"""
HTML rendering of the placeholder. Should return "safe" HTML, i.e. everything needs to be
escaped.
"""
raise NotImplementedError
def render_plain(self, **kwargs):
"""
Plain text rendering of the placeholder.
"""
raise NotImplementedError
def render_sample(self, event):
return PlainHtmlAlternativeString(
self.render_sample_plain(event=event),
self.render_sample_html(event=event),
self.is_block,
)
def render_sample_html(self, event):
raise NotImplementedError
def render_sample_plain(self, event):
raise NotImplementedError
class SimpleButtonPlaceholder(BaseRichTextPlaceholder):
def __init__(self, identifier, args, url_func, text_func, sample_url_func, sample_text_func):
super().__init__(identifier, args)
self._url_func = url_func
self._text_func = text_func
self._sample_url_func = sample_url_func
self._sample_text_func = sample_text_func
def render_html(self, **context):
text = self._text_func(**{k: context[k] for k in self._args})
url = self._url_func(**{k: context[k] for k in self._args})
return f'<a href="{url}" class="button">{escape(text)}</a>'
def render_plain(self, **context):
text = self._text_func(**{k: context[k] for k in self._args})
url = self._url_func(**{k: context[k] for k in self._args})
return f'{text}: {url}'
def render_sample_html(self, event):
text = self._sample_text_func(event)
url = self._sample_url_func(event)
return f'<a href="{url}" class="button">{escape(text)}</a>'
def render_sample_plain(self, event):
text = self._sample_text_func(event)
url = self._sample_url_func(event)
return f'{text}: {url}'
class PlaceholderContext(SafeFormatter):
"""
Holds the contextual arguments and corresponding list of available placeholders for formatting
@@ -371,27 +284,6 @@ def base_placeholders(sender, **kwargs):
}
),
),
SimpleButtonPlaceholder(
'url_button', ['order', 'event'],
url_func=lambda order, event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_secret()
}
),
text_func=lambda order, event: _("View order details"),
sample_url_func=lambda event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'hash': '98kusd8ofsj8dnkd'
}
),
sample_text_func=lambda event: _("View order details"),
),
SimpleFunctionalTextPlaceholder(
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
@@ -456,27 +348,6 @@ def base_placeholders(sender, **kwargs):
}
),
),
SimpleButtonPlaceholder(
'url_button', ['event', 'position'],
url_func=lambda event, position: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
),
text_func=lambda event, position: _("View registration details"),
sample_url_func=lambda event: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'position': '123'
}
),
sample_text_func=lambda event: _("View registration details"),
),
SimpleFunctionalTextPlaceholder(
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
event,
@@ -732,8 +603,8 @@ def base_placeholders(sender, **kwargs):
class FormPlaceholderMixin:
def _set_field_placeholders(self, fn, base_parameters, rich=False):
placeholders = get_available_placeholders(self.event, base_parameters, rich=rich)
def _set_field_placeholders(self, fn, base_parameters):
placeholders = get_available_placeholders(self.event, base_parameters)
ht = format_placeholders_help_text(placeholders, self.event)
if self.fields[fn].help_text:
self.fields[fn].help_text += ' ' + str(ht)
@@ -744,7 +615,7 @@ class FormPlaceholderMixin:
)
def get_available_placeholders(event, base_parameters, rich=False):
def get_available_placeholders(event, base_parameters):
if 'order' in base_parameters:
base_parameters.append('invoice_address')
base_parameters.append('position_or_address')
@@ -753,35 +624,6 @@ def get_available_placeholders(event, base_parameters, rich=False):
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if isinstance(v, BaseRichTextPlaceholder) and not rich:
continue
if all(rp in base_parameters for rp in v.required_context):
params[v.identifier] = v
return params
def get_sample_context(event, context_parameters, rich=True):
context_dict = {}
lbl = _('This value will be replaced based on dynamic parameters.')
for k, v in get_available_placeholders(event, context_parameters, rich=rich).items():
sample = v.render_sample(event)
if isinstance(sample, PlainHtmlAlternativeString):
context_dict[k] = PlainHtmlAlternativeString(
sample.plain,
'<{el} class="placeholder placeholder-html" title="{title}">{html}</{el}>'.format(
el='div' if sample.is_block else 'span',
title=lbl,
html=sample.html,
)
)
elif str(sample).strip().startswith('* ') or str(sample).startswith(' '):
context_dict[k] = '<div class="placeholder" title="{}">{}</div>'.format(
lbl,
markdown_compile_email(str(sample))
)
else:
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
lbl,
escape(sample)
)
return context_dict

View File

@@ -131,9 +131,6 @@
text-align: left;
padding: 0;
}
.content table td.align-right {
text-align: right;
}
a.button {
display: inline-block;
@@ -181,9 +178,6 @@
pre, pre code {
white-space: pre-line;
}
.text-right, .content table td.text-right {
text-align: right;
}
{% if rtl %}
body {
@@ -192,9 +186,6 @@
.content {
text-align: right;
}
.text-right, .content table td.text-right {
text-align: left;
}
{% endif %}
{% block addcss %}{% endblock %}

View File

@@ -305,7 +305,6 @@ def markdown_compile_email(source, allowed_tags=ALLOWED_TAGS, allowed_attributes
source,
extensions=[
'markdown.extensions.sane_lists',
'markdown.extensions.tables',
EmailNl2BrExtension(),
LinkifyAndCleanExtension(
linker,

View File

@@ -22,30 +22,16 @@
import pycountry
from django.http import JsonResponse
from pretix.base.addressvalidation import (
COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED,
)
from pretix.base.models.tax import VAT_ID_COUNTRIES
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
def states(request):
cc = request.GET.get("country", "DE")
info = {
'street': {'required': True},
'zipcode': {'required': cc in COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED},
'city': {'required': cc in COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED},
'state': {'visible': cc in COUNTRIES_WITH_STATE_IN_ADDRESS, 'required': cc in COUNTRIES_WITH_STATE_IN_ADDRESS},
'vat_id': {'visible': cc in VAT_ID_COUNTRIES, 'required': False},
}
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
return JsonResponse({'data': [], **info, })
return JsonResponse({'data': []})
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
return JsonResponse({
'data': [
{'name': s.name, 'code': s.code[3:]}
for s in sorted(statelist, key=lambda s: s.name)
],
**info,
})
return JsonResponse({'data': [
{'name': s.name, 'code': s.code[3:]}
for s in sorted(statelist, key=lambda s: s.name)
]})

View File

@@ -35,7 +35,7 @@
# License for the specific language governing permissions and limitations under the License.
from decimal import Decimal
from urllib.parse import urlencode
from urllib.parse import urlencode, urlparse
from zoneinfo import ZoneInfo
import pycountry
@@ -76,10 +76,8 @@ from pretix.control.forms import (
)
from pretix.control.forms.widgets import Select2
from pretix.helpers.countries import CachedCountries
from pretix.multidomain.models import AlternativeDomainAssignment, KnownDomain
from pretix.multidomain.urlreverse import (
build_absolute_uri, get_organizer_domain,
)
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.banktransfer.payment import BankTransfer
from pretix.presale.style import get_fonts
@@ -365,9 +363,14 @@ class EventUpdateForm(I18nModelForm):
def __init__(self, *args, **kwargs):
self.change_slug = kwargs.pop('change_slug', False)
self.domain = kwargs.pop('domain', False)
kwargs.setdefault('initial', {})
self.instance = kwargs['instance']
if self.domain and self.instance:
initial_domain = self.instance.domains.first()
if initial_domain:
kwargs['initial'].setdefault('domain', initial_domain.domainname)
super().__init__(*args, **kwargs)
if not self.change_slug:
@@ -376,54 +379,48 @@ class EventUpdateForm(I18nModelForm):
self.fields['location'].widget.attrs['placeholder'] = _(
'Sample Conference Center\nHeidelberg, Germany'
)
try:
if self.domain:
self.fields['domain'] = forms.CharField(
max_length=255,
label=_('Domain'),
initial=self.instance.domain.domainname,
required=False,
disabled=True,
help_text=_('You can configure this in your organizer settings.')
)
except KnownDomain.DoesNotExist:
domain = get_organizer_domain(self.instance.organizer)
try:
current_domain_assignment = self.instance.alternative_domain_assignment
except AlternativeDomainAssignment.DoesNotExist:
current_domain_assignment = None
self.fields['domain'] = forms.ChoiceField(
label=_('Domain'),
help_text=_('You can add more domains in your organizer account.'),
choices=[('', _('Same as organizer account') + (f" ({domain})" if domain else ""))] + [
(d.domainname, d.domainname) for d in self.instance.organizer.domains.filter(mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
],
initial=current_domain_assignment.domain_id if current_domain_assignment else "",
label=_('Custom domain'),
required=False,
help_text=_('You need to configure the custom domain in the webserver beforehand.')
)
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
'data-inverse-dependency': '<[name$=all_sales_channels]',
}, choices=self.fields['limit_sales_channels'].widget.choices)
def clean_domain(self):
d = self.cleaned_data['domain']
if d:
if d == urlparse(settings.SITE_URL).hostname:
raise ValidationError(
_('You cannot choose the base domain of this installation.')
)
if KnownDomain.objects.filter(domainname=d).exclude(event=self.instance.pk).exists():
raise ValidationError(
_('This domain is already in use for a different event or organizer.')
)
return d
def save(self, commit=True):
instance = super().save(commit)
try:
current_domain_assignment = instance.alternative_domain_assignment
except AlternativeDomainAssignment.DoesNotExist:
current_domain_assignment = None
if self.cleaned_data['domain'] and not hasattr(instance, 'domain'):
domain = self.instance.organizer.domains.get(mode=KnownDomain.MODE_ORG_ALT_DOMAIN, domainname=self.cleaned_data["domain"])
AlternativeDomainAssignment.objects.update_or_create(
event=instance,
defaults={
"domain": domain,
}
)
instance.cache.clear()
elif current_domain_assignment:
current_domain_assignment.delete()
if self.domain:
current_domain = instance.domains.first()
if self.cleaned_data['domain']:
if current_domain and current_domain.domainname != self.cleaned_data['domain']:
current_domain.delete()
KnownDomain.objects.create(
organizer=instance.organizer, event=instance, domainname=self.cleaned_data['domain']
)
elif not current_domain:
KnownDomain.objects.create(
organizer=instance.organizer, event=instance, domainname=self.cleaned_data['domain']
)
elif current_domain:
current_domain.delete()
instance.cache.clear()
return instance
@@ -1385,7 +1382,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
self.event.meta_values_cached = self.event.meta_values.select_related('property').all()
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)
for k, v in list(self.fields.items()):
if k.endswith('_attendee') and not event.settings.attendee_emails_asked:

View File

@@ -133,108 +133,63 @@ class OrganizerDeleteForm(forms.Form):
class OrganizerUpdateForm(OrganizerForm):
def __init__(self, *args, **kwargs):
self.domain = kwargs.pop('domain', False)
self.change_slug = kwargs.pop('change_slug', False)
kwargs.setdefault('initial', {})
self.instance = kwargs['instance']
if self.domain and self.instance:
initial_domain = self.instance.domains.filter(event__isnull=True).first()
if initial_domain:
kwargs['initial'].setdefault('domain', initial_domain.domainname)
super().__init__(*args, **kwargs)
if not self.change_slug:
self.fields['slug'].widget.attrs['readonly'] = 'readonly'
if self.domain:
self.fields['domain'] = forms.CharField(
max_length=255,
label=_('Custom domain'),
required=False,
help_text=_('You need to configure the custom domain in the webserver beforehand.')
)
def clean_domain(self):
d = self.cleaned_data['domain']
if d:
if d == urlparse(settings.SITE_URL).hostname:
raise ValidationError(
_('You cannot choose the base domain of this installation.')
)
if KnownDomain.objects.filter(domainname=d).exclude(organizer=self.instance.pk,
event__isnull=True).exists():
raise ValidationError(
_('This domain is already in use for a different event or organizer.')
)
return d
def clean_slug(self):
if self.change_slug:
return self.cleaned_data['slug']
return self.instance.slug
def save(self, commit=True):
instance = super().save(commit)
class KnownDomainForm(forms.ModelForm):
class Meta:
model = KnownDomain
fields = ["domainname", "mode", "event"]
field_classes = {
"event": SafeModelChoiceField,
}
if self.domain:
current_domain = instance.domains.filter(event__isnull=True).first()
if self.cleaned_data['domain']:
if current_domain and current_domain.domainname != self.cleaned_data['domain']:
current_domain.delete()
KnownDomain.objects.create(organizer=instance, domainname=self.cleaned_data['domain'])
elif not current_domain:
KnownDomain.objects.create(organizer=instance, domainname=self.cleaned_data['domain'])
elif current_domain:
current_domain.delete()
instance.cache.clear()
for ev in instance.events.all():
ev.cache.clear()
def __init__(self, *args, **kwargs):
self.organizer = kwargs.pop('organizer')
super().__init__(*args, **kwargs)
self.fields["event"].queryset = self.organizer.events.all()
if self.instance and self.instance.pk:
self.fields["domainname"].widget.attrs['readonly'] = 'readonly'
def clean_domainname(self):
if self.instance and self.instance.pk:
return self.instance.domainname
d = self.cleaned_data['domainname']
if d:
if d == urlparse(settings.SITE_URL).hostname:
raise ValidationError(
_('You cannot choose the base domain of this installation.')
)
if KnownDomain.objects.filter(domainname=d).exclude(organizer=self.instance.organizer).exists():
raise ValidationError(
_('This domain is already in use for a different event or organizer.')
)
return d
def clean(self):
d = super().clean()
if d["mode"] == KnownDomain.MODE_ORG_DOMAIN and d["event"]:
raise ValidationError(
_("Do not choose an event for this mode.")
)
if d["mode"] == KnownDomain.MODE_ORG_ALT_DOMAIN and d["event"]:
raise ValidationError(
_("Do not choose an event for this mode. You can assign events to this domain in event settings.")
)
if d["mode"] == KnownDomain.MODE_EVENT_DOMAIN and not d["event"]:
raise ValidationError(
_("You need to choose an event.")
)
return d
class BaseKnownDomainFormSet(forms.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
self.organizer = kwargs.pop('organizer')
super().__init__(*args, **kwargs)
def _construct_form(self, i, **kwargs):
kwargs['organizer'] = self.organizer
return super()._construct_form(i, **kwargs)
@property
def empty_form(self):
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
use_required_attribute=False,
organizer=self.organizer,
)
self.add_fields(form, None)
return form
def clean(self):
super().clean()
data = [f.cleaned_data for f in self.forms]
if len([d for d in data if d.get("mode") == KnownDomain.MODE_ORG_DOMAIN and not d.get("DELETE")]) > 1:
raise ValidationError(_("You may set only one organizer domain."))
return data
KnownDomainFormset = inlineformset_factory(
Organizer, KnownDomain,
KnownDomainForm,
formset=BaseKnownDomainFormSet,
can_order=False, can_delete=True, extra=0
)
return instance
class SafeOrderPositionChoiceField(forms.ModelChoiceField):

View File

@@ -61,7 +61,6 @@
<script type="text/javascript" src="{% static "fileupload/jquery.fileupload.js" %}"></script>
<script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script>
<script type="text/javascript" src="{% static "are-you-sure/jquery.are-you-sure.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
{% endcompress %}
{{ html_head|safe }}

View File

@@ -22,7 +22,7 @@
{% bootstrap_field form.name layout="control" %}
{% bootstrap_field form.slug layout="control" %}
{% if form.domain %}
{% bootstrap_field form.domain layout="horizontal" %}
{% bootstrap_field form.domain layout="control" %}
{% endif %}
{% bootstrap_field form.date_from layout="control" %}
{% bootstrap_field form.date_to layout="control" %}

View File

@@ -46,13 +46,11 @@
<div id="cp{{ pos.id }}">
<div class="panel-body">
{% for form in forms %}
<div class="profile-scope">
{% if form.pos.item != pos.item %}
{# Add-Ons #}
<legend>+ {{ form.pos.item }}</legend>
{% endif %}
{% bootstrap_form form layout="control" %}
</div>
{% if form.pos.item != pos.item %}
{# Add-Ons #}
<legend>+ {{ form.pos.item }}</legend>
{% endif %}
{% bootstrap_form form layout="control" %}
{% endfor %}
</div>
</div>

View File

@@ -294,71 +294,6 @@
<legend>{% trans "Invoices" %}</legend>
{% bootstrap_field sform.invoice_regenerate_allowed layout="control" %}
</fieldset>
{% if domain_formset %}
<fieldset>
<legend>{% trans "Domains" %}</legend>
<div class="alert alert-warning">
{% trans "This dialog is intended for advanced users." %}
{% trans "The domain needs to be configured on your webserver before it can be used here." %}
</div>
<div class="formset" data-formset data-formset-prefix="{{ domain_formset.prefix }}">
{{ domain_formset.management_form }}
{% bootstrap_formset_errors domain_formset %}
<div data-formset-body>
{% for form in domain_formset %}
<div class="row formset-row" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-4">
{% bootstrap_field form.domainname layout='' form_group_class="" %}
{% bootstrap_form_errors form %}
</div>
<div class="col-md-3">
{% bootstrap_field form.mode layout='' form_group_class="" %}
</div>
<div class="col-md-3">
{% bootstrap_field form.event layout='' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<label aria-hidden="true">&nbsp;</label><br>
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="row formset-row" data-formset-form>
<div class="sr-only">
{{ domain_formset.empty_form.id }}
{% bootstrap_field domain_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-4">
{% bootstrap_field domain_formset.empty_form.domainname layout='' form_group_class="" %}
</div>
<div class="col-md-3">
{% bootstrap_field domain_formset.empty_form.mode layout='' form_group_class="" %}
</div>
<div class="col-md-3">
{% bootstrap_field domain_formset.empty_form.event layout='' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<label aria-hidden="true">&nbsp;</label><br>
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add domain" %}</button>
</p>
</fieldset>
{% endif %}
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -62,7 +62,7 @@ from django.http import (
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.html import conditional_escape
from django.utils.html import escape
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _, gettext_noop
@@ -100,12 +100,9 @@ from ...base.models.items import (
Item, ItemCategory, ItemMetaProperty, Question, Quota,
)
from ...base.services.mail import prefix_subject
from ...base.services.placeholders import get_sample_context
from ...base.settings import LazyI18nStringList
from ...helpers.compat import CompatDeleteView
from ...helpers.format import (
PlainHtmlAlternativeString, SafeFormatter, format_map,
)
from ...helpers.format import format_map
from ..logdisplay import OVERVIEW_BANLIST
from . import CreateView, PaginationMixin, UpdateView
@@ -242,6 +239,7 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
kwargs = super().get_form_kwargs()
if self.request.user.has_active_staff_session(self.request.session.session_key):
kwargs['change_slug'] = True
kwargs['domain'] = True
return kwargs
def post(self, request, *args, **kwargs):
@@ -719,7 +717,20 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
# get all supported placeholders with dummy values
def placeholders(self, item):
return get_sample_context(self.request.event, MailSettingsForm.base_context[item])
ctx = {}
for p in get_available_placeholders(self.request.event, MailSettingsForm.base_context[item]).values():
s = str(p.render_sample(self.request.event))
if s.strip().startswith('* '):
ctx[p.identifier] = '<div class="placeholder" title="{}">{}</div>'.format(
_('This value will be replaced based on dynamic parameters.'),
markdown_compile_email(s)
)
else:
ctx[p.identifier] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
escape(s)
)
return ctx
def post(self, request, *args, **kwargs):
preview_item = request.POST.get('item', '')
@@ -741,15 +752,9 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
bleach.clean(v), self.placeholders(preview_item), raise_on_missing=True
), highlight=True)
else:
placeholders = self.placeholders(preview_item)
msgs[self.supported_locale[idx]] = format_map(
markdown_compile_email(
format_map(v, placeholders, raise_on_missing=True)
),
placeholders,
mode=SafeFormatter.MODE_RICH_TO_HTML,
msgs[self.supported_locale[idx]] = markdown_compile_email(
format_map(v, self.placeholders(preview_item), raise_on_missing=True)
)
except ValueError:
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
PlaceholderValidator.error_message)
@@ -772,18 +777,13 @@ class MailSettingsRendererPreview(MailSettingsPreview):
# get all supported placeholders with dummy values
def placeholders(self, item):
ctx = {}
for p in get_available_placeholders(self.request.event, MailSettingsForm.base_context[item], rich=True).values():
sample = p.render_sample(self.request.event)
if isinstance(sample, PlainHtmlAlternativeString):
ctx[p.identifier] = sample
else:
ctx[p.identifier] = conditional_escape(sample)
for p in get_available_placeholders(self.request.event, MailSettingsForm.base_context[item]).values():
ctx[p.identifier] = escape(str(p.render_sample(self.request.event)))
return ctx
def get(self, request, *args, **kwargs):
v = str(request.event.settings.mail_text_order_placed)
context = self.placeholders('mail_text_order_placed')
v = format_map(v, context)
v = format_map(v, self.placeholders('mail_text_order_placed'))
renderers = request.event.get_html_mail_renderers()
if request.GET.get('renderer') in renderers:
with rolledback_transaction():
@@ -801,8 +801,7 @@ class MailSettingsRendererPreview(MailSettingsPreview):
str(request.event.settings.mail_text_signature),
gettext('Your order: %(code)s') % {'code': order.code},
order,
position=None,
context=context,
position=None
)
r = HttpResponse(v, content_type='text/html')
r._csp_ignore = True

View File

@@ -134,7 +134,7 @@ from pretix.control.signals import order_search_forms
from pretix.control.views import PaginationMixin
from pretix.helpers import OF_SELF
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.format import format_map
from pretix.helpers.safedownload import check_token
from pretix.presale.signals import question_form_fields
@@ -2351,7 +2351,7 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
'subject': mark_safe(_('Subject: {subject}').format(
subject=prefix_subject(order.event, escape(email_subject), highlight=True)
)),
'html': format_map(markdown_compile_email(email_content), email_context, mode=SafeFormatter.MODE_RICH_TO_HTML)
'html': markdown_compile_email(email_content)
}
return self.get(self.request, *self.args, **self.kwargs)
else:

View File

@@ -104,11 +104,11 @@ from pretix.control.forms.organizer import (
CustomerCreateForm, CustomerUpdateForm, DeviceBulkEditForm, DeviceForm,
EventMetaPropertyAllowedValueFormSet, EventMetaPropertyForm, GateForm,
GiftCardAcceptanceInviteForm, GiftCardCreateForm, GiftCardUpdateForm,
KnownDomainFormset, MailSettingsForm, MembershipTypeForm,
MembershipUpdateForm, OrganizerDeleteForm, OrganizerFooterLinkFormset,
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm,
ReusableMediumCreateForm, ReusableMediumUpdateForm, SalesChannelForm,
SSOClientForm, SSOProviderForm, TeamForm, WebHookForm,
MailSettingsForm, MembershipTypeForm, MembershipUpdateForm,
OrganizerDeleteForm, OrganizerFooterLinkFormset, OrganizerForm,
OrganizerSettingsForm, OrganizerUpdateForm, ReusableMediumCreateForm,
ReusableMediumUpdateForm, SalesChannelForm, SSOClientForm, SSOProviderForm,
TeamForm, WebHookForm,
)
from pretix.control.forms.rrule import RRuleForm
from pretix.control.logdisplay import OVERVIEW_BANLIST
@@ -122,7 +122,7 @@ from pretix.control.views.mailsetup import MailSettingsSetupView
from pretix.helpers import OF_SELF, GroupConcat
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.dicts import merge_dicts
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.format import format_map
from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.forms.customer import TokenGenerator
@@ -357,10 +357,9 @@ class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
highlight=True,
)
else:
placeholders = self.placeholders(preview_item)
msgs[self.supported_locale[idx]] = format_map(markdown_compile_email(
format_map(v, placeholders)
), placeholders, mode=SafeFormatter.MODE_RICH_TO_HTML)
msgs[self.supported_locale[idx]] = markdown_compile_email(
format_map(v, self.placeholders(preview_item))
)
return JsonResponse({
'item': preview_item,
@@ -448,10 +447,6 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
def get_object(self, queryset=None) -> Organizer:
return self.object
@cached_property
def domain_config(self):
return self.request.user.has_active_staff_session(self.request.session.session_key)
@cached_property
def sform(self):
return OrganizerSettingsForm(
@@ -466,8 +461,6 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
context = super().get_context_data(*args, **kwargs)
context['sform'] = self.sform
context['footer_links_formset'] = self.footer_links_formset
if self.domain_config:
context['domain_formset'] = self.domain_formset
return context
@transaction.atomic
@@ -490,8 +483,6 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
self.request.organizer.log_action('pretix.organizer.footerlinks.changed', user=self.request.user, data={
'data': self.footer_links_formset.cleaned_data
})
if self.domain_config and self.domain_formset.has_changed():
self._save_domain_config()
if form.has_changed():
self.request.organizer.log_action(
'pretix.organizer.changed',
@@ -502,22 +493,10 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)
def _save_domain_config(self):
for form in self.domain_formset.initial_forms:
if form.instance.pk and form.has_changed():
self.object.domains.get(pk=form.instance.pk).log_delete(self.request.user)
self.domain_formset.save()
for new_obj in self.domain_formset.new_objects:
new_obj.log_create(self.request.user)
for ch_obj, form in self.domain_formset.changed_objects:
ch_obj.log_create(self.request.user)
self.request.organizer.cache.clear()
for ev in self.request.organizer.events.all():
ev.cache.clear()
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
if self.request.user.has_active_staff_session(self.request.session.session_key):
kwargs['domain'] = True
kwargs['change_slug'] = True
return kwargs
@@ -529,7 +508,7 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
if form.is_valid() and self.sform.is_valid() and self.footer_links_formset.is_valid() and (not self.domain_config or self.domain_formset.is_valid()):
if form.is_valid() and self.sform.is_valid() and self.footer_links_formset.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
@@ -540,11 +519,6 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
organizer=self.object,
prefix="footer-links", instance=self.object)
@cached_property
def domain_formset(self):
return KnownDomainFormset(self.request.POST if self.request.method == "POST" else None, prefix="domains",
instance=self.object, organizer=self.object)
def save_footer_links_formset(self, obj):
self.footer_links_formset.save()

View File

@@ -50,7 +50,7 @@ from django.http import (
from django.shortcuts import redirect, render
from django.urls import resolve, reverse
from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -59,12 +59,12 @@ from django.views.generic import (
)
from django_scopes import scopes_disabled
from pretix.base.email import get_available_placeholders
from pretix.base.models import (
CartPosition, LogEntry, Voucher, WaitingListEntry,
)
from pretix.base.models.vouchers import generate_codes
from pretix.base.services.mail import prefix_subject
from pretix.base.services.placeholders import get_sample_context
from pretix.base.services.vouchers import vouchers_send
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.base.views.tasks import AsyncFormView
@@ -74,7 +74,7 @@ from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import voucher_form_class
from pretix.control.views import PaginationMixin
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.format import format_map
from pretix.helpers.models import modelcopy
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -549,10 +549,22 @@ class VoucherBulkMailPreview(EventPermissionRequiredMixin, View):
# get all supported placeholders with dummy values
def placeholders(self, item):
ctx = {}
base_ctx = ['event', 'name']
if item == 'send_message':
base_ctx += ['voucher_list']
ctx = get_sample_context(self.request.event, base_ctx)
for p in get_available_placeholders(self.request.event, base_ctx).values():
s = str(p.render_sample(self.request.event))
if s.strip().startswith('* ') or s.startswith(' '):
ctx[p.identifier] = '<div class="placeholder" title="{}">{}</div>'.format(
_('This value will be replaced based on dynamic parameters.'),
markdown_compile_email(s)
)
else:
ctx[p.identifier] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
escape(s)
)
return self.SafeDict(ctx)
def post(self, request, *args, **kwargs):
@@ -567,10 +579,9 @@ class VoucherBulkMailPreview(EventPermissionRequiredMixin, View):
highlight=True
)
else:
placeholders = self.placeholders(preview_item)
msgs["all"] = format_map(markdown_compile_email(
format_map(request.POST.get(preview_item), placeholders)
), placeholders, mode=SafeFormatter.MODE_RICH_TO_HTML)
msgs["all"] = markdown_compile_email(
format_map(request.POST.get(preview_item), self.placeholders(preview_item))
)
return JsonResponse({
'item': preview_item,

View File

@@ -25,29 +25,14 @@ from string import Formatter
logger = logging.getLogger(__name__)
class PlainHtmlAlternativeString:
def __init__(self, plain, html, is_block=False):
self.plain = plain
self.html = html
self.is_block = is_block
def __repr__(self):
return f"PlainHtmlAlternativeString('{self.plain}', '{self.html}')"
class SafeFormatter(Formatter):
"""
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
(b) does not allow any unwanted shenanigans like attribute access or format specifiers.
"""
MODE_IGNORE_RICH = 0
MODE_RICH_TO_PLAIN = 1
MODE_RICH_TO_HTML = 2
def __init__(self, context, raise_on_missing=False, mode=MODE_IGNORE_RICH):
def __init__(self, context, raise_on_missing=False):
self.context = context
self.raise_on_missing = raise_on_missing
self.mode = mode
def get_field(self, field_name, args, kwargs):
return self.get_value(field_name, args, kwargs), field_name
@@ -55,22 +40,14 @@ class SafeFormatter(Formatter):
def get_value(self, key, args, kwargs):
if not self.raise_on_missing and key not in self.context:
return '{' + str(key) + '}'
r = self.context[key]
if isinstance(r, PlainHtmlAlternativeString):
if self.mode == self.MODE_IGNORE_RICH:
return '{' + str(key) + '}'
elif self.mode == self.MODE_RICH_TO_PLAIN:
return r.plain
elif self.mode == self.MODE_RICH_TO_HTML:
return r.html
return r
return self.context[key]
def format_field(self, value, format_spec):
# Ignore format_spec
# Ignore format _spec
return super().format_field(value, '')
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_IGNORE_RICH):
def format_map(template, context, raise_on_missing=False):
if not isinstance(template, str):
template = str(template)
return SafeFormatter(context, raise_on_missing, mode=mode).format(template)
return SafeFormatter(context, raise_on_missing).format(template)

View File

@@ -8,17 +8,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-19 15:34+0000\n"
"PO-Revision-Date: 2024-11-25 18:00+0000\n"
"Last-Translator: Jakub Stribrny <kubajznik@users.noreply.translate.pretix.eu>"
"\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix/cs/>"
"\n"
"PO-Revision-Date: 2023-09-15 15:21+0000\n"
"Last-Translator: Michael <michael.happl@gmx.at>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix/cs/"
">\n"
"Language: cs\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
"X-Generator: Weblate 5.8.3\n"
"X-Generator: Weblate 5.0.1\n"
#: pretix/_base_settings.py:79
msgid "English"
@@ -12462,7 +12461,7 @@ msgstr ""
#: pretix/control/forms/checkin.py:176
msgid "Barcode"
msgstr "Čárový kód"
msgstr ""
#: pretix/control/forms/checkin.py:179
msgid "Check-in time"
@@ -12475,8 +12474,6 @@ msgstr "Typ check-inu"
#: pretix/control/forms/checkin.py:187
msgid "Allow check-in of unpaid order (if check-in list permits it)"
msgstr ""
"Povolit check-in pro nezaplacenou objednávku (pokud to seznam check-in "
"dovoluje)"
#: pretix/control/forms/checkin.py:191
msgid "Support for check-in questions"
@@ -12511,11 +12508,11 @@ msgstr "Časové pásmo akce"
#: pretix/control/forms/event.py:140
msgid "I don't want to specify taxes now"
msgstr "Přidat podrobnosti o daních později"
msgstr ""
#: pretix/control/forms/event.py:141
msgid "You can always configure tax rates later."
msgstr "Daňové sazby můžete nastavit kdykoliv."
msgstr ""
#: pretix/control/forms/event.py:145
msgid "Sales tax rate"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-19 15:34+0000\n"
"PO-Revision-Date: 2024-11-29 23:00+0000\n"
"PO-Revision-Date: 2024-11-19 15:15+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.8.4\n"
"X-Generator: Weblate 5.8.3\n"
#: pretix/_base_settings.py:79
msgid "English"
@@ -3799,7 +3799,7 @@ msgstr "Todos los productos (incluso los recién creados)"
#: pretix/base/models/checkin.py:56 pretix/plugins/badges/exporters.py:436
#: pretix/plugins/checkinlists/exporters.py:842
msgid "Limit to products"
msgstr "Límite a los productos"
msgstr "Limita a los productos"
#: pretix/base/models/checkin.py:60
msgid ""
@@ -9565,7 +9565,7 @@ msgstr ""
#: pretix/base/settings.py:1175
msgid "Introductory text"
msgstr "Texto de introducción"
msgstr "Texto introductorio"
#: pretix/base/settings.py:1176
msgid "Will be printed on every invoice above the invoice rows."
@@ -11397,7 +11397,7 @@ msgstr "Color de fondo de la página"
#: pretix/base/settings.py:2845
msgid "Use round edges"
msgstr "Utilizar bordes redondos"
msgstr "Utilice bordes redondos"
#: pretix/base/settings.py:2854
msgid ""
@@ -11433,7 +11433,7 @@ msgstr ""
#: pretix/base/settings.py:2899 pretix/base/settings.py:2941
msgid "Use header image in its full size"
msgstr "Utilizar la imagen del encabezado en su tamaño completo"
msgstr "Utilice la imagen del encabezado en su tamaño completo"
#: pretix/base/settings.py:2900 pretix/base/settings.py:2942
msgid "We recommend to upload a picture at least 1170 pixels wide."
@@ -12822,11 +12822,11 @@ msgstr "Zona horaria del evento"
#: pretix/control/forms/event.py:140
msgid "I don't want to specify taxes now"
msgstr "No quiero especificar los impuestos ahora"
msgstr ""
#: pretix/control/forms/event.py:141
msgid "You can always configure tax rates later."
msgstr "Siempre puede configurar los tipos impositivos más adelante."
msgstr ""
#: pretix/control/forms/event.py:145
msgid "Sales tax rate"
@@ -12879,8 +12879,6 @@ msgid ""
"You have not specified a tax rate. If you do not want us to compute sales "
"taxes, please check \"{field}\" above."
msgstr ""
"No ha especificado el tipo impositivo. Si no desea que calculemos los "
"impuestos sobre las ventas, marque «{field}» más arriba."
#: pretix/control/forms/event.py:308
msgid "Copy configuration from"
@@ -13067,12 +13065,13 @@ msgstr ""
#: pretix/control/forms/event.py:989 pretix/control/forms/organizer.py:534
msgid "Bcc address"
msgstr "Direcciones en copia oculta"
msgstr "Direcciones CCO"
#: pretix/control/forms/event.py:990 pretix/control/forms/organizer.py:535
msgid "All emails will be sent to this address as a Bcc copy"
msgstr ""
"Todos los correos electrónicos se enviarán a esta dirección en copia oculta"
"Todos los correos electrónicos se enviarán a esta dirección como una copia "
"de CCO"
#: pretix/control/forms/event.py:996 pretix/control/forms/organizer.py:541
msgid "Signature"
@@ -13649,7 +13648,7 @@ msgstr "Fecha de inicio"
#: pretix/control/forms/filter.py:1710 pretix/control/forms/filter.py:1713
#: pretix/control/forms/filter.py:2344
msgid "Date until"
msgstr "Fecha final"
msgstr "Fecha límite"
#: pretix/control/forms/filter.py:1218
msgid "Start time from"
@@ -16019,11 +16018,11 @@ msgstr ""
#: pretix/control/logdisplay.py:422
msgid "A custom email has been sent."
msgstr "Un email personalizado ha sido enviado."
msgstr "Un e-mail personalizado ha sido enviado."
#: pretix/control/logdisplay.py:423
msgid "A custom email has been sent to an attendee."
msgstr "Un email personalizado ha sido enviado al participante."
msgstr "Un e-mail personalizado ha sido enviado al participante."
#: pretix/control/logdisplay.py:424
msgid ""
@@ -16471,7 +16470,7 @@ msgstr "Un plugin ha sido desactivado."
#: pretix/control/logdisplay.py:533
msgid "The shop has been taken live."
msgstr "La tienda ha sido puesta en marcha."
msgstr "La tienda ha sido tomada en vivo."
#: pretix/control/logdisplay.py:534
msgid "The shop has been taken offline."
@@ -16950,11 +16949,11 @@ msgstr "Parametrizaciones globales"
#: pretix/control/navigation.py:440
msgid "Update check"
msgstr "Comprobación de actualizaciones"
msgstr "Verificación de actualización"
#: pretix/control/navigation.py:445
msgid "License check"
msgstr "Verificación de la licencia"
msgstr "Revisa de licencia"
#: pretix/control/navigation.py:450
msgid "System report"
@@ -17951,7 +17950,7 @@ msgstr "Regla de check-in personalizada"
#: pretix/control/templates/pretixcontrol/vouchers/bulk.html:117
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form.html:84
msgid "Edit"
msgstr "Editar"
msgstr "Tratar"
#: pretix/control/templates/pretixcontrol/checkin/list_edit.html:89
msgid "Visualize"
@@ -20027,8 +20026,8 @@ msgid ""
"For more information or to obtain a paid pretix Enterprise license, contact "
"support@pretix.eu."
msgstr ""
"Para obtener más información u obtener una licencia pretix Enterprise de "
"pago, comuníquese con support@pretix.eu."
"Para obtener más información u obtener una licencia pretix Enterprise paga, "
"comuníquese con support@pretix.eu."
#: pretix/control/templates/pretixcontrol/global_license.html:26
msgid "License settings and check"
@@ -20070,9 +20069,10 @@ msgid ""
"pretix support when your license renews. It may also be requested by pretix "
"support to aid debugging of problems."
msgstr ""
"Si dispone de una licencia de pretix Enterprise, deberá enviar este informe "
"al servicio de asistencia pretix cuando renueve su licencia. También puede "
"ser solicitado por el soporte pretix para ayudar a la solución de problemas."
"Si tienes una licencia de pretix de Enterprise, este informe se debe "
"entregar al equipo del servicio al cliente de pretix cuando tu licencia "
"renueve. También el servicio al cliente de pretix podría pedirlo para ayudar "
"durante depurar."
#: pretix/control/templates/pretixcontrol/global_sysreport.html:8
msgid ""
@@ -21286,11 +21286,11 @@ msgstr "Revocar"
#: pretix/control/templates/pretixcontrol/oauth/authorized.html:6
#: pretix/control/templates/pretixcontrol/user/settings.html:61
msgid "Authorized applications"
msgstr "Aplicaciones autorizadas"
msgstr "Solicitudes autorizadas"
#: pretix/control/templates/pretixcontrol/oauth/authorized.html:9
msgid "Manage your own apps"
msgstr "Gestione sus propias aplicaciones"
msgstr "Gestiona tus propias aplicaciones"
#: pretix/control/templates/pretixcontrol/oauth/authorized.html:18
msgid "Permissions"
@@ -31601,7 +31601,7 @@ msgstr ""
#: pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/form.html:14
msgid "Open Layout Designer"
msgstr "Abrir herramienta de diseño"
msgstr "Abrir diseñador de diseño"
#: pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/form.html:18
msgid "Advanced mode (multiple layouts)"

View File

@@ -4,7 +4,7 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-19 15:34+0000\n"
"PO-Revision-Date: 2024-11-29 23:00+0000\n"
"PO-Revision-Date: 2024-11-19 15:15+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
">\n"
@@ -13,7 +13,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.8.4\n"
"X-Generator: Weblate 5.8.3\n"
#: pretix/_base_settings.py:79
msgid "English"
@@ -416,9 +416,7 @@ msgstr "Une facture existe déjà pour cet ordre."
#: pretix/api/views/order.py:637 pretix/control/views/orders.py:1716
#: pretix/control/views/users.py:143
msgid "There was an error sending the mail. Please try again later."
msgstr ""
"Une erreur s'est produite lors de l'envoi de l'e-mail. Veuillez réessayer "
"plus tard."
msgstr "Il y a eu une erreur d'envoi du mail. Veuillez réessayer plus tard."
#: pretix/api/views/order.py:715 pretix/base/services/cart.py:215
#: pretix/base/services/orders.py:186 pretix/presale/views/order.py:799
@@ -1413,7 +1411,7 @@ msgstr "Adresse"
#: pretix/plugins/checkinlists/exporters.py:533
#: pretix/plugins/reports/exporters.py:841
msgid "ZIP code"
msgstr "Code postal"
msgstr "Code Postal"
#: pretix/base/exporters/invoices.py:209 pretix/base/exporters/invoices.py:217
#: pretix/base/exporters/invoices.py:335 pretix/base/exporters/invoices.py:343
@@ -1497,7 +1495,7 @@ msgstr "Destinataire de facture:"
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:83
#: pretix/presale/templates/pretixpresale/event/order.html:307
msgid "Company"
msgstr "Entreprise"
msgstr "Société"
#: pretix/base/exporters/invoices.py:215 pretix/base/exporters/invoices.py:341
msgid "Street address"
@@ -2136,7 +2134,7 @@ msgstr "Règle fiscale"
#: pretix/base/exporters/orderlist.py:644
#: pretix/base/exporters/orderlist.py:648 pretix/base/pdf.py:330
msgid "Invoice address name"
msgstr "Nom de l'adresse de facturation"
msgstr "Adresse de facturation"
#: pretix/base/exporters/orderlist.py:480
#: pretix/base/exporters/orderlist.py:683 pretix/base/models/orders.py:204
@@ -2235,7 +2233,7 @@ msgstr "Nom du participant"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:176
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:179
msgid "Attendee email"
msgstr "E-mail du participant"
msgstr "Adresse mail du participant"
#: pretix/base/exporters/orderlist.py:609 pretix/base/models/vouchers.py:312
#: pretix/control/templates/pretixcontrol/vouchers/bulk.html:5
@@ -2321,19 +2319,19 @@ msgstr "Add-on à la position ID"
#: pretix/base/exporters/orderlist.py:650 pretix/base/pdf.py:340
msgid "Invoice address street"
msgstr "Adresse de facturation (rue)"
msgstr "Adresse de facturation: rue"
#: pretix/base/exporters/orderlist.py:650 pretix/base/pdf.py:345
msgid "Invoice address ZIP code"
msgstr "Adresse de facturation (Code postal)"
msgstr "Adresse de facturation: code postal"
#: pretix/base/exporters/orderlist.py:650 pretix/base/pdf.py:350
msgid "Invoice address city"
msgstr "Adresse de facturation (ville)"
msgstr "Adresse de facturation : ville"
#: pretix/base/exporters/orderlist.py:651 pretix/base/pdf.py:360
msgid "Invoice address country"
msgstr "Adresse de facturation (pays)"
msgstr "Adresse de facturation : pays"
#: pretix/base/exporters/orderlist.py:652
msgctxt "address"
@@ -3589,7 +3587,7 @@ msgstr "Vous ne pouvez pas attribuer un poste secret qui existe déjà."
#: pretix/base/modelimport_orders.py:490
msgid "Please enter a valid language code."
msgstr "Veuillez saisir un code linguistique valide."
msgstr "Veuillez saisir un code de langue valide."
#: pretix/base/modelimport_orders.py:558 pretix/base/modelimport_orders.py:560
msgid "Please enter a valid sales channel."
@@ -3612,8 +3610,8 @@ msgstr "Aucun siège correspondant na été trouvé."
msgid ""
"The seat you selected has already been taken. Please select a different seat."
msgstr ""
"La place que vous avez choisie est déjà occupée. Veuillez choisir une autre "
"place."
"Le siège que vous avez sélectionné a déjà été pris. Veuillez sélectionner un "
"autre siège."
#: pretix/base/modelimport_orders.py:592 pretix/base/services/cart.py:209
msgid "You need to select a specific seat."
@@ -3731,7 +3729,7 @@ msgstr "Vous devez choisir le produit « {prod} » pour ce siège."
#: pretix/control/templates/pretixcontrol/vouchers/tags.html:42
#: pretix/control/views/vouchers.py:120
msgid "Tag"
msgstr "Balise"
msgstr "Tag"
#: pretix/base/modelimport_vouchers.py:334 pretix/base/models/vouchers.py:297
msgid "Shows hidden products that match this voucher"
@@ -3998,7 +3996,7 @@ msgstr ""
#: pretix/base/models/customers.py:310 pretix/base/models/orders.py:1534
#: pretix/base/models/orders.py:3204 pretix/base/settings.py:1108
msgid "Company name"
msgstr "Nom de l'entreprise"
msgstr "Nom de la société"
#: pretix/base/models/customers.py:314 pretix/base/models/orders.py:1538
#: pretix/base/models/orders.py:3211 pretix/base/settings.py:81
@@ -5782,7 +5780,7 @@ msgstr "expiré"
#: pretix/base/models/orders.py:253 pretix/control/forms/filter.py:560
#: pretix/control/templates/pretixcontrol/organizers/customer.html:64
msgid "Locale"
msgstr "Langue"
msgstr "Régionalisation"
#: pretix/base/models/orders.py:268 pretix/control/forms/filter.py:571
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:57
@@ -6068,7 +6066,7 @@ msgstr "Badge"
#: pretix/control/templates/pretixcontrol/checkin/checkins.html:66
#: pretix/plugins/ticketoutputpdf/ticketoutput.py:113
msgid "Ticket"
msgstr "Billet"
msgstr "Ticket"
#: pretix/base/models/orders.py:3405
msgid "Certificate"
@@ -6474,7 +6472,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/vouchers/index.html:6
#: pretix/control/templates/pretixcontrol/vouchers/index.html:8
msgid "Vouchers"
msgstr "Bons d'achat"
msgstr "Bons de réduction"
#: pretix/base/models/vouchers.py:339
msgid "You cannot select a quota that belongs to a different event."
@@ -7178,7 +7176,7 @@ msgstr "Jacques Martin"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:186
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:189
msgid "Attendee company"
msgstr "Entreprise du participant"
msgstr "Entreprise participante"
#: pretix/base/pdf.py:178 pretix/base/pdf.py:336
#: pretix/base/services/tickets.py:118 pretix/control/views/pdf.py:111
@@ -7322,7 +7320,7 @@ msgstr "Ville quelconque"
#: pretix/base/pdf.py:335
msgid "Invoice address company"
msgstr "Adresse de facturation de l'entreprise"
msgstr "Adresse de la facturation de l'entreprise"
#: pretix/base/pdf.py:341
msgid "Sesame Street 42"
@@ -7495,7 +7493,7 @@ msgstr "Nom du participant pour la formule de salutation"
#: pretix/base/services/placeholders.py:567
#: pretix/control/forms/organizer.py:612
msgid "Mr Doe"
msgstr "M. Doe"
msgstr "M. Dupont"
#: pretix/base/pdf.py:655 pretix/base/pdf.py:662
#: pretix/plugins/badges/exporters.py:501
@@ -9966,7 +9964,7 @@ msgstr ""
#: pretix/base/settings.py:1520
msgid "Allow users to download tickets"
msgstr "Autoriser les utilisateurs à télécharger les billets"
msgstr "Autoriser les utilisateurs à télécharger des billets"
#: pretix/base/settings.py:1521
msgid "If this is off, nobody can download a ticket."
@@ -12957,11 +12955,11 @@ msgstr "Fuseau horaire de l'événement"
#: pretix/control/forms/event.py:140
msgid "I don't want to specify taxes now"
msgstr "Je ne veux pas spécifier les taxes maintenant"
msgstr ""
#: pretix/control/forms/event.py:141
msgid "You can always configure tax rates later."
msgstr "Vous pouvez toujours configurer les taux d'imposition ultérieurement."
msgstr ""
#: pretix/control/forms/event.py:145
msgid "Sales tax rate"
@@ -13016,9 +13014,6 @@ msgid ""
"You have not specified a tax rate. If you do not want us to compute sales "
"taxes, please check \"{field}\" above."
msgstr ""
"Vous n'avez pas spécifié de taux d'imposition. Si vous ne souhaitez pas que "
"nous calculions les impôts sur les ventes, veuillez cocher \"{field}\" ci-"
"dessus."
#: pretix/control/forms/event.py:308
msgid "Copy configuration from"
@@ -13775,7 +13770,7 @@ msgstr "Prévente terminée"
#: pretix/control/forms/filter.py:2339
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html:84
msgid "Date from"
msgstr "Date à partir de"
msgstr "Date de début"
#: pretix/control/forms/filter.py:1211 pretix/control/forms/filter.py:1214
#: pretix/control/forms/filter.py:1710 pretix/control/forms/filter.py:1713
@@ -16986,7 +16981,7 @@ msgstr "Widget"
#: pretix/control/templates/pretixcontrol/event/payment.html:47
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:12
msgid "Settings"
msgstr "Paramètres"
msgstr "Réglages"
#: pretix/control/navigation.py:164
msgid "Categories"
@@ -17020,7 +17015,7 @@ msgstr "Tous les bons de réduction"
#: pretix/control/navigation.py:284
msgid "Tags"
msgstr "Balises"
msgstr "Tags"
#: pretix/control/navigation.py:296
msgctxt "navigation"
@@ -17856,7 +17851,7 @@ msgstr "Supprimer"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html:91
#: pretix/presale/templates/pretixpresale/fragment_event_list_filter.html:21
msgid "Filter"
msgstr "Filtrer"
msgstr "Filtre"
#: pretix/control/templates/pretixcontrol/checkin/checkins.html:50
msgid "Your search did not match any check-ins."
@@ -17917,7 +17912,7 @@ msgstr "Refusé"
#: pretix/control/templates/pretixcontrol/checkin/checkins.html:152
#: pretix/control/templates/pretixcontrol/event/index.html:24
msgid "Copy to clipboard"
msgstr "Copier dans le presse-papier"
msgstr "Copier dans le Presse-papiers"
#: pretix/control/templates/pretixcontrol/checkin/index.html:7
#: pretix/control/templates/pretixcontrol/checkin/index.html:11
@@ -18254,7 +18249,7 @@ msgstr "Voir tous les événements récents"
#: pretix/control/templates/pretixcontrol/dashboard.html:65
msgid "Your event series"
msgstr "Vos séries d'événements"
msgstr "Votre série d'événements"
#: pretix/control/templates/pretixcontrol/dashboard.html:81
msgid "View all event series"
@@ -21947,7 +21942,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html:229
msgid "Contact email"
msgstr "E-mail de contact"
msgstr "Email de contact"
#: pretix/control/templates/pretixcontrol/order/index.html:233
msgid ""
@@ -22209,21 +22204,21 @@ msgstr "Historique des commandes"
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/history.html:4
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/history.html:6
msgid "Email history"
msgstr "Historique de l'e-mail"
msgstr "Historique des e-mails"
#: pretix/control/templates/pretixcontrol/order/mail_history.html:33
msgid ""
"This email has been sent with an older version of pretix. We are therefore "
"not able to display it here accurately."
msgstr ""
"Cet e-mail a été envoyé avec une ancienne version de pretix. Nous ne sommes "
"Cet email a été envoyé avec une ancienne version de pretix. Nous ne sommes "
"donc pas en mesure de l'afficher correctement ici."
#: pretix/control/templates/pretixcontrol/order/mail_history.html:39
#: pretix/control/templates/pretixcontrol/order/mail_history.html:50
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/history.html:30
msgid "Subject:"
msgstr "Sujet :"
msgstr "Sujet:"
#: pretix/control/templates/pretixcontrol/order/pay.html:5
#: pretix/control/templates/pretixcontrol/order/pay.html:9
@@ -22862,7 +22857,7 @@ msgstr "Aperçu des données"
#: pretix/control/templates/pretixcontrol/orders/import_process.html:43
#: pretix/control/templates/pretixcontrol/vouchers/import_process.html:43
msgid "Import settings"
msgstr "Importer les paramètres"
msgstr "Paramétrages dimportation"
#: pretix/control/templates/pretixcontrol/orders/import_process.html:49
#: pretix/control/templates/pretixcontrol/vouchers/import_process.html:49
@@ -22870,13 +22865,13 @@ msgid ""
"The import will be performed regardless of your quotas, so it will be "
"possible to overbook your event using this option."
msgstr ""
"L'importation sera effectuée sans tenir compte de vos quotas, de sorte qu'il "
"sera possible de surbooker votre événement à l'aide de cette option."
"Limportation sera effectuée quels que soient vos quotas, il sera donc "
"possible de surréserver votre événement en utilisant cette option."
#: pretix/control/templates/pretixcontrol/orders/import_process.html:57
#: pretix/control/templates/pretixcontrol/vouchers/import_process.html:57
msgid "Perform import"
msgstr "Réaliser l'importation"
msgstr "Effectuer limportation"
#: pretix/control/templates/pretixcontrol/orders/import_start.html:10
#: pretix/control/templates/pretixcontrol/vouchers/import_start.html:10
@@ -22890,15 +22885,15 @@ msgid ""
"The uploaded file should be a CSV file with a header row. You will be able "
"to assign the meanings of the different columns in the next step."
msgstr ""
"Le fichier téléchargé doit être un fichier CSV avec une ligne d'en-tête. "
"Vous pourrez définir la signification des différentes colonnes à l'étape "
"Le fichier téléchargé doit être un fichier CSV avec une ligne den-tête. "
"Vous pourrez attribuer les significations des différentes colonnes à létape "
"suivante."
#: pretix/control/templates/pretixcontrol/orders/import_start.html:22
#: pretix/control/templates/pretixcontrol/vouchers/import_start.html:22
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html:46
msgid "Import file"
msgstr "Importer le fichier"
msgstr "Importer un fichier"
#: pretix/control/templates/pretixcontrol/orders/import_start.html:25
#: pretix/control/templates/pretixcontrol/vouchers/import_start.html:25
@@ -22913,7 +22908,7 @@ msgstr "Détecter automatiquement"
#: pretix/control/templates/pretixcontrol/orders/import_start.html:35
#: pretix/control/templates/pretixcontrol/vouchers/import_start.html:35
msgid "Start import"
msgstr "Lancer l'importation"
msgstr "Démarrer l'exportation"
#: pretix/control/templates/pretixcontrol/orders/index.html:14
msgid "Nobody ordered a ticket yet."
@@ -25526,7 +25521,7 @@ msgstr "Créer plusieurs bons"
#: pretix/control/templates/pretixcontrol/vouchers/bulk.html:12
msgid "Voucher codes"
msgstr "Codes de bons d'achat"
msgstr "Codes de réduction"
#: pretix/control/templates/pretixcontrol/vouchers/bulk.html:17
msgid "Prefix (optional)"
@@ -25705,7 +25700,7 @@ msgstr "Expiration"
#: pretix/control/templates/pretixcontrol/vouchers/index.html:183
#, python-format
msgid "Any product in quota \"%(quota)s\""
msgstr "Tout produit dans le quota « %(quota)s »"
msgstr "Tout produit dans le quota \"%(quota)s\""
#: pretix/control/templates/pretixcontrol/vouchers/index.html:200
msgid "Use as a template for new vouchers"
@@ -26816,7 +26811,9 @@ msgstr[1] ""
#: pretix/presale/views/order.py:1279 pretix/presale/views/order.py:1662
#: pretix/presale/views/order.py:1693
msgid "Unknown order code or not authorized to access this order."
msgstr "Code de commande inconnu ou non autorisé pour accéder à cette commande."
msgstr ""
"Code de commande inconnu ou utilisateur non autorisé à accéder à cette "
"commande."
#: pretix/control/views/orders.py:675 pretix/presale/views/order.py:1111
msgid "Ticket download is not enabled for this product."
@@ -29300,7 +29297,7 @@ msgstr "Date de téléchargement"
#: pretix/plugins/checkinlists/exporters.py:767
msgid "Upload time"
msgstr "Temps de chargement"
msgstr "Temps de téléchargement"
#: pretix/plugins/checkinlists/exporters.py:818
msgid "OK"
@@ -33147,7 +33144,7 @@ msgstr "Nous appliquons ce bon de réduction à votre panier..."
#: pretix/presale/templates/pretixpresale/event/fragment_cart_box.html:56
#: pretix/presale/templates/pretixpresale/event/fragment_voucher_form.html:26
msgid "Redeem voucher"
msgstr "Utiliser le bon d'achat"
msgstr "Utiliser bon d'achat"
#: pretix/presale/templates/pretixpresale/event/fragment_change_confirm.html:10
msgid "Change summary"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-19 15:34+0000\n"
"PO-Revision-Date: 2024-12-02 06:00+0000\n"
"PO-Revision-Date: 2024-11-15 20:00+0000\n"
"Last-Translator: Patrick Chilton <chpatrick@gmail.com>\n"
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix/"
"hu/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.8.4\n"
"X-Generator: Weblate 5.8.3\n"
#: pretix/_base_settings.py:79
msgid "English"
@@ -2796,9 +2796,12 @@ msgid "Reusable media"
msgstr ""
#: pretix/base/exporters/reusablemedia.py:35
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgctxt "export_category"
msgid "Reusable media"
msgstr ""
msgstr "Nincs dátum kiválasztva."
#: pretix/base/exporters/reusablemedia.py:36
msgid ""
@@ -3569,8 +3572,11 @@ msgid "Invalid option selected."
msgstr ""
#: pretix/base/modelimport_orders.py:658 pretix/base/modelimport_orders.py:666
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Ambiguous option selected."
msgstr ""
msgstr "Nincs dátum kiválasztva."
#: pretix/base/modelimport_orders.py:697 pretix/base/models/orders.py:238
#: pretix/control/forms/orders.py:686 pretix/control/forms/organizer.py:795
@@ -7714,7 +7720,8 @@ msgid ""
"You are receiving this email because someone placed an order for {event} for "
"you."
msgstr ""
"Ezt az emailt a következő eseményre való rendelésed kapcsán küldtük: {event}"
"Azért küldtük ezt az e-mailt mert valaki rendelt neked jegyet a következő "
"eseményre: {event}"
#: pretix/base/services/mail.py:282 pretix/base/services/mail.py:298
#, python-brace-format
@@ -8256,8 +8263,11 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:190
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Activate re-usable media"
msgstr ""
msgstr "Nincs dátum kiválasztva."
#: pretix/base/settings.py:191
msgid ""
@@ -9707,7 +9717,7 @@ msgid ""
msgstr ""
"Szia!\n"
"\n"
"A rendelésed sikeres volt a következő eseményre: {event}.\n"
"A rendelésed sikeres volt a következő eseményre: {event}\n"
"Mivel csak ingyenes termékeket rendeltél, nem kell fizetned.\n"
"\n"
"Módosíthatod a rendelésed részleteit és megtekintheted az állapotát a "
@@ -10247,7 +10257,7 @@ msgid ""
msgstr ""
"Szia!\n"
"\n"
"Elfogadtuk a rendelésedet a következő eseményre és örömmel várunk: {event}.\n"
"Elfogadtuk a rendelésedet a következő eseményre és örömmel várunk: {event}\n"
"Mivel csak ingyenes termékeket rendeltél, nem kell fizetned.\n"
"\n"
"Módosíthatod a rendelésed részleteit és megtekintheted az állapotát a "
@@ -12848,8 +12858,11 @@ msgid "Device status"
msgstr ""
#: pretix/control/forms/filter.py:2621
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Active devices"
msgstr ""
msgstr "Nincs dátum kiválasztva."
#: pretix/control/forms/filter.py:2622
msgid "Revoked devices"
@@ -13879,8 +13892,11 @@ msgid "Organizer short name"
msgstr "Szervező rövidített név"
#: pretix/control/forms/organizer.py:1092
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Allow access to reusable media"
msgstr ""
msgstr "Nincs dátum kiválasztva."
#: pretix/control/forms/organizer.py:1093
msgid ""
@@ -20955,8 +20971,11 @@ msgid ""
msgstr ""
#: pretix/control/templates/pretixcontrol/orders/index.html:291
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Select action"
msgstr ""
msgstr "Nincs dátum kiválasztva."
#: pretix/control/templates/pretixcontrol/orders/index.html:312
#: pretix/control/views/orders.py:335
@@ -21383,8 +21402,11 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html:188
#: pretix/control/templates/pretixcontrol/subevents/index.html:211
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Edit selected"
msgstr ""
msgstr "Nincs dátum kiválasztva."
#: pretix/control/templates/pretixcontrol/organizers/edit.html:12
msgid "Organizer settings"
@@ -22725,12 +22747,18 @@ msgid "Delete selected"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/index.html:214
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Activate selected"
msgstr "Aktiválás"
msgstr "Nincs dátum kiválasztva."
#: pretix/control/templates/pretixcontrol/subevents/index.html:217
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Deactivate selected"
msgstr "Deaktiválás"
msgstr "Nincs dátum kiválasztva."
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:4
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:6
@@ -25679,8 +25707,11 @@ msgid "Badge layout: %(name)s"
msgstr ""
#: pretix/plugins/badges/templates/pretixplugins/badges/edit.html:26
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Save & continue"
msgstr "Mentés és folytatás"
msgstr "Nincs dátum kiválasztva."
#: pretix/plugins/badges/templates/pretixplugins/badges/index.html:10
msgid "You haven't created any badge layouts yet."
@@ -28598,7 +28629,7 @@ msgstr ""
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_confirm.html:4
msgid "The total amount will be withdrawn from your credit card."
msgstr "Az összeg a kártyádról kerül levonásra."
msgstr ""
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_confirm.html:8
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_card.html:29
@@ -28943,7 +28974,7 @@ msgstr "Összes jegy letöltése (PDF)"
#: pretix/plugins/ticketoutputpdf/ticketoutput.py:66
msgid "Download ticket (PDF)"
msgstr "Jegy PDF letöltése"
msgstr ""
#: pretix/plugins/ticketoutputpdf/views.py:62
msgid "Default ticket layout"
@@ -29876,9 +29907,12 @@ msgstr "%(item)s, %(var)s hozzáadása a kosárhoz"
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html:370
#: pretix/presale/templates/pretixpresale/event/voucher.html:231
#: pretix/presale/templates/pretixpresale/event/voucher.html:385
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgctxt "checkbox"
msgid "Select"
msgstr "Kiválaszt"
msgstr "Nincs dátum kiválasztva."
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:205
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:350
@@ -31074,11 +31108,11 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/position.html:10
msgid "Your registration"
msgstr "A rendelésed"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/position.html:31
msgid "Your items"
msgstr "A tételeid"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/position.html:46
msgid "Additional information"
@@ -31394,8 +31428,11 @@ msgid "Social features"
msgstr ""
#: pretix/presale/templates/pretixpresale/fragment_modals.html:114
#, fuzzy
#| msgctxt "subevent"
#| msgid "No date selected."
msgid "Save selection"
msgstr "Kijelölés mentése"
msgstr "Nincs dátum kiválasztva."
#: pretix/presale/templates/pretixpresale/fragment_week_calendar.html:82
#, python-format
@@ -31722,7 +31759,7 @@ msgstr "Ismeretlen eseménykód vagy nincs jogod az eseményhez."
#: pretix/presale/views/event.py:902
msgctxt "subevent"
msgid "No date selected."
msgstr "Nincs időpont kiválasztva."
msgstr "Nincs dátum kiválasztva."
#: pretix/presale/views/event.py:905
msgctxt "subevent"

View File

@@ -8,16 +8,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-08 13:45+0000\n"
"PO-Revision-Date: 2024-11-28 06:00+0000\n"
"PO-Revision-Date: 2024-10-01 22:52+0000\n"
"Last-Translator: Patrick Chilton <chpatrick@gmail.com>\n"
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/"
"pretix-js/hu/>\n"
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-"
"js/hu/>\n"
"Language: hu\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.8.3\n"
"X-Generator: Weblate 5.7.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -779,7 +779,7 @@ msgstr "A kosár lejárt"
#: pretix/static/pretixpresale/js/ui/main.js:588
#: pretix/static/pretixpresale/js/ui/main.js:607
msgid "Time zone:"
msgstr "Időzona:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:598
msgid "Your local time:"

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-19 15:34+0000\n"
"PO-Revision-Date: 2024-11-24 01:00+0000\n"
"Last-Translator: gabriblas <github@unowen.simplelogin.com>\n"
"PO-Revision-Date: 2024-11-18 02:00+0000\n"
"Last-Translator: Damiano <estux@users.noreply.translate.pretix.eu>\n"
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix/"
"it/>\n"
"Language: it\n"
@@ -4424,9 +4424,6 @@ msgid ""
"If checked, an event can only be taken live if the property is set. In event "
"series, its always optional to set a value for individual dates"
msgstr ""
"Se selezionato, un evento può essere pubblicato solamente se la proprietà è "
"stata impostata. Per le serie di eventi, impostare un valore per le date di "
"singoli eventi è sempre opzionale"
#: pretix/base/models/event.py:1721 pretix/base/models/items.py:2211
msgid "Valid values"
@@ -4590,10 +4587,6 @@ msgid ""
"their own. They can only be bought in combination with a product that has "
"this category configured as a possible source for add-ons."
msgstr ""
"Se selezionato, i prodotti che appartengono a questa categoria non potranno "
"essere venduti separatamente. Potranno invece essere acquistati in "
"combinazione con un altro prodotto che indichi questa categoria come un add-"
"on valido."
#: pretix/base/models/items.py:114 pretix/base/models/items.py:159
#: pretix/control/forms/item.py:99
@@ -4601,25 +4594,21 @@ msgid "Normal category"
msgstr "Categoria normale"
#: pretix/base/models/items.py:115 pretix/control/forms/item.py:112
#, fuzzy
msgid "Normal + cross-selling category"
msgstr "Categoria normale + cross-selling"
msgstr ""
#: pretix/base/models/items.py:116 pretix/control/forms/item.py:107
#, fuzzy
msgid "Cross-selling category"
msgstr "Categoria cross-selling"
msgstr ""
#: pretix/base/models/items.py:124
msgid "Always show in cross-selling step"
msgstr "Visualizza sempre nello step di cross-selling (vendita aggiuntiva)"
msgstr ""
#: pretix/base/models/items.py:125
msgid ""
"Only show products that qualify for a discount according to discount rules"
msgstr ""
"Visualizza solamente prodotti su cui è applicabile uno sconto, in base alle "
"regole di sconto"
#: pretix/base/models/items.py:126
msgid "Only show if the cart contains one of the following products"
@@ -4627,7 +4616,7 @@ msgstr "Mostra solo se il carrello contiene uno dei seguenti prodotti"
#: pretix/base/models/items.py:129
msgid "Cross-selling condition"
msgstr "Condizione per il cross-selling (vendita aggiuntiva)"
msgstr ""
#: pretix/base/models/items.py:137
#, fuzzy
@@ -7167,7 +7156,7 @@ msgstr ""
#: pretix/base/pdf.py:500
msgid "Seat: row"
msgstr "Posto: fila"
msgstr ""
#: pretix/base/pdf.py:505
msgid "Seat: seat number"
@@ -7579,7 +7568,7 @@ msgstr ""
#: pretix/base/services/cart.py:210
msgid "Please select a valid seat."
msgstr "Si prega di selezionare un posto a sedere valido."
msgstr ""
#: pretix/base/services/cart.py:211
msgid "You can not select a seat for this position."
@@ -8026,7 +8015,7 @@ msgstr ""
#: pretix/base/services/modelimport.py:236
#, python-brace-format
msgid "Invalid data in row {row}: {message}"
msgstr "Dati invalidi alla linea {row}: {message}"
msgstr ""
#: pretix/base/services/modelimport.py:217
msgid "A voucher cannot be created without a code."

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-19 15:34+0000\n"
"PO-Revision-Date: 2024-11-21 12:58+0000\n"
"Last-Translator: Ryo <saremba@rami.io>\n"
"PO-Revision-Date: 2024-10-22 17:00+0000\n"
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n"
"Language: ja\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.8.3\n"
"X-Generator: Weblate 5.7.2\n"
#: pretix/_base_settings.py:79
msgid "English"
@@ -44,6 +44,7 @@ msgid "Catalan"
msgstr "カタルーニャ語"
#: pretix/_base_settings.py:85
#, fuzzy
msgid "Chinese (simplified)"
msgstr "中国語(簡体字)"
@@ -56,6 +57,7 @@ msgid "Czech"
msgstr "チェコ"
#: pretix/_base_settings.py:88
#, fuzzy
msgid "Danish"
msgstr "デンマーク語"
@@ -121,11 +123,11 @@ msgstr "ロシア語"
#: pretix/_base_settings.py:104
msgid "Slovak"
msgstr "スロバキア語"
msgstr ""
#: pretix/_base_settings.py:105
msgid "Swedish"
msgstr "スウェーデン語"
msgstr ""
#: pretix/_base_settings.py:106
msgid "Spanish"
@@ -282,7 +284,7 @@ msgstr ""
#: pretix/api/serializers/item.py:306
msgid "Only admission products can currently be personalized."
msgstr "現在、パーソナライズできるのは入場商品(チケット)のみです。"
msgstr ""
#: pretix/api/serializers/item.py:317
msgid ""
@@ -376,11 +378,11 @@ msgstr "このユーザーは既にチームへの参加を承認されていま
#: pretix/api/views/cart.py:209
msgid ""
"The specified voucher has already been used the maximum number of times."
msgstr "指定されたバウチャーは、すでに最大使用回数に達しています。"
msgstr ""
#: pretix/api/views/checkin.py:610 pretix/api/views/checkin.py:617
msgid "Medium connected to other event"
msgstr "他のイベントに関連付けられているメディアです"
msgstr ""
#: pretix/api/views/oauth.py:107 pretix/control/logdisplay.py:476
#, python-brace-format
@@ -467,7 +469,7 @@ msgstr "外部からの払い戻し"
#: pretix/api/webhooks.py:285
msgid "Refund of payment requested by customer"
msgstr "お客様から支払いの返金が要求されました"
msgstr ""
#: pretix/api/webhooks.py:289
#, fuzzy
@@ -541,8 +543,7 @@ msgstr "イベント情報のデータが削除されました"
msgid ""
"Product changed (including product added or deleted and including changes to "
"nested objects like variations or bundles)"
msgstr "製品が変更されました(製品の追加または削除、バリエーションやバンドルのような"
"ネストされたオブジェクトの変更を含む)"
msgstr ""
#: pretix/api/webhooks.py:354
#, fuzzy
@@ -566,7 +567,7 @@ msgstr "その金額がカードに請求されました。"
#: pretix/api/webhooks.py:370
msgid "Waiting list entry added"
msgstr "ウェイティングリストにエントリーが追加されました"
msgstr ""
#: pretix/api/webhooks.py:374
#, fuzzy
@@ -580,7 +581,7 @@ msgstr "その金額がカードに請求されました。"
#: pretix/api/webhooks.py:382
msgid "Waiting list entry received voucher"
msgstr "ウェイティングリストのエントリーがバウチャーを受け取りました"
msgstr ""
#: pretix/api/webhooks.py:386
#, fuzzy
@@ -607,15 +608,15 @@ msgstr "国"
#: pretix/plugins/banktransfer/payment.py:679
#: pretix/presale/forms/customer.py:140
msgid "This field is required."
msgstr "この項目は必須です。"
msgstr ""
#: pretix/base/addressvalidation.py:213
msgid "Enter a postal code in the format XXX."
msgstr "郵便番号をXXXの形式で入力してください。"
msgstr ""
#: pretix/base/addressvalidation.py:222 pretix/base/addressvalidation.py:224
msgid "Enter a postal code in the format XXXX."
msgstr "郵便番号をXXXXの形式で入力してください。"
msgstr ""
#: pretix/base/auth.py:146
#, python-brace-format
@@ -657,7 +658,7 @@ msgstr "パスワード"
#: pretix/base/auth.py:176 pretix/base/auth.py:183
msgid "Your password must contain both numeric and alphabetic characters."
msgstr "パスワードには数字とアルファベットの両方を含める必要があります。"
msgstr ""
#: pretix/base/auth.py:202 pretix/base/auth.py:212
#, python-format
@@ -665,8 +666,7 @@ msgid "Your password may not be the same as your previous password."
msgid_plural ""
"Your password may not be the same as one of your %(history_length)s previous "
"passwords."
msgstr[0] "パスワードは、過去%(history_length)s件のいずれかのパスワードと同じにすること"
"はできません。"
msgstr[0] ""
#: pretix/base/channels.py:168
msgid "Online shop"
@@ -674,14 +674,13 @@ msgstr "オンラインショップ"
#: pretix/base/channels.py:174
msgid "API"
msgstr "API"
msgstr ""
#: pretix/base/channels.py:175
msgid ""
"API sales channels come with no built-in functionality, but may be used for "
"custom integrations."
msgstr "API販売チャネルには組み込みの機能はありませんが、カスタムインテグレーションで"
"使用することができます。"
msgstr ""
#: pretix/base/context.py:45
#, python-brace-format
@@ -700,25 +699,25 @@ msgstr "ソースコード"
#: pretix/base/customersso/oidc.py:61
#, python-brace-format
msgid "Configuration option \"{name}\" is missing."
msgstr "設定オプション\"{name}\"がありません。"
msgstr ""
#: pretix/base/customersso/oidc.py:69 pretix/base/customersso/oidc.py:74
#, python-brace-format
msgid ""
"Unable to retrieve configuration from \"{url}\". Error message: \"{error}\"."
msgstr "\"{url}\"から設定を取得できませんでした。エラーメッセージ:\"{error}\"。"
msgstr ""
#: pretix/base/customersso/oidc.py:80 pretix/base/customersso/oidc.py:85
#: pretix/base/customersso/oidc.py:90 pretix/base/customersso/oidc.py:95
#: pretix/base/customersso/oidc.py:100 pretix/base/customersso/oidc.py:105
#, python-brace-format
msgid "Incompatible SSO provider: \"{error}\"."
msgstr "互換性のないSSOプロバイダー: \"{error}\"。"
msgstr ""
#: pretix/base/customersso/oidc.py:111
#, python-brace-format
msgid "You are not requesting \"{scope}\"."
msgstr "\"{scope}\" をリクエストしていません。"
msgstr ""
#: pretix/base/customersso/oidc.py:117
#, python-brace-format

View File

@@ -80,32 +80,28 @@ class MultiDomainMiddleware(MiddlewareMixin):
request.port = int(port) if port else None
request.host = domain
if domain == default_domain:
request.domain_mode = "system"
request.urlconf = "pretix.multidomain.maindomain_urlconf"
elif domain:
cached = cache.get('pretix_multidomain_instances_{}'.format(domain))
cached = cache.get('pretix_multidomain_instance_{}'.format(domain))
if cached is None:
try:
kd = KnownDomain.objects.select_related('organizer', 'event').get(domainname=domain) # noqa
orga = kd.organizer
event = kd.event
mode = kd.mode
except KnownDomain.DoesNotExist:
orga = False
event = False
mode = "system"
cache.set(
'pretix_multidomain_instances_{}'.format(domain),
(orga.pk if orga else None, event.pk if event else None, mode),
'pretix_multidomain_instance_{}'.format(domain),
(orga.pk if orga else None, event.pk if event else None),
3600
)
else:
orga, event, mode = cached
orga, event = cached
if mode == KnownDomain.MODE_EVENT_DOMAIN:
if event:
request.event_domain = True
request.domain_mode = KnownDomain.MODE_EVENT_DOMAIN
if isinstance(event, Event):
request.organizer = orga
request.event = event
@@ -114,18 +110,11 @@ class MultiDomainMiddleware(MiddlewareMixin):
request.event = Event.objects.select_related('organizer').get(pk=event)
request.organizer = request.event.organizer
request.urlconf = "pretix.multidomain.event_domain_urlconf"
elif mode == KnownDomain.MODE_ORG_ALT_DOMAIN:
elif orga:
request.organizer_domain = True
request.domain_mode = KnownDomain.MODE_ORG_ALT_DOMAIN
request.organizer = orga if isinstance(orga, Organizer) else Organizer.objects.get(pk=orga)
request.urlconf = "pretix.multidomain.organizer_alternative_domain_urlconf"
elif mode == KnownDomain.MODE_ORG_DOMAIN:
request.organizer_domain = True
request.domain_mode = KnownDomain.MODE_ORG_DOMAIN
request.organizer = orga if isinstance(orga, Organizer) else Organizer.objects.get(pk=orga)
request.urlconf = "pretix.multidomain.organizer_domain_urlconf"
elif settings.DEBUG or domain in LOCAL_HOST_NAMES:
request.domain_mode = "system"
request.urlconf = "pretix.multidomain.maindomain_urlconf"
else:
with scopes_disabled():

View File

@@ -1,71 +0,0 @@
# Generated by Django 4.2.16 on 2024-11-12 10:46
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0273_remove_checkinlist_auto_checkin_sales_channels"),
("pretixmultidomain", "0002_knowndomain_event"),
]
operations = [
migrations.CreateModel(
name="AlternativeDomainAssignment",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
],
),
migrations.AddField(
model_name="knowndomain",
name="mode",
field=models.CharField(default="organizer", max_length=255),
),
migrations.AlterField(
model_name="knowndomain",
name="event",
field=models.OneToOneField(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="domain",
to="pretixbase.event",
),
),
migrations.RunSQL(
sql="UPDATE pretixmultidomain_knowndomain SET mode = 'event' WHERE event_id IS NOT NULL",
reverse_sql=migrations.RunSQL.noop,
),
migrations.AddConstraint(
model_name="knowndomain",
constraint=models.UniqueConstraint(
condition=models.Q(("mode", "organizer")),
fields=("organizer",),
name="unique_organizer_domain",
),
),
migrations.AddField(
model_name="alternativedomainassignment",
name="domain",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="event_assignments",
to="pretixmultidomain.knowndomain",
),
),
migrations.AddField(
model_name="alternativedomainassignment",
name="event",
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="alternative_domain_assignment",
to="pretixbase.event",
),
),
]

View File

@@ -21,7 +21,6 @@
#
from django.core.cache import cache
from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django_scopes import scopes_disabled
@@ -29,134 +28,39 @@ from pretix.base.models import Event, Organizer
class KnownDomain(models.Model):
MODE_ORG_DOMAIN = "organizer"
MODE_ORG_ALT_DOMAIN = "organizer_alternative"
MODE_EVENT_DOMAIN = "event"
MODES = (
(MODE_ORG_DOMAIN, _("Organizer domain")),
(MODE_ORG_ALT_DOMAIN, _("Alternative organizer domain for a set of events")),
(MODE_EVENT_DOMAIN, _("Event domain")),
)
domainname = models.CharField(
max_length=255,
primary_key=True,
verbose_name=_("Domain name"),
)
mode = models.CharField(
max_length=255,
choices=MODES,
default=MODE_ORG_DOMAIN,
verbose_name=_("Mode"),
)
organizer = models.ForeignKey(
Organizer,
blank=True,
null=True,
related_name='domains',
on_delete=models.CASCADE
)
event = models.OneToOneField(
Event,
blank=True,
null=True,
related_name='domain',
on_delete=models.PROTECT,
verbose_name=_("Event"),
)
domainname = models.CharField(max_length=255, primary_key=True)
organizer = models.ForeignKey(Organizer, blank=True, null=True, related_name='domains', on_delete=models.CASCADE)
event = models.ForeignKey(Event, blank=True, null=True, related_name='domains', on_delete=models.PROTECT)
class Meta:
verbose_name = _("Known domain")
verbose_name_plural = _("Known domains")
constraints = [
models.UniqueConstraint(
fields=("organizer",),
name="unique_organizer_domain",
condition=Q(mode="organizer"),
),
]
ordering = ("-mode", "domainname")
def __str__(self):
return self.domainname
@scopes_disabled()
def save(self, *args, **kwargs):
if self.event:
self.mode = KnownDomain.MODE_EVENT_DOMAIN
elif self.mode == KnownDomain.MODE_EVENT_DOMAIN:
raise ValueError("Event domain needs event")
super().save(*args, **kwargs)
if self.event:
self.event.get_cache().clear()
try:
self.event.alternative_domain_assignment.delete()
except AlternativeDomainAssignment.DoesNotExist:
pass
elif self.organizer:
self.organizer.get_cache().clear()
for event in self.organizer.events.all():
event.get_cache().clear()
cache.delete('pretix_multidomain_organizer_{}'.format(self.domainname))
cache.delete('pretix_multidomain_instances_{}'.format(self.domainname))
cache.delete('pretix_multidomain_instance_{}'.format(self.domainname))
cache.delete('pretix_multidomain_event_{}'.format(self.domainname))
@scopes_disabled()
def delete(self, *args, **kwargs):
if self.event:
self.event.cache.clear()
self.event.get_cache().clear()
elif self.organizer:
self.organizer.cache.clear()
self.organizer.get_cache().clear()
for event in self.organizer.events.all():
event.cache.clear()
event.get_cache().clear()
cache.delete('pretix_multidomain_organizer_{}'.format(self.domainname))
cache.delete('pretix_multidomain_instances_{}'.format(self.domainname))
cache.delete('pretix_multidomain_instance_{}'.format(self.domainname))
cache.delete('pretix_multidomain_event_{}'.format(self.domainname))
super().delete(*args, **kwargs)
def _log_domain_action(self, user, data):
if self.event:
self.event.log_action(
'pretix.event.settings',
user=user,
data=data
)
else:
self.organizer.log_action(
'pretix.organizer.settings',
user=user,
data=data
)
def log_create(self, user):
self._log_domain_action(user, {'add_alt_domain': self.domainname} if self.mode == KnownDomain.MODE_ORG_ALT_DOMAIN else {'domain': self.domainname})
def log_delete(self, user):
self._log_domain_action(user, {'remove_alt_domain': self.domainname} if self.mode == KnownDomain.MODE_ORG_ALT_DOMAIN else {'domain': None})
class AlternativeDomainAssignment(models.Model):
domain = models.ForeignKey(
KnownDomain,
on_delete=models.CASCADE,
related_name="event_assignments",
)
event = models.OneToOneField(
Event,
related_name="alternative_domain_assignment",
on_delete=models.CASCADE,
)
@scopes_disabled()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.event.cache.clear()
cache.delete('pretix_multidomain_instances_{}'.format(self.domain_id))
cache.delete('pretix_multidomain_event_{}'.format(self.domain_id))
@scopes_disabled()
def delete(self, *args, **kwargs):
self.event.cache.clear()
cache.delete('pretix_multidomain_instances_{}'.format(self.domain_id))
cache.delete('pretix_multidomain_event_{}'.format(self.domain_id))
super().delete(*args, **kwargs)

View File

@@ -1,63 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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 importlib.util
from django.apps import apps
from django.urls import include, re_path
from pretix.multidomain.plugin_handler import plugin_event_urls
from pretix.presale.urls import event_patterns, locale_patterns
from pretix.presale.views import organizer
from pretix.urls import common_patterns
presale_patterns = [
re_path(r'', include((locale_patterns + [
re_path(r'^$', organizer.RedirectToOrganizerIndex.as_view(), name='organizer.alt.index'),
re_path(r'^(?P<event>[^/]+)/', include(event_patterns)),
], 'presale')))
]
raw_plugin_patterns = []
for app in apps.get_app_configs():
if hasattr(app, 'PretixPluginMeta'):
if importlib.util.find_spec(app.name + '.urls'):
urlmod = importlib.import_module(app.name + '.urls')
if hasattr(urlmod, 'event_patterns'):
patterns = plugin_event_urls(urlmod.event_patterns, plugin=app.name)
raw_plugin_patterns.append(
re_path(r'^(?P<event>[^/]+)/', include((patterns, app.label)))
)
if hasattr(urlmod, 'organizer_patterns'):
patterns = plugin_event_urls(urlmod.organizer_patterns, plugin=app.name)
raw_plugin_patterns.append(
re_path(r'', include((patterns, app.label)))
)
plugin_patterns = [
re_path(r'', include((raw_plugin_patterns, 'plugins')))
]
# The presale namespace comes last, because it contains a wildcard catch
urlpatterns = common_patterns + plugin_patterns + presale_patterns
handler404 = 'pretix.base.views.errors.page_not_found'
handler500 = 'pretix.base.views.errors.server_error'

View File

@@ -43,33 +43,28 @@ from pretix.base.models import Event, Organizer
from .models import KnownDomain
def get_event_domain(event, fallback=False, return_mode=False):
def get_event_domain(event, fallback=False, return_info=False):
assert isinstance(event, Event)
if not event.pk:
# Can happen on the "event deleted" response
return (None, None) if return_mode else None
suffix = ('_fallback' if fallback else '') + ('_mode' if return_mode else '')
return (None, None) if return_info else None
suffix = ('_fallback' if fallback else '') + ('_info' if return_info else '')
domain = getattr(event, '_cached_domain' + suffix, None) or event.cache.get('domain' + suffix)
if domain is None:
domain = None, None
if hasattr(event, 'alternative_domain_assignment'):
domain = event.alternative_domain_assignment.domain_id, KnownDomain.MODE_ORG_ALT_DOMAIN
elif fallback:
if fallback:
domains = KnownDomain.objects.filter(
Q(event=event, mode=KnownDomain.MODE_EVENT_DOMAIN) |
Q(organizer_id=event.organizer_id, event__isnull=True, mode=KnownDomain.MODE_ORG_DOMAIN)
Q(event=event) | Q(organizer_id=event.organizer_id, event__isnull=True)
)
domains_event = [d for d in domains if d.event_id == event.pk]
domains_org = [d for d in domains if not d.event_id]
if domains_event:
domain = domains_event[0].domainname, KnownDomain.MODE_EVENT_DOMAIN
domain = domains_event[0].domainname, "event"
elif domains_org:
domain = domains_org[0].domainname, KnownDomain.MODE_ORG_DOMAIN
domain = domains_org[0].domainname, "organizer"
else:
try:
domain = event.domain.domainname, KnownDomain.MODE_EVENT_DOMAIN
except KnownDomain.DoesNotExist:
domain = None, None
domains = event.domains.all()
domain = domains[0].domainname if domains else None, "event"
event.cache.set('domain' + suffix, domain or 'none')
setattr(event, '_cached_domain' + suffix, domain or 'none')
elif domain == 'none':
@@ -77,7 +72,7 @@ def get_event_domain(event, fallback=False, return_mode=False):
domain = None, None
else:
setattr(event, '_cached_domain' + suffix, domain)
return domain if return_mode else domain[0]
return domain if return_info or not isinstance(domain, tuple) else domain[0]
def get_organizer_domain(organizer):
@@ -86,7 +81,7 @@ def get_organizer_domain(organizer):
return None
domain = getattr(organizer, '_cached_domain', None) or organizer.cache.get('domain')
if domain is None:
domains = organizer.domains.filter(event__isnull=True, mode=KnownDomain.MODE_ORG_DOMAIN)
domains = organizer.domains.filter(event__isnull=True)
domain = domains[0].domainname if domains else None
organizer.cache.set('domain', domain or 'none')
organizer._cached_domain = domain or 'none'
@@ -136,8 +131,7 @@ def eventreverse(obj, name, kwargs=None):
:returns: An absolute or relative URL as a string
"""
from pretix.multidomain import (
event_domain_urlconf, maindomain_urlconf,
organizer_alternative_domain_urlconf, organizer_domain_urlconf,
event_domain_urlconf, maindomain_urlconf, organizer_domain_urlconf,
)
c = None
@@ -159,24 +153,17 @@ def eventreverse(obj, name, kwargs=None):
raise TypeError('obj should be Event or Organizer')
if event:
domain, domaintype = get_event_domain(obj, fallback=True, return_mode=True)
domain, domaintype = get_event_domain(obj, fallback=True, return_info=True)
else:
domain, domaintype = get_organizer_domain(organizer), KnownDomain.MODE_ORG_DOMAIN
domain, domaintype = get_organizer_domain(organizer), "organizer"
if domain:
if domaintype == KnownDomain.MODE_EVENT_DOMAIN and 'event' in kwargs:
if domaintype == "event" and 'event' in kwargs:
del kwargs['event']
if 'organizer' in kwargs:
del kwargs['organizer']
if domaintype == KnownDomain.MODE_EVENT_DOMAIN:
urlconf = event_domain_urlconf
elif domaintype == KnownDomain.MODE_ORG_ALT_DOMAIN:
urlconf = organizer_alternative_domain_urlconf
else:
urlconf = organizer_domain_urlconf
path = reverse(name, kwargs=kwargs, urlconf=urlconf)
path = reverse(name, kwargs=kwargs, urlconf=event_domain_urlconf if domaintype == "event" else organizer_domain_urlconf)
siteurlsplit = urlsplit(settings.SITE_URL)
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
domain = '%s:%d' % (domain, siteurlsplit.port)

View File

@@ -79,8 +79,8 @@ class BaseMailForm(FormPlaceholderMixin, forms.Form):
widget=I18nMarkdownTextarea, required=True,
locales=event.settings.get('locales'),
)
self._set_field_placeholders('subject', context_parameters, rich=False)
self._set_field_placeholders('message', context_parameters, rich=True)
self._set_field_placeholders('subject', context_parameters)
self._set_field_placeholders('message', context_parameters)
class WaitinglistMailForm(BaseMailForm):
@@ -382,7 +382,7 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
)
self._set_field_placeholders('subject', ['event', 'order', 'event_or_subevent'])
self._set_field_placeholders('template', ['event', 'order', 'event_or_subevent'], rich=True)
self._set_field_placeholders('template', ['event', 'order', 'event_or_subevent'])
choices = [(e, l) for e, l in Order.STATUS_CHOICE if e != 'n']
choices.insert(0, ('n__valid_if_pending', _('payment pending but already confirmed')))

View File

@@ -46,10 +46,12 @@ from django.shortcuts import get_object_or_404, redirect
from django.template.loader import get_template
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, ngettext
from django.views.generic import DeleteView, FormView, ListView, TemplateView
from pretix.base.email import get_available_placeholders
from pretix.base.i18n import LazyI18nString, language
from pretix.base.models import Checkin, LogEntry, Order, OrderPosition
from pretix.base.models.event import SubEvent
@@ -61,8 +63,7 @@ from pretix.plugins.sendmail.tasks import (
)
from ...base.services.mail import prefix_subject
from ...base.services.placeholders import get_sample_context
from ...helpers.format import SafeFormatter, format_map
from ...helpers.format import format_map
from ...helpers.models import modelcopy
from . import forms
from .models import Rule, ScheduledMail
@@ -190,15 +191,17 @@ class BaseSenderView(EventPermissionRequiredMixin, FormView):
if self.request.POST.get("action") != "send":
for l in self.request.event.settings.locales:
with language(l, self.request.event.settings.region):
context_dict = get_sample_context(self.request.event, self.context_parameters)
context_dict = {}
for k, v in get_available_placeholders(self.request.event, self.context_parameters).items():
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
escape(v.render_sample(self.request.event))
)
subject = bleach.clean(form.cleaned_data['subject'].localize(l), tags=set())
preview_subject = prefix_subject(self.request.event, format_map(subject, context_dict), highlight=True)
message = form.cleaned_data['message'].localize(l)
preview_text = format_map(
markdown_compile_email(format_map(message, context_dict)),
context_dict,
mode=SafeFormatter.MODE_RICH_TO_HTML,
)
preview_text = markdown_compile_email(format_map(message, context_dict))
self.output[l] = {
'subject': _('Subject: {subject}').format(subject=preview_subject),
@@ -600,6 +603,31 @@ class CreateRule(EventPermissionRequiredMixin, CreateView):
return super().form_invalid(form)
def form_valid(self, form):
self.output = {}
if self.request.POST.get("action") == "preview":
for l in self.request.event.settings.locales:
with language(l, self.request.event.settings.region):
context_dict = {}
for k, v in get_available_placeholders(self.request.event, ['event', 'order',
'position_or_address']).items():
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
escape(v.render_sample(self.request.event))
)
subject = bleach.clean(form.cleaned_data['subject'].localize(l), tags=set())
preview_subject = prefix_subject(self.request.event, format_map(subject, context_dict), highlight=True)
template = form.cleaned_data['template'].localize(l)
preview_text = markdown_compile_email(format_map(template, context_dict))
self.output[l] = {
'subject': _('Subject: {subject}').format(subject=preview_subject),
'html': preview_text,
}
return self.get(self.request, *self.args, **self.kwargs)
messages.success(self.request, _('Your rule has been created.'))
form.instance.event = self.request.event
@@ -657,15 +685,17 @@ class UpdateRule(EventPermissionRequiredMixin, UpdateView):
for lang in self.request.event.settings.locales:
with language(lang, self.request.event.settings.region):
placeholders = get_sample_context(self.request.event, ['event', 'order', 'position_or_address'])
placeholders = {}
for k, v in get_available_placeholders(self.request.event, ['event', 'order', 'position_or_address']).items():
placeholders[k] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
escape(v.render_sample(self.request.event))
)
subject = bleach.clean(self.object.subject.localize(lang), tags=set())
preview_subject = prefix_subject(self.request.event, format_map(subject, placeholders), highlight=True)
template = self.object.template.localize(lang)
preview_text = format_map(
markdown_compile_email(format_map(template, placeholders)),
placeholders,
mode=SafeFormatter.MODE_RICH_TO_HTML,
)
preview_text = markdown_compile_email(format_map(template, placeholders))
o[lang] = {
'subject': _('Subject: {subject}'.format(subject=preview_subject)),

View File

@@ -160,7 +160,7 @@ class BaseCheckoutFlowStep:
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
return eventreverse(self.request.event, 'presale:event.index', kwargs=kwargs)
else:
return prev.get_step_url(request) + '?dir=prev'
return prev.get_step_url(request)
def get_next_url(self, request):
n = self.get_next_applicable(request)
@@ -662,7 +662,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
if 'async_id' in request.GET and settings.HAS_CELERY:
return self.get_result(request)
if len(self.forms) == 0 and len(self.cross_selling_data) == 0 and self.is_completed(request):
return redirect(self.get_prev_url(request) if request.GET.get('dir') == 'prev' else self.get_next_url(request))
return redirect(self.get_next_url(request))
return TemplateFlowStep.get(self, request)
def _clean_category(self, form, category):
@@ -1076,8 +1076,8 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))
return False
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_addresses_required', as_type=bool) \
and (cp.street is None and cp.city is None and cp.country is None):
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_attendees_required', as_type=bool) \
and (cp.street is None or cp.city is None or cp.country is None):
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))
return False

View File

@@ -133,7 +133,6 @@ class InvoiceAddressForm(BaseInvoiceAddressForm):
class InvoiceNameForm(InvoiceAddressForm):
address_validation = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@@ -13,7 +13,7 @@
</p>
{% elif incomplete %}
<div class="alert alert-danger">
{% trans "A product in your cart is only sold in combination with add-on products that are not available. Please contact the event organizer." %}
{% trans "A product in your cart is only sold in combination with add-on products that are no longer available. Please contact the event organizer." %}
</div>
{% endif %}
<form class="form-horizontal" method="post" data-asynctask

View File

@@ -10,12 +10,12 @@
<section aria-labelledby="{{ form_prefix }}category-{{ category.id }}"{% if category.description %} aria-describedby="{{ form_prefix }}category-info-{{ category.id }}"{% endif %}>
<h{{ headline_level|default:3 }} class="h3" id="{{ form_prefix }}category-{{ category.id }}">{{ category.name }}
{% if category.subevent_name %}
<small class="text-muted"><i class="fa fa-calendar" aria-hidden="true"></i> {{ category.subevent_name }}</small>
<small class="text-muted"><i class="fa fa-calendar"></i> {{ category.subevent_name }}</small>
{% endif %}
{% if category.category_has_discount %}
<small class="text-success">
<i class="fa fa-star" aria-hidden="true"></i>
<span class="sr-only">{% trans "Congratulations!" %}</span>
<span class="sr-only">Congratulations!</span>
{% trans "Your order qualifies for a discount" %}
</small>
{% endif %}

View File

@@ -2,7 +2,7 @@
{% load date_fast %}
{% load calendarhead %}
<div class="table-responsive">
<table class="table table-calendar" role="grid">
<table class="table table-calendar">
<caption class="sr-only">{% trans "Calendar" %}</caption>
<thead>
<tr>
@@ -18,26 +18,7 @@
{% if day %}
<td class="day {% if day.events %}has-events{% else %}no-events{% endif %}"
data-date="{{ day.date|date_fast:"SHORT_DATE_FORMAT" }}">
<p>
{% if day.events %}
<a href="#selected-day" class="day-label event hidden-sm hidden-md hidden-lg">
<b aria-hidden="true">{{ day.day }}</b>
<time datetime="{{ day.date|date_fast:"Y-m-d" }}" class="sr-only">
{{ day.date|date_fast:"SHORT_DATE_FORMAT" }}
</time>
<span class="sr-only">
({% blocktrans trimmed count count=day.events|length %}
{{ count }} event
{% plural %}
{{ count }} events
{% endblocktrans %})
</span>
</a>
<time datetime="{{ day.date|date_fast:"Y-m-d" }}" class="hidden-xs">{{ day.day }}</time>
{% else %}
<time datetime="{{ day.date|date_fast:"Y-m-d" }}" class="day-label">{{ day.day }}</time>
{% endif %}
</p>
<p><time datetime="{{ day.date|date_fast:"Y-m-d" }}">{{ day.day }}</time></p>
<ul class="events">
{% for event in day.events %}
<li><a class="event {% if event.continued %}continued{% endif %} {% spaceless %}
@@ -130,7 +111,9 @@
{% endfor %}
</tr>
{% endfor %}
<tr class="selected-day hidden">
<td colspan="7"></td>
</tr>
</tbody>
</table>
<div id="selected-day" aria-live="polite" class="table-calendar hidden-sm hidden-md hidden-lg"></div>
</div>

View File

@@ -20,5 +20,4 @@
<script type="text/javascript" src="{% static "pretixpresale/js/ui/cart.js" %}"></script>
<script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/iframe.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
{% endcompress %}

View File

@@ -57,9 +57,8 @@ from pretix.base.middleware import LocaleMiddleware
from pretix.base.models import Customer, Event, Organizer
from pretix.base.timemachine import time_machine_now_assigned_from_request
from pretix.helpers.http import redirect_to_url
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import (
build_absolute_uri, get_event_domain, get_organizer_domain,
get_event_domain, get_organizer_domain,
)
from pretix.presale.signals import process_request, process_response
@@ -135,7 +134,7 @@ def update_customer_session_auth_hash(request, customer):
def add_customer_to_request(request):
if 'cross_domain_customer_auth' in request.GET and request.domain_mode in (KnownDomain.MODE_EVENT_DOMAIN, KnownDomain.MODE_ORG_ALT_DOMAIN):
if 'cross_domain_customer_auth' in request.GET and request.event_domain:
# The user is logged in on the main domain and now wants to take their session
# to a event-specific domain. We validate the one time token received via a
# query parameter and make sure we invalidate it right away. Then, we look up
@@ -259,12 +258,11 @@ def _detect_event(request, require_live=True, require_plugin=None):
url = resolve(request.path_info)
request_domain_mode = getattr(request, 'domain_mode', 'system')
try:
if request_domain_mode == KnownDomain.MODE_EVENT_DOMAIN:
if hasattr(request, 'event_domain'):
# We are on an event's custom domain
pass
elif request_domain_mode in (KnownDomain.MODE_ORG_DOMAIN, KnownDomain.MODE_ORG_ALT_DOMAIN):
elif hasattr(request, 'organizer_domain'):
# We are on an organizer's custom domain
if 'organizer' in url.kwargs and url.kwargs['organizer']:
if url.kwargs['organizer'] != request.organizer.slug:
@@ -279,20 +277,12 @@ def _detect_event(request, require_live=True, require_plugin=None):
organizer=request.organizer,
)
# If this event has a custom domain or is not available on this alt domain, send the user there
domain, domainmode = get_event_domain(request.event, fallback=False, return_mode=True)
if not domain and request_domain_mode == KnownDomain.MODE_ORG_ALT_DOMAIN:
path = request.get_full_path().split("/", 2)[-1]
r = redirect_to_url(build_absolute_uri(request.event, "presale:event.index") + path)
r['Access-Control-Allow-Origin'] = '*'
return r
elif domain and domain != request.host:
# If this event has a custom domain, send the user there
domain = get_event_domain(request.event)
if domain:
if request.port and request.port not in (80, 443):
domain = '%s:%d' % (domain, request.port)
if domainmode == KnownDomain.MODE_EVENT_DOMAIN:
path = request.get_full_path().split("/", 2)[-1]
else:
path = request.get_full_path()
path = request.get_full_path().split("/", 2)[-1]
r = redirect_to_url(urljoin('%s://%s' % (request.scheme, domain), path))
r['Access-Control-Allow-Origin'] = '*'
return r
@@ -309,14 +299,11 @@ def _detect_event(request, require_live=True, require_plugin=None):
request.organizer = request.event.organizer
# If this event has a custom domain, send the user there
domain, domainmode = get_event_domain(request.event, fallback=False, return_mode=True)
domain = get_event_domain(request.event)
if domain:
if request.port and request.port not in (80, 443):
domain = '%s:%d' % (domain, request.port)
if domainmode == KnownDomain.MODE_EVENT_DOMAIN:
path = request.get_full_path().split("/", 3)[-1]
else:
path = request.get_full_path().split("/", 2)[-1]
path = request.get_full_path().split("/", 3)[-1]
r = redirect_to_url(urljoin('%s://%s' % (request.scheme, domain), path))
r['Access-Control-Allow-Origin'] = '*'
return r
@@ -390,7 +377,6 @@ def _detect_event(request, require_live=True, require_plugin=None):
except Event.DoesNotExist:
try:
if hasattr(request, 'organizer_domain'):
# Redirect for case-insensitive event slug
event = request.organizer.events.get(
slug__iexact=url.kwargs['event'],
organizer=request.organizer,
@@ -402,7 +388,6 @@ def _detect_event(request, require_live=True, require_plugin=None):
return r
else:
if 'event' in url.kwargs and 'organizer' in url.kwargs:
# Redirect for case-insensitive event or organizer slug
event = Event.objects.select_related('organizer').get(
slug__iexact=url.kwargs['event'],
organizer__slug__iexact=url.kwargs['organizer']
@@ -418,7 +403,6 @@ def _detect_event(request, require_live=True, require_plugin=None):
raise Http404(_('The selected event was not found.'))
except Organizer.DoesNotExist:
if 'organizer' in url.kwargs:
# Redirect for case-insensitive organizer slug
try:
organizer = Organizer.objects.get(
slug__iexact=url.kwargs['organizer']

View File

@@ -89,7 +89,7 @@ class CheckoutView(View):
else:
previous_step = step
step.c_is_before = True
step.c_resolved_url = step.get_step_url(request) + '?dir=prev'
step.c_resolved_url = step.get_step_url(request)
raise Http404()
def redirect(self, url):

View File

@@ -77,7 +77,7 @@ class RedirectBackMixin:
self.redirect_field_name,
self.request.GET.get(self.redirect_field_name, '')
)
hosts = list(KnownDomain.objects.filter(organizer=self.request.organizer).values_list('domainname', flat=True))
hosts = list(KnownDomain.objects.filter(event__organizer=self.request.organizer).values_list('domainname', flat=True))
siteurlsplit = urlsplit(settings.SITE_URL)
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
hosts = ['%s:%d' % (h, siteurlsplit.port) for h in hosts]
@@ -168,7 +168,7 @@ class LogoutView(View):
return HttpResponseRedirect(next_page)
def get_next_page(self):
if getattr(self.request, 'domain_mode', 'system') in (KnownDomain.MODE_ORG_ALT_DOMAIN, KnownDomain.MODE_EVENT_DOMAIN):
if getattr(self.request, 'event_domain', False):
# After we cleared the cookies on this domain, redirect to the parent domain to clear cookies as well
next_page = eventreverse(self.request.organizer, 'presale:organizer.customer.logout', kwargs={})
if self.redirect_field_name in self.request.POST or self.redirect_field_name in self.request.GET:
@@ -188,7 +188,7 @@ class LogoutView(View):
self.redirect_field_name,
self.request.GET.get(self.redirect_field_name)
)
hosts = list(KnownDomain.objects.filter(organizer=self.request.organizer).values_list('domainname', flat=True))
hosts = list(KnownDomain.objects.filter(event__organizer=self.request.organizer).values_list('domainname', flat=True))
siteurlsplit = urlsplit(settings.SITE_URL)
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
hosts = ['%s:%d' % (h, siteurlsplit.port) for h in hosts]

View File

@@ -71,7 +71,7 @@ from pretix.helpers.formats.en.formats import (
)
from pretix.helpers.http import redirect_to_url
from pretix.helpers.thumb import get_thumbnail
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.forms.organizer import EventListFilterForm
from pretix.presale.ical import get_public_ical
from pretix.presale.views import OrganizerViewMixin
@@ -1305,8 +1305,3 @@ class OrganizerFavicon(View):
return redirect_to_url(get_thumbnail(icon_file, '32x32^', formats=settings.PILLOW_FORMATS_QUESTIONS_FAVICON).thumb.url)
else:
return redirect_to_url(static("pretixbase/img/favicon.ico"))
class RedirectToOrganizerIndex(View):
def get(self, *args, **kwargs):
return redirect_to_url(build_absolute_uri(self.request.organizer, "presale:organizer.index"))

View File

@@ -726,11 +726,7 @@ PASSWORD_HASHERS = [
# the HistoricPassword model will not be changed automatically. In case a serious issue with a hasher
# comes to light, dropping the contents of the HistoricPassword table might be the more risk-adequate
# decision.
*(
["django.contrib.auth.hashers.Argon2PasswordHasher"]
if config.getboolean('django', 'passwords_argon2', fallback=True)
else []
),
"django.contrib.auth.hashers.Argon2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",

View File

@@ -275,12 +275,12 @@
}
// Apply the mixin to the panel headings only
.panel-default > .panel-heading, .panel-default > legend > .panel-heading { @include panel-heading-styles($panel-default-heading-bg); }
.panel-primary > .panel-heading, .panel-primary > legend > .panel-heading { @include panel-heading-styles($panel-primary-heading-bg); }
.panel-success > .panel-heading, .panel-success > legend > .panel-heading { @include panel-heading-styles($panel-success-heading-bg); }
.panel-info > .panel-heading, .panel-info > legend > .panel-heading { @include panel-heading-styles($panel-info-heading-bg); }
.panel-warning > .panel-heading, .panel-warning > legend > .panel-heading { @include panel-heading-styles($panel-warning-heading-bg); }
.panel-danger > .panel-heading, .panel-danger > legend > .panel-heading { @include panel-heading-styles($panel-danger-heading-bg); }
.panel-default > .panel-heading { @include panel-heading-styles($panel-default-heading-bg); }
.panel-primary > .panel-heading { @include panel-heading-styles($panel-primary-heading-bg); }
.panel-success > .panel-heading { @include panel-heading-styles($panel-success-heading-bg); }
.panel-info > .panel-heading { @include panel-heading-styles($panel-info-heading-bg); }
.panel-warning > .panel-heading { @include panel-heading-styles($panel-warning-heading-bg); }
.panel-danger > .panel-heading { @include panel-heading-styles($panel-danger-heading-bg); }
//

View File

@@ -3,7 +3,7 @@
@mixin panel-variant($border, $heading-text-color, $heading-bg-color, $heading-border) {
border-color: $border;
& > .panel-heading, & > legend > .panel-heading {
& > .panel-heading {
color: $heading-text-color;
background-color: $heading-bg-color;
border-color: $heading-border;

View File

@@ -1,64 +0,0 @@
$(function () {
"use strict";
$("select[data-country-information-url]").each(function () {
let xhr;
const dependency = $(this),
loader = $("<span class='fa fa-cog fa-spin'></span>").hide().prependTo(dependency.closest(".form-group").find("label")),
url = this.getAttribute('data-country-information-url'),
form = dependency.closest(".panel-body, form, .profile-scope"),
isRequired = dependency.closest(".form-group").is(".required"),
dependents = {
'city': form.find("input[name$=city]"),
'zipcode': form.find("input[name$=zipcode]"),
'street': form.find("textarea[name$=street]"),
'state': form.find("select[name$=state]"),
'vat_id': form.find("input[name$=vat_id]"),
},
update = function (ev) {
if (xhr) {
xhr.abort();
}
for (var k in dependents) dependents[k].prop("disabled", true);
loader.show();
xhr = $.getJSON(url + '?country=' + dependency.val(), function (data) {
var selected_value = dependents.state.prop("data-selected-value");
if (selected_value) dependents.state.prop("data-selected-value", "");
dependents.state.find("option:not([value=''])").remove();
if (data.data.length > 0) {
$.each(data.data, function (k, s) {
var o = $("<option>").attr("value", s.code).text(s.name);
if (selected_value == s.code) o.prop("selected", true);
dependents.state.append(o);
});
}
for(var k in dependents) {
const options = data[k],
dependent = dependents[k],
visible = 'visible' in options ? options.visible : true,
required = 'required' in options && options.required && isRequired && visible;
dependent.closest(".form-group").toggle(visible).toggleClass('required', required);
dependent.prop("required", required);
}
for (var k in dependents) dependents[k].prop("disabled", false);
}).always(function() {
loader.hide();
}).fail(function(){
// In case of errors, show everything and require nothing, we can still handle errors in backend
for(var k in dependents) {
const dependent = dependents[k],
visible = true,
required = false;
dependent.closest(".form-group").toggle(visible).toggleClass('required', required);
dependent.prop("required", required);
}
});
};
dependents.state.prop("data-selected-value", dependents.state.val());
update();
dependency.on("change", update);
});
});

View File

@@ -53,9 +53,7 @@ $in-border-radius-small: 2px !default;
--pretix-brand-primary-darken-17: #{darken($in-brand-primary, 17%)};
--pretix-brand-primary-darken-20: #{darken($in-brand-primary, 20%)};
--pretix-brand-primary-darken-30: #{darken($in-brand-primary, 30%)};
--pretix-brand-primary-tint-90: #{tint($in-brand-primary, 90%)};
--pretix-brand-primary-shade-25: #{shade($in-brand-primary, 25%)};
--pretix-brand-primary-shade-42: #{shade($in-brand-primary, 42%)};
--pretix-brand-primary-lighten-28-saturate-20: #{saturate(lighten($in-brand-primary, 28%), 20%)};
--pretix-brand-primary-lighten-23-saturate-2: #{saturate(lighten($in-brand-primary, 23%), 2%)};

View File

@@ -434,6 +434,60 @@ var form_handlers = function (el) {
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update);
});
$("input[name$=vat_id][data-countries-with-vat-id]").each(function () {
var dependent = $(this),
dependency_country = $(this).closest(".panel-body, form").find('select[name$=country]'),
dependency_id_is_business_1 = $(this).closest(".panel-body, form").find('input[id$=id_is_business_1]'),
update = function (ev) {
if (dependency_id_is_business_1.length && !dependency_id_is_business_1.prop("checked")) {
dependent.closest(".form-group").hide();
} else if (dependent.attr('data-countries-with-vat-id').split(',').includes(dependency_country.val())) {
dependent.closest(".form-group").show();
} else {
dependent.closest(".form-group").hide();
}
};
update();
dependency_country.on("change", update);
dependency_id_is_business_1.on("change", update);
});
$("select[name$=state]:not([data-static])").each(function () {
var dependent = $(this),
counter = 0,
dependency = $(this).closest(".panel-body, form").find('select[name$=country]'),
update = function (ev) {
counter++;
var curCounter = counter;
dependent.prop("disabled", true);
dependency.closest(".form-group").find("label").prepend("<span class='fa fa-cog fa-spin'></span> ");
$.getJSON('/js_helpers/states/?country=' + dependency.val(), function (data) {
if (counter > curCounter) {
return; // Lost race
}
dependent.find("option").filter(function (t) {return !!$(this).attr("value")}).remove();
if (data.data.length > 0) {
$.each(data.data, function (k, s) {
dependent.append($("<option>").attr("value", s.code).text(s.name));
});
dependent.closest(".form-group").show();
dependent.prop('required', dependency.prop("required"));
} else {
dependent.closest(".form-group").hide();
dependent.prop("required", false);
}
dependent.prop("disabled", false);
dependency.closest(".form-group").find("label .fa-spin").remove();
});
};
if (dependent.find("option").length === 1) {
dependent.closest(".form-group").hide();
} else {
dependent.prop('required', dependency.prop("required"));
}
dependency.on("change", update);
});
el.find("div.scrolling-choice:not(.no-search)").each(function () {
if ($(this).find("input[type=text]").length > 0) {
return;

View File

@@ -194,6 +194,66 @@ div[data-formset-body], div[data-formset-form], div[data-nested-formset-form], d
line-height: 30px;
}
div.mail-preview {
border: 1px solid #ccc;
border-top-width: 1px;
border-radius: 3px;
.placeholder {
background: var(--pretix-brand-warning-transparent-60);
}
}
.mail-preview-group div[lang] {
@include border-top-radius(0px);
@include border-bottom-radius(0px);
border-top-width: 0;
margin-bottom: 0;
padding-right: 15px;
padding-bottom: 8px;
&:first-child {
@include border-top-radius($input-border-radius);
border-top-width: 1px;
}
&:last-child {
@include border-bottom-radius($input-border-radius);
margin-bottom: 20px;
}
h2, h3 {
margin-bottom: 20px;
margin-top: 10px;
}
p {
margin: 0 0 10px;
/* These are technically the same, but use both */
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
/* This is the dangerous one in WebKit, as it breaks things wherever */
word-break: break-all;
/* Instead use this non-standard one: */
word-break: break-word;
/* Adds a hyphen where the word breaks, if supported (No Blink) */
-ms-hyphens: auto;
-moz-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
}
p:last-child {
margin-bottom: 0;
}
/* Reset styling from bootstrap that we don't actually have in emails */
pre {
background: none;
border: none;
padding: 0;
}
}
.search-line {
width: 100%;
margin-bottom: 20px;
@@ -543,7 +603,7 @@ table td > .checkbox input[type="checkbox"] {
display: block;
margin: 0;
}
.panel-default>.accordion-radio>.panel-heading, fieldset.accordion-panel>legend>.panel-heading {
.panel-default>.accordion-radio>.panel-heading {
color: #333;
background-color: #f5f5f5;
padding: 12px 15px;
@@ -555,12 +615,6 @@ table td > .checkbox input[type="checkbox"] {
top: 2px;
}
}
fieldset.accordion-panel > legend {
display: contents;
}
fieldset.accordion-panel[disabled] > .panel-body {
display: none;
}
.maildesignpreview {
label {
display: block;

View File

@@ -1,93 +0,0 @@
div.mail-preview {
border: 1px solid #ccc;
border-top-width: 1px;
border-radius: 3px;
.placeholder {
background: var(--pretix-brand-warning-transparent-60);
}
.placeholder-html {
background: none;
outline: 2px solid var(--pretix-brand-warning-transparent-60);
display: inline-block;
}
}
.mail-preview-group div[lang] {
@include border-top-radius(0px);
@include border-bottom-radius(0px);
border-top-width: 0;
margin-bottom: 0;
padding-right: 15px;
padding-bottom: 8px;
&:first-child {
@include border-top-radius($input-border-radius);
border-top-width: 1px;
}
&:last-child {
@include border-bottom-radius($input-border-radius);
margin-bottom: 20px;
}
h2, h3 {
margin-bottom: 20px;
margin-top: 10px;
}
p {
margin: 0 0 10px;
/* These are technically the same, but use both */
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
/* This is the dangerous one in WebKit, as it breaks things wherever */
word-break: break-all;
/* Instead use this non-standard one: */
word-break: break-word;
/* Adds a hyphen where the word breaks, if supported (No Blink) */
-ms-hyphens: auto;
-moz-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
}
p:last-child {
margin-bottom: 0;
}
/* Reset styling from bootstrap that we don't actually have in emails */
pre {
background: none;
border: none;
padding: 0;
}
/* Add some basic styling similar to our default email renderers */
a.button {
display: inline-block;
padding: 10px 16px;
font-size: 14px;
line-height: 1.33333;
border: 1px solid #cccccc;
border-radius: 6px;
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
margin: 5px;
text-decoration: none;
color: var(--pretix-brand-primary);
}
table {
width: 100%;
}
table td {
vertical-align: top;
text-align: left;
padding: 0;
}
.text-right, table td.text-right {
text-align: right;
}
}

View File

@@ -12,7 +12,6 @@
@import "_flags.scss";
@import "_orders.scss";
@import "_dashboard.scss";
@import "_mail_preview.scss";
@import "../../pretixbase/scss/webfont.scss";
@import "../../fileupload/jquery.fileupload.scss";
@import "../../leaflet/leaflet.scss";

View File

@@ -243,11 +243,6 @@ function setup_basics(el) {
$($(this).attr("data-target")).collapse('show');
}
});
$("fieldset.accordion-panel > legend input[type=radio]").change(function() {
$(this).closest("fieldset").siblings("fieldset").prop('disabled', true);
$(this).closest("fieldset").prop('disabled', false);
}).each(function() { $(this).closest("fieldset").prop('disabled', true); }).filter(":checked").trigger('change');
el.find(".js-only").removeClass("js-only");
el.find(".js-hidden").hide();
@@ -464,26 +459,10 @@ $(function () {
.on("change mouseup keyup", update_cart_form);
$(".table-calendar td.has-events").click(function () {
var $grid = $(this).closest("[role='grid']");
$grid.find("[aria-selected]").attr("aria-selected", false);
$(this).attr("aria-selected", true);
$("#selected-day")
.html($(this).find(".events").clone())
.prepend($("<h3>").text($(this).attr("data-date")));
}).each(function() {
// check all events classes and set the "winning" class for the availability of the day-label on mobile
var $dayLabel = $('.day-label', this);
if ($('.available.low', this).length == $('.available', this).length) {
$dayLabel.addClass('low');
}
var classes = ['available', 'waitinglist', 'soon', 'reserved', 'soldout', 'continued', 'over'];
for (var c of classes) {
if ($('.'+c, this).length) {
$dayLabel.addClass(c);
// CAREFUL: „return“ as „break“ is not supported before ES2015 and breaks e.g. on iOS 15
return;
}
}
var $tr = $(this).closest(".table-calendar").find(".selected-day");
$tr.find("td").html($(this).find(".events").clone());
$tr.find("td").prepend($("<h3>").text($(this).attr("data-date")));
$tr.removeClass("hidden");
});
$(".print-this-page").on("click", function (e) {
@@ -538,6 +517,65 @@ $(function () {
dependency.closest('.form-group, form').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update);
});
$("input[name$=vat_id][data-countries-with-vat-id]").each(function () {
var dependent = $(this),
dependency_country = $(this).closest(".panel-body, form").find('select[name$=country]'),
dependency_id_is_business_1 = $(this).closest(".panel-body, form").find('input[id$=id_is_business_1]'),
update = function (ev) {
if (dependency_id_is_business_1.length && !dependency_id_is_business_1.prop("checked")) {
dependent.closest(".form-group").hide();
} else if (dependent.attr('data-countries-with-vat-id').split(',').includes(dependency_country.val())) {
dependent.closest(".form-group").show();
} else {
dependent.closest(".form-group").hide();
}
};
update();
dependency_country.on("change", update);
dependency_id_is_business_1.on("change", update);
});
$("select[name$=state]").each(function () {
var dependent = $(this),
counter = 0,
dependency = $(this).closest(".panel-body, form").find('select[name$=country]'),
update = function (ev) {
counter++;
var curCounter = counter;
dependent.prop("disabled", true);
dependency.closest(".form-group").find("label").prepend("<span class='fa fa-cog fa-spin'></span> ");
$.getJSON('/js_helpers/states/?country=' + dependency.val(), function (data) {
if (counter > curCounter) {
return; // Lost race
}
var selected_value = dependent.prop("data-selected-value");
dependent.find("option").filter(function (t) {return !!$(this).attr("value")}).remove();
if (data.data.length > 0) {
$.each(data.data, function (k, s) {
var o = $("<option>").attr("value", s.code).text(s.name);
if (s.code == selected_value || (selected_value && selected_value.indexOf && selected_value.indexOf(s.code) > -1)) {
o.prop("selected", true);
}
dependent.append(o);
});
dependent.closest(".form-group").show();
dependent.prop('required', dependency.prop("required"));
} else {
dependent.closest(".form-group").hide();
dependent.prop("required", false);
}
dependent.prop("disabled", false);
dependency.closest(".form-group").find("label .fa-spin").remove();
});
};
if (dependent.find("option").length === 1) {
dependent.closest(".form-group").hide();
} else {
dependent.prop('required', dependency.prop("required"));
}
dependency.on("change", update);
});
form_handlers($("body"));
var local_tz = moment.tz.guess()

View File

@@ -13,66 +13,93 @@
padding: 0;
}
a.event {
--status-bg-color: var(--pretix-brand-primary-tint-90);
--status-text-color: var(--pretix-brand-primary-shade-42);
--status-border-color: #{$brand-primary};
position: relative;
display: block;
background: var(--status-bg-color);
color: var(--status-text-color);
border: 1px solid var(--status-border-color);
background: var(--pretix-brand-primary-lighten-48);
color: $brand-primary;
border-radius: $border-radius-base;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 11px;
height: 100%;
background: var(--status-border-color);
}
border-style: solid;
border-color: var(--pretix-brand-primary-lighten-30);
border-width: 1px 1px 1px 12px;
border-left-color: inherit;
padding: 3px 5px 3px 17px;
padding: 3px 5px;
margin-bottom: 3px;
font-size: 12px;
overflow-wrap: anywhere;
text-decoration: none;
&:hover {
outline: 1px solid var(--status-border-color);
outline-offset: 0;
background: var(--pretix-brand-primary-lighten-50);
border-color: $brand-primary;
}
&:focus {
outline: 2px solid var(--status-border-color);
outline-offset: 2px;
outline-color: inherit;
}
&.continued, &.over {
--status-bg-color: #{$table-bg-accent};
--status-text-color: #{$text-muted};
--status-border-color: #{tint($text-muted, 50%)};
background: lighten(#767676, 54%);
border-color: lighten(#767676, 44%);
border-left-color: lighten(#767676, 44%);
color: #767676;
&:hover {
background: lighten(#767676, 54%);
border-color: lighten(#767676, 40%);
}
}
&.soon {
background: var(--pretix-brand-primary-lighten-53);
border-color: var(--pretix-brand-primary-lighten-40);
border-left-color: var(--pretix-brand-primary-lighten-20);
color: var(--pretix-brand-primary-lighten-5);
&:hover {
background: var(--pretix-brand-primary-lighten-55);
border-color: var(--pretix-brand-primary-lighten-20);
}
}
&.available {
--status-bg-color: #{$alert-success-bg};
--status-text-color: #{$alert-success-text};
--status-border-color: #{$alert-success-border};
background: var(--pretix-brand-success-lighten-48);
border-color: var(--pretix-brand-success-lighten-30);
border-left-color: $brand-success;
color: var(--pretix-brand-success-darken-12);
&.low:before {
background: linear-gradient(to bottom, var(--pretix-brand-warning) 1em, var(--status-border-color) 2.5em);
&.low {
border-left-color: var(--pretix-brand-warning-lighten-12);
}
&:hover {
background: var(--pretix-brand-success-lighten-50);
border-color: $brand-success;
&.low {
border-left-color: $brand-warning;
}
}
}
&.waitinglist {
--status-bg-color: #{$alert-warning-bg};
--status-text-color: #{$alert-warning-text};
--status-border-color: #{$alert-warning-border};
background: var(--pretix-brand-warning-lighten-41);
border-color: var(--pretix-brand-warning-lighten-31);
border-left-color: var(--pretix-brand-warning-lighten-12);
color: #963;
&:hover {
background: var(--pretix-brand-warning-lighten-43);
border-color: $brand-warning;
}
}
&.reserved, &.soldout, {
--status-bg-color: #{$alert-danger-bg};
--status-text-color: #{$alert-danger-text};
--status-border-color: #{$alert-danger-border};
background: var(--pretix-brand-danger-lighten-43);
border-color: var(--pretix-brand-danger-lighten-30);
border-left-color: var(--pretix-brand-danger-lighten-30);
color: var(--pretix-brand-danger-darken-5);
&:hover {
background: var(--pretix-brand-danger-lighten-45);
border-color: var(--pretix-brand-danger-lighten-25);
}
}
&.available > *:first-child,
@@ -407,6 +434,8 @@ if concurrency is higher than 9, JavaScript (currently in pretixpresale/js/ui/ma
}
@media (min-width: $screen-md-min) {
.week-calendar {
display: flex;
@@ -435,35 +464,24 @@ if concurrency is higher than 9, JavaScript (currently in pretixpresale/js/ui/ma
}
}
}
@media (max-width: $screen-xs-max) {
.table-calendar {
.day, .no-day {
padding: 3px 2px;
}
p {
margin-bottom: 0;
}
.day .events {
display: none;
}
a.day-label, .day-label {
--status-text-color: #{$text-muted};
display: block;
padding: 3px 3px 15px 12px;
font-size: 12px;
font-weight: bold;
color: var(--status-text-color);
margin-bottom: 0;
}
.no-events .day-label {
padding-left: 12px;
}
a.day-label:before {
width: 8px;
@media (min-width: $screen-sm-min) {
.table-calendar, .week-calendar {
.selected-day {
display: none !important;
}
}
#selected-day:has(*) {
padding: $table-cell-padding;
}
@media (max-width: $screen-xs-max) {
.table-calendar .day .events {
display: none;
}
.table-calendar td.day.has-events {
background: $brand-primary;
cursor: pointer;
color: white;
}
.table-calendar td.day.has-events:hover {
background: var(--pretix-brand-primary-darken-15);
}
}
#monthselform .row {

View File

@@ -134,7 +134,7 @@ a.btn, button.btn {
display: block;
margin: 0;
}
.panel-default>.accordion-radio>.panel-heading, fieldset.accordion-panel>legend>.panel-heading {
.panel-default>.accordion-radio>.panel-heading {
color: #333;
background-color: #f5f5f5;
padding: 8px 15px;
@@ -147,12 +147,6 @@ a.btn, button.btn {
.panel-default>.accordion-radio+.panel-collapse>.panel-body {
border-top: 1px solid #ddd;
}
fieldset.accordion-panel > legend {
display: contents;
}
fieldset.accordion-panel[disabled] > .panel-body {
display: none;
}
.nav-tabs {
border-bottom: 0px solid #ddd;

View File

@@ -1601,80 +1601,6 @@ def test_event_block_unblock_seat(token_client, organizer, event, seatingplan, i
assert resp.data['blocked'] is False
@pytest.mark.django_db
def test_event_block_unblock_seat_bulk(token_client, organizer, event, seatingplan, item):
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
{
"seating_plan": seatingplan.pk,
"seat_category_mapping": {
"Stalls": item.pk
}
},
format='json'
)
assert resp.status_code == 200
event.refresh_from_db()
s1 = event.seats.first()
s2 = event.seats.last()
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/seats/bulk_block/'.format(organizer.slug, event.slug),
{
"ids": [s1.pk, s2.pk],
},
format='json'
)
assert resp.status_code == 200
s1.refresh_from_db()
s2.refresh_from_db()
assert s1.blocked
assert s2.blocked
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/seats/bulk_unblock/'.format(organizer.slug, event.slug),
{
"ids": [s1.pk, s2.pk],
},
format='json'
)
assert resp.status_code == 200
s1.refresh_from_db()
s2.refresh_from_db()
assert not s1.blocked
assert not s2.blocked
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/seats/bulk_block/'.format(organizer.slug, event.slug),
{
"seat_guids": [s1.seat_guid, s2.seat_guid],
},
format='json'
)
assert resp.status_code == 200
s1.refresh_from_db()
s2.refresh_from_db()
assert s1.blocked
assert s2.blocked
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/seats/bulk_unblock/'.format(organizer.slug, event.slug),
{
"seat_guids": [s1.seat_guid, s2.seat_guid],
},
format='json'
)
assert resp.status_code == 200
s1.refresh_from_db()
s2.refresh_from_db()
assert not s1.blocked
assert not s2.blocked
@pytest.mark.django_db
def test_event_expand_seat_filter_and_querycount(token_client, organizer, event, seatingplan, item):
event.settings.seating_minimal_distance = 2

View File

@@ -1967,7 +1967,7 @@ def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_q
assert not resp.data['positions'][0].get('pdf_data')
# order list
with django_assert_max_num_queries(32):
with django_assert_max_num_queries(31):
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
))
@@ -1982,7 +1982,7 @@ def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_q
assert not resp.data['results'][0]['positions'][0].get('pdf_data')
# position list
with django_assert_max_num_queries(35):
with django_assert_max_num_queries(34):
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/?pdf_data=true'.format(
organizer.slug, event.slug
))

View File

@@ -19,9 +19,7 @@
# 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/>.
#
from pretix.helpers.format import (
PlainHtmlAlternativeString, SafeFormatter, format_map,
)
from pretix.helpers.format import format_map
def test_format_map():
@@ -30,16 +28,3 @@ def test_format_map():
assert format_map("Foo {bar.__module__}", {"bar": 3}) == "Foo {bar.__module__}"
assert format_map("Foo {bar!s}", {"bar": 3}) == "Foo 3"
assert format_map("Foo {bar:<20}", {"bar": 3}) == "Foo 3"
def test_format_alternatives():
ctx = {
"bar": PlainHtmlAlternativeString(
"plain text",
"<span>HTML version</span>",
)
}
assert format_map("Foo {bar}", ctx, mode=SafeFormatter.MODE_IGNORE_RICH) == "Foo {bar}"
assert format_map("Foo {bar}", ctx, mode=SafeFormatter.MODE_RICH_TO_PLAIN) == "Foo plain text"
assert format_map("Foo {bar}", ctx, mode=SafeFormatter.MODE_RICH_TO_HTML) == "Foo <span>HTML version</span>"

View File

@@ -40,21 +40,16 @@ def env():
@pytest.mark.django_db
def test_control_only_on_main_domain(env, client):
KnownDomain.objects.create(domainname='foobar', organizer=env[0], mode=KnownDomain.MODE_ORG_DOMAIN)
KnownDomain.objects.create(domainname='foobar', organizer=env[0])
r = client.get('/control/login', HTTP_HOST='foobar')
assert r.status_code == 302
assert r['Location'] == 'http://example.com/control/login'
KnownDomain.objects.create(domainname='barfoo', organizer=env[0], event=env[1], mode=KnownDomain.MODE_EVENT_DOMAIN)
KnownDomain.objects.create(domainname='barfoo', organizer=env[0], event=env[1])
r = client.get('/control/login', HTTP_HOST='barfoo')
assert r.status_code == 302
assert r['Location'] == 'http://example.com/control/login'
KnownDomain.objects.create(domainname='altfoo', organizer=env[0], mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
r = client.get('/control/login', HTTP_HOST='altfoo')
assert r.status_code == 302
assert r['Location'] == 'http://example.com/control/login'
@pytest.mark.django_db
def test_append_slash(env, client):
@@ -85,15 +80,6 @@ def test_event_on_custom_domain(env, client):
assert b'<meta property="og:title" content="MRMCD2015" />' in r.content
@pytest.mark.django_db
def test_event_on_org_alt_domain(env, client):
d = KnownDomain.objects.create(domainname='foobar', organizer=env[0], mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
d.event_assignments.create(event=env[1])
r = client.get('/2015/', HTTP_HOST='foobar')
assert r.status_code == 200
assert b'<meta property="og:title" content="MRMCD2015" />' in r.content
@pytest.mark.django_db
def test_path_without_trailing_slash_on_org_domain(env, client):
KnownDomain.objects.create(domainname='foobar', organizer=env[0])
@@ -109,15 +95,6 @@ def test_event_with_org_domain_on_main_domain(env, client):
assert r['Location'] == 'http://foobar/2015/'
@pytest.mark.django_db
def test_event_with_org_alt_domain_on_main_domain(env, client):
d = KnownDomain.objects.create(domainname='foobar', organizer=env[0])
d.event_assignments.create(event=env[1])
r = client.get('/mrmcd/2015/', HTTP_HOST='example.com')
assert r.status_code == 302
assert r['Location'] == 'http://foobar/2015/'
@pytest.mark.django_db
def test_event_with_custom_domain_on_main_domain(env, client):
KnownDomain.objects.create(domainname='foobar', organizer=env[0], event=env[1])
@@ -135,40 +112,6 @@ def test_event_with_custom_domain_on_org_domain(env, client):
assert r['Location'] == 'http://barfoo'
@pytest.mark.django_db
def test_event_with_custom_domain_on_org_alt_domain(env, client):
KnownDomain.objects.create(domainname='foobar', organizer=env[0])
KnownDomain.objects.create(domainname='altfoo', organizer=env[0], mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
KnownDomain.objects.create(domainname='barfoo', organizer=env[0], event=env[1])
r = client.get('/2015/', HTTP_HOST='altfoo')
assert r.status_code == 302
assert r['Location'] == 'http://barfoo'
@pytest.mark.django_db
def test_event_with_org_alt_domain_on_org_domain(env, client):
KnownDomain.objects.create(domainname='foobar', organizer=env[0], mode=KnownDomain.MODE_ORG_DOMAIN)
d = KnownDomain.objects.create(domainname='altfoo', organizer=env[0], mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
d.event_assignments.create(event=env[1])
r = client.get('/2015/', HTTP_HOST='foobar')
assert r.status_code == 302
assert r['Location'] == 'http://altfoo/2015/'
@pytest.mark.django_db
def test_event_without_org_alt_domain_on_org_alt_domain(env, client):
KnownDomain.objects.create(domainname='foobar', organizer=env[0], mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
r = client.get('/', HTTP_HOST='foobar')
assert r.status_code == 302
assert r['Location'] == 'http://example.com/mrmcd/'
KnownDomain.objects.create(domainname='foobaz', organizer=env[0], mode=KnownDomain.MODE_ORG_DOMAIN)
r = client.get('/', HTTP_HOST='foobar')
assert r.status_code == 302
assert r['Location'] == 'http://foobaz/'
@pytest.mark.django_db
def test_organizer_with_org_domain_on_main_domain(env, client):
KnownDomain.objects.create(domainname='foobar', organizer=env[0])
@@ -200,10 +143,6 @@ def test_unknown_event_on_org_domain(env, client):
r = client.get('/1234/', HTTP_HOST='foobar')
assert r.status_code == 404
KnownDomain.objects.create(domainname='altfoo', organizer=env[0], mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
r = client.get('/1234/', HTTP_HOST='altfoo')
assert r.status_code == 404
@pytest.mark.django_db
def test_cookie_domain_on_org_domain(env, client):
@@ -214,16 +153,6 @@ def test_cookie_domain_on_org_domain(env, client):
assert r.client.cookies['pretix_session']['domain'] == ''
@pytest.mark.django_db
def test_cookie_domain_on_org_alt_domain(env, client):
d = KnownDomain.objects.create(domainname='foobar', organizer=env[0], mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
d.event_assignments.create(event=env[1])
client.post('/2015/cart/add', HTTP_HOST='foobar')
r = client.get('/2015/', HTTP_HOST='foobar')
assert r.client.cookies['pretix_csrftoken']['domain'] == ''
assert r.client.cookies['pretix_session']['domain'] == ''
@pytest.mark.django_db
def test_cookie_domain_on_event_domain(env, client):
KnownDomain.objects.create(domainname='foobar', organizer=env[0])

View File

@@ -62,36 +62,6 @@ def test_event_custom_domain_front_page(env):
assert rendered == 'http://foobar/2015/'
@pytest.mark.django_db
def test_event_custom_event_domain_front_page(env):
KnownDomain.objects.create(domainname='foobar', organizer=env[0], event=env[1], mode=KnownDomain.MODE_EVENT_DOMAIN)
rendered = TEMPLATE_FRONT_PAGE.render(Context({
'event': env[1]
})).strip()
assert rendered == 'http://foobar/'
@pytest.mark.django_db
def test_event_custom_org_alt_domain_front_page(env):
KnownDomain.objects.create(domainname='foobar', organizer=env[0], mode=KnownDomain.MODE_ORG_DOMAIN)
d = KnownDomain.objects.create(domainname='altfoo', organizer=env[0], mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
d.event_assignments.create(event=env[1])
rendered = TEMPLATE_FRONT_PAGE.render(Context({
'event': env[1]
})).strip()
assert rendered == 'http://altfoo/2015/'
@pytest.mark.django_db
def test_event_custom_org_alt_domain_unassigned_front_page(env):
KnownDomain.objects.create(domainname='foobar', organizer=env[0], mode=KnownDomain.MODE_ORG_DOMAIN)
KnownDomain.objects.create(domainname='altfoo', organizer=env[0], mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
rendered = TEMPLATE_FRONT_PAGE.render(Context({
'event': env[1]
})).strip()
assert rendered == 'http://foobar/2015/'
@pytest.mark.django_db
def test_event_main_domain_kwargs(env):
rendered = TEMPLATE_KWARGS.render(Context({

View File

@@ -37,7 +37,7 @@ def env():
organizer=o, name='MRMCD2015', slug='2015',
date_from=now()
)
event.cache.clear()
event.get_cache().clear()
return o, event
@@ -60,16 +60,6 @@ def test_event_org_domain_kwargs(env):
assert eventreverse(env[1], 'presale:event.checkout', {'step': 'payment'}) == 'http://foobar/2015/checkout/payment/'
@pytest.mark.django_db
def test_event_org_alt_domain_kwargs(env):
KnownDomain.objects.create(domainname='foobar', organizer=env[0])
d = KnownDomain.objects.create(domainname='altfoo', organizer=env[0], mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
assert eventreverse(env[1], 'presale:event.checkout', {'step': 'payment'}) == 'http://foobar/2015/checkout/payment/'
d.event_assignments.create(event=env[1])
with scopes_disabled():
assert eventreverse(Event.objects.get(pk=env[1].pk), 'presale:event.checkout', {'step': 'payment'}) == 'http://altfoo/2015/checkout/payment/'
@pytest.mark.django_db
def test_event_main_domain_kwargs(env):
assert eventreverse(env[1], 'presale:event.checkout', {'step': 'payment'}) == '/mrmcd/2015/checkout/payment/'
@@ -82,15 +72,6 @@ def test_event_org_domain_front_page(env):
assert eventreverse(env[0], 'presale:organizer.index') == 'http://foobar/'
@pytest.mark.django_db
def test_event_org_alt_domain_front_page(env):
KnownDomain.objects.create(domainname='foobar', organizer=env[0])
d = KnownDomain.objects.create(domainname='altfoo', organizer=env[0], mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
d.event_assignments.create(event=env[1])
assert eventreverse(env[1], 'presale:event.index') == 'http://altfoo/2015/'
assert eventreverse(env[0], 'presale:organizer.index') == 'http://foobar/'
@pytest.mark.django_db
def test_event_custom_domain_front_page(env):
KnownDomain.objects.create(domainname='barfoo', organizer=env[0], event=env[1])
@@ -128,8 +109,8 @@ def test_event_org_domain_keep_scheme(env):
}
})
def test_event_main_domain_cache(env):
env[0].cache.clear()
with assert_num_queries(2):
env[0].get_cache().clear()
with assert_num_queries(1):
eventreverse(env[1], 'presale:event.index')
with assert_num_queries(0):
eventreverse(env[1], 'presale:event.index')
@@ -144,8 +125,8 @@ def test_event_main_domain_cache(env):
})
def test_event_org_domain_cache(env):
KnownDomain.objects.create(domainname='foobar', organizer=env[0])
env[0].cache.clear()
with assert_num_queries(2):
env[0].get_cache().clear()
with assert_num_queries(1):
eventreverse(env[1], 'presale:event.index')
with assert_num_queries(0):
eventreverse(env[1], 'presale:event.index')
@@ -161,36 +142,13 @@ def test_event_org_domain_cache(env):
def test_event_custom_domain_cache(env):
KnownDomain.objects.create(domainname='foobar', organizer=env[0])
KnownDomain.objects.create(domainname='barfoo', organizer=env[0], event=env[1])
env[0].cache.clear()
env[0].get_cache().clear()
with assert_num_queries(1):
eventreverse(env[1], 'presale:event.index')
with assert_num_queries(0):
eventreverse(env[1], 'presale:event.index')
@pytest.mark.django_db
@override_settings(CACHES={
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-snowflake',
}
})
@scopes_disabled()
def test_event_org_alt_domain_cache_clear(env):
KnownDomain.objects.create(domainname='foobar', organizer=env[0])
kd_alt = KnownDomain.objects.create(domainname='altfoo', organizer=env[0], mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
env[0].cache.clear()
with assert_num_queries(2):
eventreverse(env[1], 'presale:event.index')
kd_alt.event_assignments.create(event=env[1])
with assert_num_queries(2):
ev = Event.objects.get(pk=env[1].pk)
assert ev.pk == env[1].pk
assert ev.organizer == env[0]
with assert_num_queries(1):
eventreverse(ev, 'presale:event.index')
@pytest.mark.django_db
@override_settings(CACHES={
'default': {
@@ -202,14 +160,14 @@ def test_event_org_alt_domain_cache_clear(env):
def test_event_org_domain_cache_clear(env):
kd = KnownDomain.objects.create(domainname='foobar', organizer=env[0])
env[0].cache.clear()
with assert_num_queries(2):
with assert_num_queries(1):
eventreverse(env[1], 'presale:event.index')
kd.delete()
with assert_num_queries(2):
ev = Event.objects.get(pk=env[1].pk)
assert ev.pk == env[1].pk
assert ev.organizer == env[0]
with assert_num_queries(2):
with assert_num_queries(1):
eventreverse(ev, 'presale:event.index')
@@ -232,7 +190,7 @@ def test_event_custom_domain_cache_clear(env):
ev = Event.objects.get(pk=env[1].pk)
assert ev.pk == env[1].pk
assert ev.organizer == env[0]
with assert_num_queries(2):
with assert_num_queries(1):
eventreverse(ev, 'presale:event.index')
@@ -252,11 +210,3 @@ def test_event_custom_domain_absolute(env):
def test_event_org_domain_absolute(env):
KnownDomain.objects.create(domainname='foobar', organizer=env[0])
assert build_absolute_uri(env[1], 'presale:event.index') == 'http://foobar/2015/'
@pytest.mark.django_db
def test_event_org_alt_domain_absolute(env):
KnownDomain.objects.create(domainname='foobar', organizer=env[0])
d = KnownDomain.objects.create(domainname='altfoo', organizer=env[0], mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
d.event_assignments.create(event=env[1])
assert build_absolute_uri(env[1], 'presale:event.index') == 'http://altfoo/2015/'

View File

@@ -1207,65 +1207,6 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
}
assert ia.name_cached == 'Mr John Kennedy'
def test_invoice_address_required_no_zipcode_country(self):
self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = True
self.event.settings.invoice_address_not_asked_free = True
self.event.settings.set('name_scheme', 'title_given_middle_family')
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select('input[name="city"]')), 1)
# Not all required fields filled out, expect failure
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
'company': 'Foo',
'name_parts_0': 'Mr',
'name_parts_1': 'John',
'name_parts_2': '',
'name_parts_3': 'Kennedy',
'street': '',
'zipcode': '',
'city': '',
'country': 'BI',
'email': 'admin@localhost'
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
# Correct request for a country where zip code is not required in address
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
'company': 'Foo',
'name_parts_0': 'Mr',
'name_parts_1': 'John',
'name_parts_2': '',
'name_parts_3': 'Kennedy',
'street': 'BP 12345',
'zipcode': '',
'city': 'Bujumbura',
'country': 'BI',
'email': 'admin@localhost'
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
with scopes_disabled():
ia = InvoiceAddress.objects.last()
assert ia.name_parts == {
'title': 'Mr',
'given_name': 'John',
'middle_name': '',
'family_name': 'Kennedy',
"_scheme": "title_given_middle_family"
}
assert ia.name_cached == 'Mr John Kennedy'
def test_invoice_address_validated(self):
self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = True

View File

@@ -378,13 +378,6 @@ def test_org_sso_login_new_customer_popup(env, client, provider):
_sso_login(client, provider, popup_origin="https://popuporigin")
@pytest.mark.django_db
def test_org_sso_login_new_customer_popup_org_alt_domain(env, client, provider):
d = KnownDomain.objects.create(organizer=env[0], domainname="popuporigin", mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
d.event_assignments.create(event=env[1])
_sso_login(client, provider, popup_origin="https://popuporigin")
@pytest.mark.django_db
def test_org_sso_login_new_customer_popup_invalid_origin(env, client, provider):
KnownDomain.objects.create(organizer=env[0], event=env[1], domainname="popuporigin")
@@ -703,21 +696,16 @@ def client2():
return Client()
def _cross_domain_login(env, client, client2, org_alt=False):
def _cross_domain_login(env, client, client2):
with scopes_disabled():
customer = env[0].customers.create(email='john@example.org', is_verified=True)
customer.set_password('foo')
customer.save()
KnownDomain.objects.create(domainname='org.test', organizer=env[0])
if org_alt:
d = KnownDomain.objects.create(domainname='event.test', organizer=env[0], mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
d.event_assignments.create(event=env[1])
else:
KnownDomain.objects.create(domainname='event.test', organizer=env[0], event=env[1])
KnownDomain.objects.create(domainname='event.test', organizer=env[0], event=env[1])
# Log in on org domain
path = '/conf/' if org_alt else '/'
r = client.post(f'/account/login?next=https://event.test{path}redeem&request_cross_domain_customer_auth=true', {
r = client.post('/account/login?next=https://event.test/redeem&request_cross_domain_customer_auth=true', {
'email': 'john@example.org',
'password': 'foo',
}, HTTP_HOST='org.test')
@@ -725,12 +713,12 @@ def _cross_domain_login(env, client, client2, org_alt=False):
u = urlparse(r.headers['Location'])
assert u.netloc == 'event.test'
assert u.path == path + 'redeem'
assert u.path == '/redeem'
q = parse_qs(u.query)
assert 'cross_domain_customer_auth' in q
# Take session over to event domain
r = client2.get(f'{path}?{u.query}', HTTP_HOST='event.test')
r = client2.get(f'/?{u.query}', HTTP_HOST='event.test')
assert r.status_code == 200
assert b'john@example.org' in r.content
@@ -739,27 +727,12 @@ def _cross_domain_login(env, client, client2, org_alt=False):
def test_cross_domain_login(env, client, client2):
_cross_domain_login(env, client, client2)
# Logged in on evnet domain
# Logged in on org domain
r = client.get('/', HTTP_HOST='event.test')
assert r.status_code == 200
assert b'john@example.org' in r.content
# Logged in on org domain
r = client2.get('/', HTTP_HOST='org.test')
assert r.status_code == 200
assert b'john@example.org' in r.content
@pytest.mark.django_db
def test_cross_domain_login_org_alt(env, client, client2):
_cross_domain_login(env, client, client2, org_alt=True)
# Logged in on org alt domain
r = client.get('/conf/', HTTP_HOST='event.test')
assert r.status_code == 200
assert b'john@example.org' in r.content
# Logged in on org domain
# Logged in on event domain
r = client2.get('/', HTTP_HOST='org.test')
assert r.status_code == 200
assert b'john@example.org' in r.content