mirror of
https://github.com/pretix/pretix.git
synced 2026-07-20 07:38:38 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd63399d51 |
@@ -53,7 +53,7 @@ Working with the code
|
||||
---------------------
|
||||
If you do not have a recent installation of ``nodejs``, install it now::
|
||||
|
||||
curl -sL https://deb.nodesource.com/setup_24.x | sudo -E bash -
|
||||
curl -sL https://deb.nodesource.com/setup_17.x | sudo -E bash -
|
||||
sudo apt install nodejs
|
||||
|
||||
To make sure it is on your path variable, close and reopen your terminal. Now, install the Python-level dependencies of pretix::
|
||||
|
||||
+3
-3
@@ -41,9 +41,9 @@ dependencies = [
|
||||
"django-bootstrap3==26.1",
|
||||
"django-compressor==4.6.0",
|
||||
"django-countries==8.2.*",
|
||||
"django-filter==26.1",
|
||||
"django-filter==25.1",
|
||||
"django-formset-js-improved==0.5.0.5",
|
||||
"django-formtools==2.7",
|
||||
"django-formtools==2.6.1",
|
||||
"django-hierarkey==2.0.*,>=2.0.1",
|
||||
"django-hijack==3.7.*",
|
||||
"django-i18nfield==1.11.*",
|
||||
@@ -94,7 +94,7 @@ dependencies = [
|
||||
"redis==7.4.*",
|
||||
"reportlab==5.0.*",
|
||||
"requests==2.34.*",
|
||||
"sentry-sdk==2.65.*",
|
||||
"sentry-sdk==2.64.*",
|
||||
"sepaxml==2.7.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
|
||||
@@ -1658,7 +1658,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
count_waitinglist=False,
|
||||
force=request.data.get('force', False),
|
||||
send_mail=send_mail,
|
||||
ignore_date=request.data.get('force', False),
|
||||
)
|
||||
except Quota.QuotaExceededException:
|
||||
pass
|
||||
@@ -1694,8 +1693,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
auth=self.request.auth,
|
||||
count_waitinglist=False,
|
||||
send_mail=send_mail,
|
||||
force=force,
|
||||
ignore_date=force)
|
||||
force=force)
|
||||
except Quota.QuotaExceededException as e:
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except PaymentException as e:
|
||||
|
||||
@@ -1116,13 +1116,6 @@ class BaseQuestionsForm(forms.Form):
|
||||
if q.dependency_question_id and not question_is_visible(q.dependency_question_id, q.dependency_values) and answer is not None:
|
||||
d['question_%d' % q.pk] = None
|
||||
|
||||
# Strip False answers to required yes/no questions even if all_optional is set, as our data model assumes that
|
||||
# required yes/no questions can only be answered with yes
|
||||
for q in question_cache.values():
|
||||
if q.required and q.type == Question.TYPE_BOOLEAN:
|
||||
if 'question_%d' % q.pk in d and d['question_%d' % q.pk] is False:
|
||||
del d['question_%d' % q.pk]
|
||||
|
||||
return d
|
||||
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ from django.core.cache import cache
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import close_old_connections
|
||||
from django.dispatch.dispatcher import NO_RECEIVERS
|
||||
from django_querytagger.tagging import with_tag
|
||||
|
||||
from pretix.helpers.periodic import SKIPPED
|
||||
|
||||
@@ -83,8 +82,7 @@ class Command(BaseCommand):
|
||||
try:
|
||||
# Check if the DB connection is still good, it might be closed if the previous task took too long.
|
||||
close_old_connections()
|
||||
with with_tag(f"periodictask={name}"):
|
||||
r = receiver(signal=periodic_task, sender=self)
|
||||
r = receiver(signal=periodic_task, sender=self)
|
||||
except Exception as err:
|
||||
if isinstance(err, KeyboardInterrupt):
|
||||
raise err
|
||||
|
||||
@@ -266,22 +266,6 @@ def _merge_csp(a, b):
|
||||
|
||||
|
||||
class SecurityMiddleware(MiddlewareMixin):
|
||||
SAFE_TYPES = (
|
||||
# CSP policies are only used for:
|
||||
# - HTML and SVG in top-level contexts
|
||||
# - SVG or JS Workers delivered in embedded contexts
|
||||
# See: https://www.w3.org/TR/CSP2/#which-policy-applies
|
||||
# Therefore, we can save bandwidth on not including our (sometimes huge) policy
|
||||
# on API responses or CSS. We do however include it with other types as a precaution
|
||||
# (whitelist instead of blacklist) and we also do not whitelist JavaScript in
|
||||
# we ever add service workers to not break the protection of this feature:
|
||||
# https://www.w3.org/TR/CSP2/#sandboxing-and-workers
|
||||
'application/json',
|
||||
'text/css',
|
||||
# We used to skip CSP for PDF since it was necessary for inline previews in Safari,
|
||||
# but at the moment it does not seem to be an issue to just send it.
|
||||
)
|
||||
|
||||
def process_response(self, request, resp):
|
||||
if settings.DEBUG and resp.status_code >= 400:
|
||||
# Don't use CSP on debug error page as it breaks of Django's fancy error
|
||||
@@ -293,11 +277,6 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
# https://github.com/pretix/pretix/issues/765
|
||||
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
|
||||
|
||||
if "Content-Type" in resp and resp["Content-Type"].split(";")[0] in self.SAFE_TYPES:
|
||||
if 'Content-Security-Policy' in resp:
|
||||
del resp['Content-Security-Policy']
|
||||
return resp
|
||||
|
||||
if not getattr(resp, '_csp_ignore', False):
|
||||
resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp))
|
||||
elif 'Content-Security-Policy' in resp:
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# Generated by Django 4.2.8 on 2024-07-01 09:27
|
||||
import logging
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.models import Count
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def clean_duplicate_secrets(apps, schema_editor):
|
||||
# This will autofix all possible duplicate Order.code and OrderPosition.secret values,
|
||||
# unless Order.code is already too long to append something. This would need to be fixed by
|
||||
# sysadmins manually.
|
||||
OrderPosition = apps.get_model("pretixbase", "OrderPosition")
|
||||
Order = apps.get_model("pretixbase", "Order")
|
||||
|
||||
qs = OrderPosition.all.values("secret", "order__event__organizer_id").order_by().annotate(c=Count("*")).filter(c__gt=1)
|
||||
for row in qs:
|
||||
affected = OrderPosition.all.filter(
|
||||
**{k: v for k, v in row.items() if k != "c"}
|
||||
).order_by("pk")
|
||||
logger.error(f"Found {row['c']} tickets with with the same secret \"{row['secret']}\" in organizer {row['order__event__organizer_id']}, all except one will be changed")
|
||||
for i, a in enumerate(affected):
|
||||
if i > 0:
|
||||
a.secret = a.secret + "__dupl__" + str(a.pk)
|
||||
logger.info(
|
||||
f"Ticket {a.pk} has new secret {a.secret}"
|
||||
)
|
||||
a.save(update_fields=["organizer_id", "secret"])
|
||||
|
||||
qs = Order.objects.values("code", "event__organizer_id").order_by().annotate(c=Count("*")).filter(c__gt=1)
|
||||
for row in qs:
|
||||
affected = Order.objects.filter(
|
||||
**{k: v for k, v in row.items() if k != "c"}
|
||||
).order_by("pk")
|
||||
logger.error(f"Found {row['c']} orders with with the same code \"{row['code']}\" in organizer {row['event__organizer_id']}, all except one will be changed")
|
||||
for i, a in enumerate(affected):
|
||||
if i > 0:
|
||||
if len(a.code) > 16 - len(str(a.pk)):
|
||||
raise ValueError(f"Cannot auto-fix order with duplicate code {a.code}, order code is too long already")
|
||||
a.code = a.code + str(a.pk).zfill(16 - len(a.code))
|
||||
logger.info(
|
||||
f"Order {a.pk} has new code {a.code}"
|
||||
)
|
||||
a.save(update_fields=["organizer_id", "code"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
(
|
||||
"pretixbase",
|
||||
"0301_reusablemedium_remove_orderposition",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(clean_duplicate_secrets, migrations.RunPython.noop),
|
||||
]
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
# Generated by Django 4.2.8 on 2024-07-01 09:27
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
(
|
||||
"pretixbase",
|
||||
"0302_resolve_duplicate_codes_and_secrets",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunSQL(
|
||||
"UPDATE pretixbase_order "
|
||||
"SET organizer_id = (SELECT e.organizer_id FROM pretixbase_event e WHERE e.id = pretixbase_order.event_id) "
|
||||
"WHERE pretixbase_order.organizer_id IS NULL;",
|
||||
migrations.RunSQL.noop,
|
||||
),
|
||||
migrations.RunSQL(
|
||||
"UPDATE pretixbase_orderposition "
|
||||
"SET organizer_id = (SELECT e.organizer_id FROM pretixbase_order o LEFT JOIN pretixbase_event e ON e.id = o.event_id WHERE o.id = pretixbase_orderposition.order_id) "
|
||||
"WHERE pretixbase_orderposition.organizer_id IS NULL;",
|
||||
migrations.RunSQL.noop,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="order",
|
||||
name="organizer",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="orders",
|
||||
to="pretixbase.organizer",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="orderposition",
|
||||
name="organizer",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="order_positions",
|
||||
to="pretixbase.organizer",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1403,12 +1403,15 @@ class Event(EventMixin, LoggedModel):
|
||||
|
||||
for mp in self.organizer.meta_properties.all():
|
||||
if mp.required and not self.meta_data.get(mp.name):
|
||||
issues.append(format_html(
|
||||
'<a href="{href}{href_hash}">{text}</a>',
|
||||
text=gettext('You need to fill the meta parameter "{property}".').format(property=mp.name),
|
||||
href=reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}),
|
||||
href_hash=f'#id_prop-{mp.pk}-value',
|
||||
))
|
||||
issues.append(
|
||||
('<a {a_attr}>' + gettext('You need to fill the meta parameter "{property}".') + '</a>').format(
|
||||
property=mp.name,
|
||||
a_attr='href="%s#id_prop-%d-value"' % (
|
||||
reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}),
|
||||
mp.pk
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
responses = event_live_issues.send(self)
|
||||
for receiver, response in sorted(responses, key=lambda r: str(r[0])):
|
||||
|
||||
@@ -224,6 +224,8 @@ class Order(LockModel, LoggedModel):
|
||||
"Organizer",
|
||||
related_name="orders",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
@@ -327,7 +329,7 @@ class Order(LockModel, LoggedModel):
|
||||
default="line",
|
||||
)
|
||||
|
||||
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='organizer')
|
||||
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Order")
|
||||
@@ -2539,6 +2541,8 @@ class OrderPosition(AbstractPosition):
|
||||
"Organizer",
|
||||
related_name="order_positions",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
order = models.ForeignKey(
|
||||
Order,
|
||||
@@ -2595,7 +2599,7 @@ class OrderPosition(AbstractPosition):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
all = ScopedManager(organizer='organizer')
|
||||
all = ScopedManager(organizer='order__event__organizer')
|
||||
objects = ActivePositionManager()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -32,10 +32,8 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
import datetime
|
||||
from dataclasses import dataclass
|
||||
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from typing import Union
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -423,33 +421,27 @@ class Voucher(LoggedModel):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_affected_quotas(quota, item, variation, subevent):
|
||||
if quota:
|
||||
return {quota}
|
||||
elif item and variation:
|
||||
return set(variation.quotas.filter(subevent=subevent))
|
||||
elif item and not item.has_variations:
|
||||
return set(item.quotas.filter(subevent=subevent))
|
||||
elif item and item.has_variations:
|
||||
return set(
|
||||
Quota.objects.filter(
|
||||
pk__in=Quota.variations.through.objects.filter(
|
||||
itemvariation__item=item,
|
||||
quota__subevent=subevent,
|
||||
).values('quota_id')
|
||||
)
|
||||
)
|
||||
else:
|
||||
return set()
|
||||
|
||||
@staticmethod
|
||||
def clean_quota_get_ignored(voucher_data: Union["VoucherBulkData", "Voucher"]):
|
||||
if voucher_data:
|
||||
valid = voucher_data.valid_until is None or voucher_data.valid_until >= now()
|
||||
if valid and voucher_data.block_quota and voucher_data.max_usages > voucher_data.redeemed:
|
||||
return Voucher.get_affected_quotas(voucher_data.quota, voucher_data.item, voucher_data.variation, voucher_data.subevent)
|
||||
|
||||
return set()
|
||||
def clean_quota_get_ignored(old_instance):
|
||||
quotas = set()
|
||||
was_valid = old_instance and (
|
||||
old_instance.valid_until is None or old_instance.valid_until >= now()
|
||||
)
|
||||
if old_instance and old_instance.block_quota and was_valid:
|
||||
if old_instance.quota:
|
||||
quotas.add(old_instance.quota)
|
||||
elif old_instance.variation:
|
||||
quotas |= set(old_instance.variation.quotas.filter(subevent=old_instance.subevent))
|
||||
elif old_instance.item:
|
||||
if old_instance.item.has_variations:
|
||||
quotas |= set(
|
||||
Quota.objects.filter(pk__in=Quota.variations.through.objects.filter(
|
||||
itemvariation__item=old_instance.item,
|
||||
quota__subevent=old_instance.subevent,
|
||||
).values('quota_id'))
|
||||
)
|
||||
else:
|
||||
quotas |= set(old_instance.item.quotas.filter(subevent=old_instance.subevent))
|
||||
return quotas
|
||||
|
||||
@staticmethod
|
||||
def clean_quota_check(data, cnt, old_instance, event, quota, item, variation):
|
||||
@@ -461,8 +453,22 @@ class Voucher(LoggedModel):
|
||||
if event.has_subevents and data.get('block_quota') and not data.get('subevent'):
|
||||
raise ValidationError(_('If you want this voucher to block quota, you need to select a specific date.'))
|
||||
|
||||
new_quotas = Voucher.get_affected_quotas(quota, item, variation, data.get('subevent'))
|
||||
if not new_quotas:
|
||||
if quota:
|
||||
new_quotas = {quota}
|
||||
elif item and variation:
|
||||
new_quotas = set(variation.quotas.filter(subevent=data.get('subevent')))
|
||||
elif item and not item.has_variations:
|
||||
new_quotas = set(item.quotas.filter(subevent=data.get('subevent')))
|
||||
elif item and item.has_variations:
|
||||
new_quotas = set(
|
||||
Quota.objects.filter(
|
||||
pk__in=Quota.variations.through.objects.filter(
|
||||
itemvariation__item=item,
|
||||
quota__subevent=data.get('subevent'),
|
||||
).values('quota_id')
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
|
||||
'tickets.'))
|
||||
|
||||
@@ -638,16 +644,3 @@ class Voucher(LoggedModel):
|
||||
]
|
||||
).aggregate(s=Sum('voucher_budget_use'))['s'] or Decimal('0.00')
|
||||
return ops
|
||||
|
||||
|
||||
@dataclass
|
||||
class VoucherBulkData:
|
||||
item: object
|
||||
variation: object
|
||||
quota: object
|
||||
block_quota: bool
|
||||
valid_until: datetime.datetime
|
||||
subevent: object
|
||||
redeemed: int
|
||||
max_usages: int
|
||||
allow_ignore_quota: bool
|
||||
|
||||
@@ -1706,58 +1706,6 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
)
|
||||
|
||||
|
||||
class BaseHistoricalPaymentProvider(BasePaymentProvider):
|
||||
"""
|
||||
Base class for payment providers that no longer exist but can't be deleted to make sure historical
|
||||
payments are shown correctly.
|
||||
|
||||
Subclasses are recommended to only implement:
|
||||
- identifier
|
||||
- verbose_name
|
||||
- public_name
|
||||
- payment_control_render
|
||||
- payment_control_render_short
|
||||
- refund_control_render
|
||||
- refund_control_render_short
|
||||
- render_invoice_text
|
||||
- render_invoice_stamp
|
||||
- api_payment_details
|
||||
- api_refund_details
|
||||
- shred_payment_info
|
||||
- matching_id
|
||||
- refund_matching_id
|
||||
"""
|
||||
|
||||
@property
|
||||
def is_enabled(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def settings_form_fields(self) -> dict:
|
||||
return {}
|
||||
|
||||
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
|
||||
return False
|
||||
|
||||
def payment_is_valid_session(self, request: HttpRequest, payment: OrderPayment):
|
||||
return False
|
||||
|
||||
def order_change_allowed(self, order: Order, request: HttpRequest=None) -> bool:
|
||||
return False
|
||||
|
||||
def payment_refund_supported(self, payment: OrderPayment) -> bool:
|
||||
return False
|
||||
|
||||
def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
|
||||
return False
|
||||
|
||||
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
|
||||
raise PaymentException(_("This payment provider exists for historical purposes only and is no longer usable."))
|
||||
|
||||
def execute_refund(self, refund: OrderRefund):
|
||||
raise PaymentException(_("This payment provider exists for historical purposes only and is no longer usable."))
|
||||
|
||||
|
||||
@receiver(register_payment_providers, dispatch_uid="payment_free")
|
||||
def register_payment_provider(sender, **kwargs):
|
||||
return [FreeOrderProvider, BoxOfficeProvider, OffsettingProvider, ManualPayment, GiftCardPayment]
|
||||
|
||||
@@ -535,9 +535,8 @@ EventPluginRegistry = PluginAwareRegistry # for backwards compatibility
|
||||
event_live_issues = EventPluginSignal()
|
||||
"""
|
||||
This signal is sent out to determine whether an event can be taken live. If you want to
|
||||
prevent the event from going live, return an error message to display to the user (either
|
||||
as a SafeString containing HTML, or a string that will be HTML-escaped). If you don't,
|
||||
your receiver should return ``None``.
|
||||
prevent the event from going live, return a string that will be displayed to the user
|
||||
as the error message. If you don't, your receiver should return ``None``.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
import importlib
|
||||
|
||||
from django import template
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from pretix.base.models import Event
|
||||
@@ -45,7 +44,7 @@ def eventsignal(event: Event, signame: str, **kwargs):
|
||||
_html = []
|
||||
for receiver, response in signal.send(event, **kwargs):
|
||||
if response:
|
||||
_html.append(conditional_escape(response))
|
||||
_html.append(response)
|
||||
return mark_safe("".join(_html))
|
||||
|
||||
|
||||
@@ -64,5 +63,5 @@ def signal(signame: str, request, **kwargs):
|
||||
_html = []
|
||||
for receiver, response in signal.send(request, **kwargs):
|
||||
if response:
|
||||
_html.append(conditional_escape(response))
|
||||
_html.append(response)
|
||||
return mark_safe("".join(_html))
|
||||
|
||||
@@ -27,11 +27,11 @@ from django.template import TemplateDoesNotExist, loader
|
||||
from django.template.loader import get_template
|
||||
from django.utils.functional import Promise
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.decorators.csrf import requires_csrf_token
|
||||
from sentry_sdk import last_event_id
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.middleware import get_language_from_request
|
||||
from pretix.multidomain.middlewares import requires_csrf_token
|
||||
|
||||
|
||||
def csrf_failure(request, reason=""):
|
||||
|
||||
@@ -28,6 +28,10 @@ from django.dispatch import receiver
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.settings")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from pretix.settings import LOGGING
|
||||
|
||||
LOGGING['formatters']['default']['format'] = '[%(asctime)s: %(levelname)s/%(processName)s] RequestId=%(request_id)s %(name)s %(module)s %(message)s'
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
app = Celery('pretix')
|
||||
@@ -59,7 +63,7 @@ def on_task_prerun(sender, task_id, task, **kwargs):
|
||||
from pretix.helpers.logs import local
|
||||
|
||||
local.request_id = task_id
|
||||
if task.request.headers and "X-Pretix-Trace" in task.request.headers:
|
||||
if "X-Pretix-Trace" in task.request.headers:
|
||||
local.trace = task.request.headers["X-Pretix-Trace"].split(" ")
|
||||
else:
|
||||
local.trace = []
|
||||
|
||||
@@ -1905,6 +1905,12 @@ class QuickSetupForm(I18nForm):
|
||||
required=False,
|
||||
help_text=_("We'll show this publicly to allow attendees to contact you.")
|
||||
)
|
||||
contact_url = forms.URLField(
|
||||
label=_("Contact URL"),
|
||||
required=False,
|
||||
help_text=_("If you set this, the footer contact link will point here instead of using the email address above. "
|
||||
"Please note that you still need to add a contact email address that will be shared with all emails you send.")
|
||||
)
|
||||
total_quota = forms.IntegerField(
|
||||
label=_("Total capacity"),
|
||||
min_value=0,
|
||||
|
||||
@@ -32,20 +32,16 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import copy
|
||||
import csv
|
||||
from collections import Counter, namedtuple
|
||||
from collections import namedtuple
|
||||
from io import StringIO
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.validators import EmailValidator
|
||||
from django.db.models import Count, F, Max
|
||||
from django.db.models.functions import Upper
|
||||
from django.forms.utils import ErrorDict
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes.forms import SafeModelChoiceField
|
||||
|
||||
from pretix.base.email import get_available_placeholders
|
||||
@@ -54,10 +50,7 @@ from pretix.base.forms import (
|
||||
)
|
||||
from pretix.base.forms.widgets import format_placeholders_help_text
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Item, ItemVariation, Quota, SubEvent, Voucher
|
||||
from pretix.base.models.vouchers import VoucherBulkData
|
||||
from pretix.base.services.locking import lock_objects
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.models import Item, Voucher
|
||||
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
|
||||
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
||||
from pretix.control.signals import voucher_form_validation
|
||||
@@ -112,22 +105,20 @@ class VoucherForm(I18nModelForm):
|
||||
except Item.DoesNotExist:
|
||||
pass
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.event and self.instance:
|
||||
self.event = self.instance.event
|
||||
|
||||
self.fields['tag'].widget.attrs['data-typeahead-url'] = reverse('control:event.vouchers.tags.typeahead', kwargs={
|
||||
'event': self.event.slug,
|
||||
'organizer': self.event.organizer.slug,
|
||||
'event': instance.event.slug,
|
||||
'organizer': instance.event.organizer.slug,
|
||||
})
|
||||
|
||||
if self.event.has_subevents:
|
||||
self.fields['subevent'].queryset = self.event.subevents.all()
|
||||
if instance.event.has_subevents:
|
||||
self.fields['subevent'].queryset = instance.event.subevents.all()
|
||||
self.fields['subevent'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'event',
|
||||
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
||||
'event': self.event.slug,
|
||||
'organizer': self.event.organizer.slug,
|
||||
'event': instance.event.slug,
|
||||
'organizer': instance.event.organizer.slug,
|
||||
}),
|
||||
}
|
||||
)
|
||||
@@ -137,19 +128,18 @@ class VoucherForm(I18nModelForm):
|
||||
del self.fields['subevent']
|
||||
|
||||
choices = []
|
||||
prefix = (self.prefix + '-') if self.prefix else ''
|
||||
if 'itemvar' in initial or (self.data and prefix + 'itemvar' in self.data):
|
||||
iv = self.data.get(prefix + 'itemvar', '') or initial.get('itemvar', '') or ''
|
||||
if 'itemvar' in initial or (self.data and 'itemvar' in self.data):
|
||||
iv = self.data.get('itemvar') or initial.get('itemvar', '')
|
||||
if iv.startswith('q-'):
|
||||
q = self.event.quotas.get(pk=iv[2:])
|
||||
q = self.instance.event.quotas.get(pk=iv[2:])
|
||||
choices.append(('q-%d' % q.pk, _('Any product in quota "{quota}"').format(quota=q)))
|
||||
elif '-' in iv:
|
||||
itemid, varid = iv.split('-')
|
||||
i = self.event.items.get(pk=itemid)
|
||||
i = self.instance.event.items.get(pk=itemid)
|
||||
v = i.variations.get(pk=varid)
|
||||
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (str(i), v.value)))
|
||||
elif iv:
|
||||
i = self.event.items.get(pk=iv)
|
||||
i = self.instance.event.items.get(pk=iv)
|
||||
if i.variations.exists():
|
||||
choices.append((str(i.pk), _('{product} – Any variation').format(product=i)))
|
||||
else:
|
||||
@@ -160,8 +150,8 @@ class VoucherForm(I18nModelForm):
|
||||
attrs={
|
||||
'data-model-select2': 'generic',
|
||||
'data-select2-url': reverse('control:event.vouchers.itemselect2', kwargs={
|
||||
'event': self.event.slug,
|
||||
'organizer': self.event.organizer.slug,
|
||||
'event': instance.event.slug,
|
||||
'organizer': instance.event.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': _('All products')
|
||||
}
|
||||
@@ -169,7 +159,7 @@ class VoucherForm(I18nModelForm):
|
||||
self.fields['itemvar'].required = False
|
||||
self.fields['itemvar'].widget.choices = self.fields['itemvar'].choices
|
||||
|
||||
if self.event.seating_plan or self.event.subevents.filter(seating_plan__isnull=False).exists():
|
||||
if self.instance.event.seating_plan or self.instance.event.subevents.filter(seating_plan__isnull=False).exists():
|
||||
self.fields['seat'] = forms.CharField(
|
||||
label=_("Specific seat ID"),
|
||||
max_length=255,
|
||||
@@ -179,45 +169,40 @@ class VoucherForm(I18nModelForm):
|
||||
help_text=str(self.instance.seat) if self.instance.seat else '',
|
||||
)
|
||||
|
||||
def parse_itemvar(self, data):
|
||||
try:
|
||||
itemid = quotaid = None
|
||||
iv = data.get('itemvar', '')
|
||||
if iv.startswith('q-'):
|
||||
quotaid = iv[2:]
|
||||
elif '-' in iv:
|
||||
itemid, varid = iv.split('-')
|
||||
elif iv:
|
||||
itemid, varid = iv, None
|
||||
else:
|
||||
itemid, varid = None, None
|
||||
|
||||
if itemid:
|
||||
item = self.event.items.get(pk=itemid)
|
||||
if varid:
|
||||
variation = item.variations.get(pk=varid)
|
||||
else:
|
||||
variation = None
|
||||
quota = None
|
||||
elif quotaid:
|
||||
quota = self.event.quotas.get(pk=quotaid)
|
||||
item = None
|
||||
variation = None
|
||||
else:
|
||||
quota = None
|
||||
item = None
|
||||
variation = None
|
||||
|
||||
return (item, variation, quota)
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError(_("Invalid product selected."))
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
|
||||
if not self._errors:
|
||||
self.instance.item, self.instance.variation, self.instance.quota = self.parse_itemvar(self.data)
|
||||
try:
|
||||
itemid = quotaid = None
|
||||
iv = self.data.get('itemvar', '')
|
||||
if iv.startswith('q-'):
|
||||
quotaid = iv[2:]
|
||||
elif '-' in iv:
|
||||
itemid, varid = iv.split('-')
|
||||
elif iv:
|
||||
itemid, varid = iv, None
|
||||
else:
|
||||
itemid, varid = None, None
|
||||
|
||||
if itemid:
|
||||
self.instance.item = self.instance.event.items.get(pk=itemid)
|
||||
if varid:
|
||||
self.instance.variation = self.instance.item.variations.get(pk=varid)
|
||||
else:
|
||||
self.instance.variation = None
|
||||
self.instance.quota = None
|
||||
elif quotaid:
|
||||
self.instance.quota = self.instance.event.quotas.get(pk=quotaid)
|
||||
self.instance.item = None
|
||||
self.instance.variation = None
|
||||
else:
|
||||
self.instance.quota = None
|
||||
self.instance.item = None
|
||||
self.instance.variation = None
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError(_("Invalid product selected."))
|
||||
|
||||
if 'codes' in data:
|
||||
data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a]
|
||||
@@ -229,7 +214,7 @@ class VoucherForm(I18nModelForm):
|
||||
|
||||
try:
|
||||
Voucher.clean_item_properties(
|
||||
data, self.event,
|
||||
data, self.instance.event,
|
||||
self.instance.quota, self.instance.item, self.instance.variation,
|
||||
seats_given=data.get('seat') or data.get('seats'),
|
||||
block_quota=data.get('block_quota')
|
||||
@@ -249,7 +234,7 @@ class VoucherForm(I18nModelForm):
|
||||
|
||||
try:
|
||||
Voucher.clean_subevent(
|
||||
data, self.event
|
||||
data, self.instance.event
|
||||
)
|
||||
except ValidationError as e:
|
||||
raise ValidationError({"subevent": e.message})
|
||||
@@ -265,19 +250,19 @@ class VoucherForm(I18nModelForm):
|
||||
if check_quota:
|
||||
Voucher.clean_quota_check(
|
||||
data, cnt, self.initial_instance_data,
|
||||
self.event, self.instance.quota, self.instance.item, self.instance.variation
|
||||
self.instance.event, self.instance.quota, self.instance.item, self.instance.variation
|
||||
)
|
||||
Voucher.clean_voucher_code(data, self.event, self.instance.pk)
|
||||
Voucher.clean_voucher_code(data, self.instance.event, self.instance.pk)
|
||||
if 'seat' in self.fields:
|
||||
if data.get('seat'):
|
||||
self.instance.seat = Voucher.clean_seat_id(
|
||||
data, self.instance.item, self.instance.quota, self.event, self.instance.pk
|
||||
data, self.instance.item, self.instance.quota, self.instance.event, self.instance.pk
|
||||
)
|
||||
self.instance.item = self.instance.seat.product
|
||||
else:
|
||||
self.instance.seat = None
|
||||
|
||||
voucher_form_validation.send(sender=self.event, form=self, data=data)
|
||||
voucher_form_validation.send(sender=self.instance.event, form=self, data=data)
|
||||
|
||||
return data
|
||||
|
||||
@@ -285,215 +270,6 @@ class VoucherForm(I18nModelForm):
|
||||
return super().save(commit)
|
||||
|
||||
|
||||
class VoucherBulkEditForm(VoucherForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.mixed_values = kwargs.pop('mixed_values')
|
||||
self.queryset = kwargs.pop('queryset')
|
||||
super().__init__(**kwargs)
|
||||
del self.fields["code"]
|
||||
self.fields.pop("seat", None)
|
||||
|
||||
def is_bulk_checked(self, fieldname):
|
||||
return self.prefix + fieldname in self.data.getlist('_bulk')
|
||||
|
||||
def clean(self):
|
||||
# We skip the parent class because it's not suited for bulk editing and implement custom validation here.
|
||||
# This does not validate *everything* we validate in VoucherForm. For example, we skip validation that one does
|
||||
# not create a voucher for an add-on product or that the seat matches the product to save on complexity.
|
||||
# This is a UX validation only anyway, since one could first create the voucher and then make the product an
|
||||
# add-on product. However, we need to validate everything that we don't want violated in the database.
|
||||
data = super(VoucherForm, self).clean()
|
||||
|
||||
if self.is_bulk_checked("itemvar"):
|
||||
data["item"], data["variation"], data["quota"] = self.parse_itemvar(data)
|
||||
|
||||
if self.is_bulk_checked("max_usages") and "max_usages" in data:
|
||||
max_redeemed = self.queryset.aggregate(m=Max("redeemed"))["m"]
|
||||
if data["max_usages"] < max_redeemed:
|
||||
raise ValidationError(_(
|
||||
"You cannot reduce the maximum number of redemptions to %(max_usages)s, because at least one "
|
||||
"of the selected vouchers has already been redeemed %(max_redeemed)s times."
|
||||
) % {"max_usages": data["max_usages"], "max_redeemed": max_redeemed})
|
||||
|
||||
# Check diff on product and quota usage based on old groups of vouchers
|
||||
if any(self.is_bulk_checked(k) for k in ("max_usages", "itemvar", "block_quota", "valid_until", "subevent")):
|
||||
quota_diff = Counter()
|
||||
|
||||
current_vouchers = self.queryset.order_by().values(
|
||||
"item", "variation", "quota", "block_quota", "valid_until", "subevent", "redeemed", "max_usages",
|
||||
"allow_ignore_quota",
|
||||
).annotate(c=Count("*"))
|
||||
item_cache = {i.pk: i for i in Item.objects.filter(pk__in=[c["item"] for c in current_vouchers])}
|
||||
var_cache = {v.pk: v for v in ItemVariation.objects.filter(pk__in=[c["variation"] for c in current_vouchers])}
|
||||
quota_cache = {q.pk: q for q in Quota.objects.filter(pk__in=[c["quota"] for c in current_vouchers])}
|
||||
subevent_cache = {s.pk: s for s in SubEvent.objects.filter(pk__in=[c["subevent"] for c in current_vouchers])}
|
||||
|
||||
for current in current_vouchers:
|
||||
bulk_count = current.pop('c')
|
||||
current = VoucherBulkData(**current)
|
||||
# Get quotas that are currently used
|
||||
if current.item:
|
||||
current.item = item_cache[current.item]
|
||||
if current.variation:
|
||||
current.variation = var_cache[current.variation]
|
||||
if current.quota:
|
||||
current.quota = quota_cache[current.quota]
|
||||
if current.subevent:
|
||||
current.subevent = subevent_cache[current.subevent]
|
||||
|
||||
old_quotas = Voucher.clean_quota_get_ignored(current)
|
||||
old_amount = max(current.max_usages - current.redeemed, 0) * bulk_count
|
||||
|
||||
# Predict state after change
|
||||
after_change = copy.copy(current)
|
||||
if self.is_bulk_checked("itemvar") and "itemvar" in data:
|
||||
after_change.item = data["item"]
|
||||
after_change.variation = data["variation"]
|
||||
after_change.quota = data["quota"]
|
||||
if self.is_bulk_checked("subevent") and "subevent" in data:
|
||||
after_change.subevent = data["subevent"]
|
||||
if self.is_bulk_checked("max_usages") and "max_usages" in data:
|
||||
after_change.max_usages = data["max_usages"]
|
||||
if self.is_bulk_checked("block_quota") and "block_quota" in data:
|
||||
after_change.block_quota = data["block_quota"]
|
||||
if self.is_bulk_checked("valid_until") and "valid_until" in data:
|
||||
after_change.valid_until = data["valid_until"]
|
||||
if self.is_bulk_checked("allow_ignore_quota") and "allow_ignore_quota" in data:
|
||||
after_change.allow_ignore_quota = data["allow_ignore_quota"]
|
||||
|
||||
if after_change.quota and self.event.has_subevents and not after_change.subevent:
|
||||
raise ValidationError(_("You cannot create a voucher that allows selection of a quota but has no date selected."))
|
||||
|
||||
if after_change.quota and after_change.subevent and after_change.quota.subevent_id != after_change.subevent.pk:
|
||||
raise ValidationError(_("The selected quota does not match the selected subevent."))
|
||||
|
||||
if after_change.block_quota and self.event.has_subevents and not after_change.subevent:
|
||||
raise ValidationError(
|
||||
_('If you want this voucher to block quota, you need to select a specific date.'))
|
||||
|
||||
if after_change.block_quota and not after_change.item and not after_change.quota:
|
||||
raise ValidationError(
|
||||
_('You need to select a specific product or quota if this voucher should reserve '
|
||||
'tickets.')
|
||||
)
|
||||
|
||||
if after_change.allow_ignore_quota:
|
||||
# todo: is this the most useful way to do this?
|
||||
continue
|
||||
|
||||
new_quotas = Voucher.clean_quota_get_ignored(after_change)
|
||||
new_amount = max(after_change.max_usages - after_change.redeemed, 0) * bulk_count
|
||||
|
||||
if new_quotas != old_quotas or new_amount != old_amount:
|
||||
for q in old_quotas:
|
||||
quota_diff[q] -= old_amount
|
||||
for q in new_quotas:
|
||||
quota_diff[q] += new_amount
|
||||
|
||||
if any(v > 0 for q, v in quota_diff.items()):
|
||||
lock_objects([q for q, v in quota_diff.items() if q.size is not None and v > 0], shared_lock_objects=[self.event])
|
||||
qa = QuotaAvailability(count_waitinglist=False)
|
||||
qa.queue(*(q for q, v in quota_diff.items() if v > 0))
|
||||
qa.compute()
|
||||
|
||||
if any(qa.results[q][0] != Quota.AVAILABILITY_OK or (qa.results[q][1] is not None and qa.results[q][1] < required)
|
||||
for q, required in quota_diff.items() if required > 0):
|
||||
raise ValidationError(_(
|
||||
'There is no sufficient quota available to perform this change.'
|
||||
))
|
||||
|
||||
has_seat = self.queryset.filter(seat__isnull=False).exists()
|
||||
if has_seat:
|
||||
if self.is_bulk_checked("max_usages"):
|
||||
raise ValidationError(_(
|
||||
'Changing the maximum number of usages in bulk is not supported if any of the selected vouchers '
|
||||
'is assigned a seat.'
|
||||
))
|
||||
if self.is_bulk_checked("subevent"):
|
||||
raise ValidationError(pgettext_lazy(
|
||||
'subevent',
|
||||
'Changing the date in bulk is not supported if any of the selected vouchers '
|
||||
'is assigned a seat.'
|
||||
))
|
||||
if self.is_bulk_checked("itemvar") and data["quota"]:
|
||||
raise ValidationError(_(
|
||||
'Changing the product to a quota is not supported if any of the selected vouchers '
|
||||
'is assigned a seat.'
|
||||
))
|
||||
|
||||
if self.is_bulk_checked("valid_until"):
|
||||
if data["valid_until"] is None or data["valid_until"] >= now():
|
||||
currently_not_blocked_seats = self.queryset.filter(
|
||||
seat__isnull=False,
|
||||
max_usages__gt=F("redeemed"),
|
||||
valid_until__lt=now(),
|
||||
)
|
||||
if self.event.has_subevents:
|
||||
subevents = self.event.subevents.filter(pk__in=currently_not_blocked_seats.values_list("subevent"))
|
||||
for se in subevents:
|
||||
conflicts = currently_not_blocked_seats.filter(
|
||||
subevent=se
|
||||
).exclude(
|
||||
seat_id__in=se.free_seats().values("pk")
|
||||
)
|
||||
if conflicts:
|
||||
raise ValidationError(_(
|
||||
'This change cannot be completed because not all assigned seats of the vouchers are '
|
||||
'still available'
|
||||
))
|
||||
else:
|
||||
conflicts = currently_not_blocked_seats.exclude(
|
||||
seat_id__in=self.event.free_seats().values("pk")
|
||||
)
|
||||
if conflicts:
|
||||
raise ValidationError(_(
|
||||
'This change cannot be completed because not all assigned seats of the vouchers are '
|
||||
'still available'
|
||||
))
|
||||
|
||||
return data
|
||||
|
||||
def save(self, commit=True):
|
||||
objs = list(self.queryset)
|
||||
fields = set()
|
||||
|
||||
check_map = {
|
||||
'price_mode': '__price',
|
||||
'value': '__price',
|
||||
}
|
||||
for k in self.fields:
|
||||
if not self.is_bulk_checked(check_map.get(k, k)):
|
||||
continue
|
||||
|
||||
if k == 'itemvar':
|
||||
fields.add("item")
|
||||
fields.add("variation")
|
||||
fields.add("quota")
|
||||
else:
|
||||
fields.add(k)
|
||||
for obj in objs:
|
||||
if k == 'itemvar':
|
||||
obj.item = self.cleaned_data["item"]
|
||||
obj.variation = self.cleaned_data["variation"]
|
||||
obj.quota = self.cleaned_data["quota"]
|
||||
else:
|
||||
setattr(obj, k, self.cleaned_data[k])
|
||||
|
||||
fields = [f for f in fields if f != 'itemvars']
|
||||
if fields:
|
||||
Voucher.objects.bulk_update(objs, fields, 200)
|
||||
|
||||
def full_clean(self):
|
||||
if len(self.data) == 0:
|
||||
# form wasn't submitted
|
||||
self._errors = ErrorDict()
|
||||
return
|
||||
super().full_clean()
|
||||
|
||||
def _post_clean(self):
|
||||
pass # skip model-level clean
|
||||
|
||||
|
||||
class VoucherBulkForm(VoucherForm):
|
||||
codes = forms.CharField(
|
||||
widget=forms.Textarea,
|
||||
|
||||
@@ -37,7 +37,7 @@ from urllib.parse import quote, urljoin, urlparse
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, resolve_url
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import get_script_prefix, resolve, reverse
|
||||
@@ -98,8 +98,6 @@ class PermissionMiddleware:
|
||||
super().__init__()
|
||||
|
||||
def _login_redirect(self, request):
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
|
||||
# Taken from django/contrib/auth/decorators.py
|
||||
path = request.build_absolute_uri()
|
||||
# urlparse chokes on lazy objects in Python 3, force to str
|
||||
@@ -112,21 +110,10 @@ class PermissionMiddleware:
|
||||
if ((not login_scheme or login_scheme == current_scheme) and
|
||||
(not login_netloc or login_netloc == current_netloc)):
|
||||
path = request.get_full_path()
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
# It's not useful to return a 302 redirect on a XMLHttpRequest request, because
|
||||
# the XMLHttpRequest is unable to detect redirects.
|
||||
return HttpResponse(
|
||||
"Authentication required",
|
||||
status=401,
|
||||
headers={
|
||||
# Appending ?next= is handled by client, because it should be the top-level context url,
|
||||
# not the URL called in the background
|
||||
"X-Login-Url": resolved_login_url,
|
||||
}
|
||||
)
|
||||
|
||||
return redirect_to_login(path, resolved_login_url, REDIRECT_FIELD_NAME)
|
||||
return redirect_to_login(
|
||||
path, resolved_login_url, REDIRECT_FIELD_NAME)
|
||||
|
||||
def __call__(self, request):
|
||||
url = resolve(request.path_info)
|
||||
|
||||
@@ -39,8 +39,7 @@ from pretix.base.signals import (
|
||||
html_page_start = GlobalSignal()
|
||||
"""
|
||||
This signal allows you to put code in the beginning of the main page for every
|
||||
page in the backend. You are expected to return a SafeString containing HTML, or
|
||||
a string that will be HTML-escaped.
|
||||
page in the backend. You are expected to return HTML.
|
||||
|
||||
The ``sender`` keyword argument will contain the request.
|
||||
"""
|
||||
@@ -130,7 +129,7 @@ event_dashboard_top = EventPluginSignal()
|
||||
Arguments: 'request'
|
||||
|
||||
This signal is sent out to include custom HTML in the top part of the the event dashboard.
|
||||
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
Receivers should return HTML.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
An additional keyword argument ``subevent`` *can* contain a sub-event.
|
||||
@@ -173,7 +172,6 @@ Arguments: 'form'
|
||||
|
||||
This signal allows you to add additional HTML to the form that is used for modifying vouchers.
|
||||
You receive the form object in the ``form`` keyword argument.
|
||||
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
@@ -211,7 +209,6 @@ Arguments: 'quota'
|
||||
|
||||
This signal allows you to append HTML to a Quota's detail view. You receive the
|
||||
quota as argument in the ``quota`` keyword argument.
|
||||
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
@@ -222,7 +219,6 @@ Arguments: 'subevent'
|
||||
|
||||
This signal allows you to append HTML to a SubEvent's detail view. You receive the
|
||||
subevent as argument in the ``subevent`` keyword argument.
|
||||
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
@@ -269,8 +265,7 @@ order_info = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``order``, ``request``
|
||||
|
||||
This signal is sent out to display additional information on the order detail page.
|
||||
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
This signal is sent out to display additional information on the order detail page
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
Additionally, the argument ``order`` and ``request`` are available.
|
||||
@@ -280,8 +275,7 @@ order_approve_info = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``order``, ``request``
|
||||
|
||||
This signal is sent out to display additional information on the order approve page.
|
||||
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
This signal is sent out to display additional information on the order approve page
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
Additionally, the argument ``order`` and ``request`` are available.
|
||||
@@ -292,7 +286,6 @@ order_position_buttons = EventPluginSignal()
|
||||
Arguments: ``order``, ``position``, ``request``
|
||||
|
||||
This signal is sent out to display additional buttons for a single position of an order.
|
||||
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
Additionally, the argument ``order`` and ``request`` are available.
|
||||
@@ -322,7 +315,6 @@ Arguments: 'request'
|
||||
|
||||
This signal is sent out to include template snippets on the settings page of an event
|
||||
that allows generating a pretix Widget code.
|
||||
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
A second keyword argument ``request`` will contain the request object.
|
||||
|
||||
@@ -56,4 +56,5 @@
|
||||
</form>
|
||||
<script type="text/plain" id="good_origin">{{ good_origin }}</script>
|
||||
<script type="text/plain" id="bad_origin_report_url">{{ bad_origin_report_url }}</script>
|
||||
<!-- pretix-login-marker -->{# marker required for ajax calls to detect that user session is over #}
|
||||
{% endblock %}
|
||||
|
||||
@@ -50,46 +50,6 @@
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if dkim_warning %}
|
||||
<div class="alert alert-danger">
|
||||
<p>
|
||||
{{ dkim_warning }}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "Your new DKIM record should be set up as a CNAME record like this:" %}
|
||||
</p>
|
||||
<pre><code>{{ dkim_hostname }} CNAME {{ dkim_cname }}</code></pre>
|
||||
<p>
|
||||
{% trans "Please keep in mind that updates to DNS might require multiple hours to take effect." %}
|
||||
</p>
|
||||
</div>
|
||||
{% elif dkim_cname %}
|
||||
<div class="alert alert-success">
|
||||
{% blocktrans trimmed %}
|
||||
We found a DKIM record on your domain for this system. Great!
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if dmarc_warning %}
|
||||
<div class="alert alert-danger">
|
||||
<p>
|
||||
{{ dmarc_warning }}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "Your new DMARC record could look like this:" %}
|
||||
</p>
|
||||
<pre><code>_dmarc.{{ hostname }} TXT "v=DMARC1; p=quarantine; sp=none; adkim=r; aspf=r;"</code></pre>
|
||||
<p>
|
||||
{% trans "Please keep in mind that updates to DNS might require multiple hours to take effect." %}
|
||||
</p>
|
||||
</div>
|
||||
{% elif dkim_cname %}
|
||||
<div class="alert alert-success">
|
||||
{% blocktrans trimmed %}
|
||||
We found a DMARC record on your domain for this system. Great!
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if verification %}
|
||||
<h3>{% trans "Verification" %}</h3>
|
||||
<p>
|
||||
@@ -110,7 +70,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if spf_warning or dkim_warning or dmarc_warning %}
|
||||
{% if spf_warning %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="" class="btn btn-default btn-save">
|
||||
{% trans "Cancel" %}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</p>
|
||||
<ul>
|
||||
{% for issue in issues %}
|
||||
<li>{{ issue }}</li>
|
||||
<li>{{ issue|safe }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@
|
||||
</p>
|
||||
<ul>
|
||||
{% for issue in issues %}
|
||||
<li>{{ issue }}</li>
|
||||
<li>{{ issue|safe }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% bootstrap_field form.contact_mail layout="control" %}
|
||||
{% bootstrap_field form.contact_url layout="control" %}
|
||||
{% bootstrap_field form.imprint_url layout="control" %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -560,10 +560,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/plain" id="schema-url">{% static "schema/pdf-layout.schema.json" %}</script>
|
||||
<script type="text/javascript" src="{% static "pdfjs/pdf.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "ajv/ajv2020.bundle.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "fabric/fabric.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/editor.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "schema/pdf-layout.validate.js" %}"></script>
|
||||
<img src="{% static 'pretixpresale/pdf/powered_by_pretix_dark.png' %}" id="poweredby-dark" class="sr-only">
|
||||
<img src="{% static 'pretixpresale/pdf/powered_by_pretix_white.png' %}" id="poweredby-white" class="sr-only">
|
||||
{% for family, styles in fonts.items %}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load eventsignal %}
|
||||
{% load eventurl %}
|
||||
{% block title %}{% trans "Change multiple vouchers" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>
|
||||
{% trans "Change multiple vouchers" %}
|
||||
<small>
|
||||
{% blocktrans trimmed with number=vouchers.count %}
|
||||
{{ number }} selected
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="hidden">
|
||||
{% for v in vouchers %}
|
||||
<input type="hidden" name="voucher" value="{{ v.pk }}">
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Voucher details" %}</legend>
|
||||
{% bootstrap_field form.max_usages layout="bulkedit" %}
|
||||
{% bootstrap_field form.valid_until layout="bulkedit" %}
|
||||
{% bootstrap_field form.itemvar layout="bulkedit" %}
|
||||
|
||||
<div class="bulk-edit-field-group">
|
||||
<label class="field-toggle">
|
||||
<input type="checkbox" name="_bulk" value="{{ form.prefix }}__price" {% if form.prefix|add:"__price" in bulk_selected %}checked{% endif %}>
|
||||
{% trans "change" context "form_bulk" %}
|
||||
</label>
|
||||
<div class="field-content">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_tag">{% trans "Price effect" %}</label>
|
||||
<div class="col-md-5">
|
||||
{% bootstrap_field form.price_mode show_label=False form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.value show_label=False form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<div class="controls">
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
If you choose "any product" for a specific quota and choose to reserve quota for this
|
||||
voucher above, the product can still be unavailable to the voucher holder if another quota
|
||||
associated with the product is sold out!
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if form.subevent %}
|
||||
{% bootstrap_field form.subevent layout="bulkedit" %}
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Advanced settings" %}</legend>
|
||||
{% bootstrap_field form.block_quota layout="bulkedit" %}
|
||||
{% bootstrap_field form.allow_ignore_quota layout="bulkedit" %}
|
||||
{% bootstrap_field form.min_usages layout="bulkedit" %}
|
||||
{% bootstrap_field form.budget addon_after=request.event.currency layout="bulkedit" %}
|
||||
{% bootstrap_field form.tag layout="bulkedit" %}
|
||||
{% bootstrap_field form.comment layout="bulkedit" %}
|
||||
{% bootstrap_field form.show_hidden_items layout="bulkedit" %}
|
||||
{% bootstrap_field form.all_addons_included layout="bulkedit" %}
|
||||
{% bootstrap_field form.all_bundles_included layout="bulkedit" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -99,9 +99,6 @@
|
||||
</p>
|
||||
<form action="{% url "control:event.vouchers.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
{% for field in filter_form %}
|
||||
{{ field.as_hidden }}
|
||||
{% endfor %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
@@ -147,18 +144,6 @@
|
||||
{% endif %}
|
||||
<th></th>
|
||||
</tr>
|
||||
{% if "event.vouchers:write" in request.eventpermset and page_obj.paginator.num_pages > 1 %}
|
||||
<tr class="table-select-all warning hidden">
|
||||
<td>
|
||||
<input type="checkbox" name="__ALL" id="__all" data-results-total="{{ page_obj.paginator.count }}">
|
||||
</td>
|
||||
<td colspan="5">
|
||||
<label for="__all">
|
||||
{% trans "Select all results on other pages as well" %}
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for v in vouchers %}
|
||||
@@ -226,10 +211,6 @@
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
{% trans "Delete selected" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary btn-save" name="action" value="edit"
|
||||
formaction="{% url "control:event.vouchers.bulkedit" organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||
<i class="fa fa-edit"></i>{% trans "Edit selected" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
@@ -49,11 +49,11 @@
|
||||
<td>
|
||||
<strong>
|
||||
{% if t.tag %}
|
||||
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?filter-tag={{ '"'|add:t.tag|add:'"'|urlencode }}">
|
||||
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?tag={{ '"'|add:t.tag|add:'"'|urlencode }}">
|
||||
{{ t.tag }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?filter-tag={{ '<>'|urlencode }}">
|
||||
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?tag={{ '<>'|urlencode }}">
|
||||
{% trans "Empty tag" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -383,7 +383,6 @@ urlpatterns = [
|
||||
re_path(r'^vouchers/bulk_add$', vouchers.VoucherBulkCreate.as_view(), name='event.vouchers.bulk'),
|
||||
re_path(r'^vouchers/bulk_add/mail_preview$', vouchers.VoucherBulkMailPreview.as_view(), name='event.vouchers.bulk.mail_preview'),
|
||||
re_path(r'^vouchers/bulk_action$', vouchers.VoucherBulkAction.as_view(), name='event.vouchers.bulkaction'),
|
||||
re_path(r'^vouchers/bulk_edit$', vouchers.VoucherBulkUpdateView.as_view(), name='event.vouchers.bulkedit'),
|
||||
re_path(r'^vouchers/import/$', modelimport.VoucherImportView.as_view(), name='event.vouchers.import'),
|
||||
re_path(r'^vouchers/import/(?P<file>[^/]+)/$', modelimport.VoucherProcessView.as_view(), name='event.vouchers.import.process'),
|
||||
re_path(r'^orders/(?P<code>[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(),
|
||||
|
||||
@@ -243,8 +243,7 @@ def invite(request, token):
|
||||
if request.user.is_authenticated:
|
||||
if inv.team.members.filter(pk=request.user.pk).exists():
|
||||
messages.error(request, _('You cannot accept the invitation for "{}" as you already are part of '
|
||||
'this team. If you want to add a different user or create a new account, '
|
||||
'log out and click the invitation link again.').format(inv.team.name))
|
||||
'this team.').format(inv.team.name))
|
||||
return redirect('control:index')
|
||||
else:
|
||||
with transaction.atomic():
|
||||
|
||||
@@ -952,12 +952,7 @@ class MailSettingsRendererPreview(MailSettingsPreview):
|
||||
context=context,
|
||||
)
|
||||
r = HttpResponse(v, content_type='text/html')
|
||||
r['Content-Security-Policy'] = (
|
||||
# Plugin-provided email templates will contain inline styles or remote images
|
||||
# but emails should not contain JS
|
||||
"style-src 'unsafe-inline'; "
|
||||
"img-src https: data:"
|
||||
)
|
||||
r._csp_ignore = True
|
||||
return r
|
||||
else:
|
||||
raise Http404(_('Unknown email renderer.'))
|
||||
@@ -1433,16 +1428,11 @@ class TaxUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, UpdateView
|
||||
form.instance.custom_rules = json.dumps([
|
||||
f.cleaned_data for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
|
||||
], cls=I18nJSONEncoder)
|
||||
if form.has_changed() or self.formset.has_changed():
|
||||
change_data = {
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
if self.formset.has_changed():
|
||||
change_data["custom_rules"] = [
|
||||
f.cleaned_data for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
|
||||
]
|
||||
if form.has_changed():
|
||||
self.object.log_action(
|
||||
'pretix.event.taxrule.changed', user=self.request.user, data=change_data
|
||||
'pretix.event.taxrule.changed', user=self.request.user, data={
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
)
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
@@ -41,30 +41,6 @@ from pretix.control.forms.mailsetup import SimpleMailForm, SMTPMailForm
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_cname_record(hostname):
|
||||
try:
|
||||
r = dns.resolver.Resolver()
|
||||
answers = r.resolve(hostname, 'CNAME')
|
||||
answers = list(answers)
|
||||
if len(answers) != 1:
|
||||
logger.exception('Found multiple CNAME records for {}'.format(hostname))
|
||||
return
|
||||
return str(answers[0].target).lower()
|
||||
except:
|
||||
logger.exception('Could not fetch CNAME record for {}'.format(hostname))
|
||||
|
||||
|
||||
def get_dmarc_record(hostname):
|
||||
try:
|
||||
r = dns.resolver.Resolver()
|
||||
for resp in r.resolve("_dmarc." + hostname, 'TXT'):
|
||||
data = b''.join(resp.strings).decode()
|
||||
if 'DMARC1' in data.strip():
|
||||
return data
|
||||
except:
|
||||
logger.exception("Could not fetch DMARC record for {}".format(hostname))
|
||||
|
||||
|
||||
def get_spf_record(hostname):
|
||||
try:
|
||||
r = dns.resolver.Resolver()
|
||||
@@ -73,7 +49,7 @@ def get_spf_record(hostname):
|
||||
if data.lower().strip().startswith('v=spf1 '): # RFC7208, section 4.5
|
||||
return data
|
||||
except:
|
||||
logger.exception("Could not fetch SPF record for {}".format(hostname))
|
||||
logger.exception("Could not fetch SPF record")
|
||||
|
||||
|
||||
def _check_spf_record(not_found_lookup_parts, spf_record, depth):
|
||||
@@ -192,15 +168,10 @@ class MailSettingsSetupView(TemplateView):
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
session_key = f'sender_mail_verification_code_{self.request.path}_{self.simple_form.cleaned_data.get("mail_from")}'
|
||||
verify_dns = (
|
||||
settings.MAIL_CUSTOM_SENDER_SPF_STRING or
|
||||
(settings.MAIL_CUSTOM_SENDER_DKIM_CNAME and settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR) or
|
||||
settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED
|
||||
)
|
||||
allow_save = (
|
||||
(not settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED or
|
||||
('verification' in self.request.POST and self.request.POST.get('verification', '') == self.request.session.get(session_key, None))) and
|
||||
(not verify_dns or self.request.POST.get('state') == 'save')
|
||||
(not settings.MAIL_CUSTOM_SENDER_SPF_STRING or self.request.POST.get('state') == 'save')
|
||||
)
|
||||
|
||||
if allow_save:
|
||||
@@ -221,8 +192,8 @@ class MailSettingsSetupView(TemplateView):
|
||||
|
||||
spf_warning = None
|
||||
spf_record = None
|
||||
hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1]
|
||||
if settings.MAIL_CUSTOM_SENDER_SPF_STRING:
|
||||
hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1]
|
||||
spf_record = get_spf_record(hostname)
|
||||
if not spf_record:
|
||||
spf_warning = _(
|
||||
@@ -239,43 +210,7 @@ class MailSettingsSetupView(TemplateView):
|
||||
'this system in the SPF record.'
|
||||
)
|
||||
|
||||
dkim_warning = None
|
||||
dkim_hostname = None
|
||||
dkim_cname = None
|
||||
if settings.MAIL_CUSTOM_SENDER_DKIM_CNAME and settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR:
|
||||
dkim_hostname = settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR + '._domainkey.' + hostname
|
||||
cname_target = get_cname_record(dkim_hostname)
|
||||
dkim_cname = settings.MAIL_CUSTOM_SENDER_DKIM_CNAME
|
||||
if not dkim_cname.endswith("."):
|
||||
dkim_cname += "."
|
||||
if "%s" in dkim_cname:
|
||||
dkim_cname = dkim_cname.replace("%s", hostname.replace(".", "-").lower())
|
||||
if not cname_target:
|
||||
dkim_warning = _(
|
||||
'We could not find a CNAME record pointing to our DKIM key for domain you are trying to use. '
|
||||
'This means that there is a very high change most of the emails will be rejected or marked as '
|
||||
'spam. We strongly recommend setting up DKIM through a CNAME record. You can do so through the '
|
||||
'DNS settings at the provider you registered your domain with.'
|
||||
)
|
||||
elif cname_target != dkim_cname:
|
||||
dkim_warning = _(
|
||||
'We found a CNAME record for a DKIM key, but it is not pointing to the right location. '
|
||||
'This means that there is a very high chance most of the emails will be rejected or marked as '
|
||||
'spam. You should update the DNS settings of your domain.'
|
||||
)
|
||||
|
||||
dmarc_warning = None
|
||||
dmarc_record = None
|
||||
if settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED:
|
||||
dmarc_record = get_dmarc_record(hostname)
|
||||
if not dmarc_record:
|
||||
spf_warning = _(
|
||||
'We did not find DMARC record for your domain. This means that there is a very high chance '
|
||||
'most of the emails will be rejected or marked as spam. You should update the DNS settings '
|
||||
'of your domain.'
|
||||
)
|
||||
|
||||
verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning and not dkim_warning and not dmarc_warning
|
||||
verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning
|
||||
if verification:
|
||||
if 'verification' in self.request.POST:
|
||||
messages.error(request, _('The verification code was incorrect, please try again.'))
|
||||
@@ -306,12 +241,6 @@ class MailSettingsSetupView(TemplateView):
|
||||
'spf_warning': spf_warning,
|
||||
'spf_record': spf_record,
|
||||
'spf_key': settings.MAIL_CUSTOM_SENDER_SPF_STRING,
|
||||
'dkim_warning': dkim_warning,
|
||||
'dkim_hostname': dkim_hostname,
|
||||
'dkim_cname': dkim_cname,
|
||||
'dmarc_warning': dmarc_warning,
|
||||
'dmarc_record': dmarc_record,
|
||||
'hostname': hostname,
|
||||
'recp': self.simple_form.cleaned_data.get('mail_from')
|
||||
},
|
||||
using=self.template_engine,
|
||||
|
||||
@@ -70,6 +70,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
if 'placeholders' in request.GET:
|
||||
return self.get_placeholders_help(request)
|
||||
resp = super().get(request, *args, **kwargs)
|
||||
resp._csp_ignore = True
|
||||
return resp
|
||||
|
||||
def get_placeholders_help(self, request):
|
||||
|
||||
@@ -40,11 +40,9 @@ import bleach
|
||||
from defusedcsv import csv
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import (
|
||||
BadRequest, PermissionDenied, ValidationError,
|
||||
)
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.db import connection, transaction
|
||||
from django.db.models import Count, Exists, OuterRef, Sum
|
||||
from django.db.models import Exists, OuterRef, Sum
|
||||
from django.http import (
|
||||
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
|
||||
JsonResponse,
|
||||
@@ -57,7 +55,7 @@ from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import (
|
||||
CreateView, FormView, ListView, TemplateView, UpdateView, View,
|
||||
CreateView, ListView, TemplateView, UpdateView, View,
|
||||
)
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
@@ -72,9 +70,7 @@ 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
|
||||
from pretix.control.forms.filter import VoucherFilterForm, VoucherTagFilterForm
|
||||
from pretix.control.forms.vouchers import (
|
||||
VoucherBulkEditForm, VoucherBulkForm, VoucherForm,
|
||||
)
|
||||
from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.signals import voucher_form_class
|
||||
from pretix.control.views import PaginationMixin
|
||||
@@ -84,37 +80,7 @@ from pretix.helpers.models import modelcopy
|
||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
||||
|
||||
|
||||
class VoucherQueryMixin:
|
||||
|
||||
@cached_property
|
||||
def request_data(self):
|
||||
if self.request.method == "POST":
|
||||
return self.request.POST
|
||||
return self.request.GET
|
||||
|
||||
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.vouchers.exclude(
|
||||
Exists(WaitingListEntry.objects.filter(voucher_id=OuterRef('pk')))
|
||||
)
|
||||
if 'voucher' in self.request_data and '__ALL' not in self.request_data:
|
||||
qs = qs.filter(
|
||||
id__in=self.request_data.getlist('voucher')
|
||||
)
|
||||
elif self.request.method == 'GET' or '__ALL' in self.request_data:
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
else:
|
||||
raise BadRequest("No vouchers selected")
|
||||
|
||||
return qs
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return VoucherFilterForm(data=self.request_data, prefix='filter', event=self.request.event)
|
||||
|
||||
|
||||
class VoucherList(VoucherQueryMixin, PaginationMixin, EventPermissionRequiredMixin, ListView):
|
||||
class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
|
||||
model = Voucher
|
||||
context_object_name = 'vouchers'
|
||||
template_name = 'pretixcontrol/vouchers/index.html'
|
||||
@@ -122,15 +88,25 @@ class VoucherList(VoucherQueryMixin, PaginationMixin, EventPermissionRequiredMix
|
||||
|
||||
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
|
||||
def get_queryset(self):
|
||||
return Voucher.annotate_budget_used(super().get_queryset().select_related(
|
||||
qs = Voucher.annotate_budget_used(self.request.event.vouchers.exclude(
|
||||
Exists(WaitingListEntry.objects.filter(voucher_id=OuterRef('pk')))
|
||||
).select_related(
|
||||
'item', 'variation', 'seat'
|
||||
))
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['filter_form'] = self.filter_form
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return VoucherFilterForm(data=self.request.GET, event=self.request.event)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if request.GET.get("download", "") == "yes":
|
||||
return self._download_csv()
|
||||
@@ -317,12 +293,6 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
f.disabled = True
|
||||
return form
|
||||
|
||||
def get_form_kwargs(self):
|
||||
return {
|
||||
**super().get_form_kwargs(),
|
||||
"event": self.request.event,
|
||||
}
|
||||
|
||||
def get_object(self, queryset=None) -> VoucherForm:
|
||||
url = resolve(self.request.path_info)
|
||||
try:
|
||||
@@ -633,21 +603,26 @@ class VoucherRNG(EventPermissionRequiredMixin, View):
|
||||
})
|
||||
|
||||
|
||||
class VoucherBulkAction(VoucherQueryMixin, EventPermissionRequiredMixin, View):
|
||||
class VoucherBulkAction(EventPermissionRequiredMixin, View):
|
||||
permission = 'event.vouchers:write'
|
||||
|
||||
@cached_property
|
||||
def objects(self):
|
||||
return self.request.event.vouchers.filter(
|
||||
id__in=self.request.POST.getlist('voucher')
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
if request.POST.get('action') == 'delete':
|
||||
return render(request, 'pretixcontrol/vouchers/delete_bulk.html', {
|
||||
'allowed': self.get_queryset().filter(redeemed=0),
|
||||
'forbidden': self.get_queryset().exclude(redeemed=0),
|
||||
'allowed': self.objects.filter(redeemed=0),
|
||||
'forbidden': self.objects.exclude(redeemed=0),
|
||||
})
|
||||
elif request.POST.get('action') == 'delete_confirm':
|
||||
log_entries = []
|
||||
to_delete = []
|
||||
to_update = []
|
||||
for obj in self.get_queryset():
|
||||
for obj in self.objects:
|
||||
if obj.allow_delete():
|
||||
log_entries.append(obj.log_action('pretix.voucher.deleted', user=self.request.user, save=False))
|
||||
to_delete.append(obj.pk)
|
||||
@@ -657,14 +632,12 @@ class VoucherBulkAction(VoucherQueryMixin, EventPermissionRequiredMixin, View):
|
||||
'bulk': True
|
||||
}, save=False))
|
||||
obj.max_usages = min(obj.redeemed, obj.max_usages)
|
||||
to_update.append(obj)
|
||||
obj.save(update_fields=['max_usages'])
|
||||
|
||||
if to_delete:
|
||||
CartPosition.objects.filter(addon_to__voucher_id__in=to_delete).delete()
|
||||
CartPosition.objects.filter(voucher_id__in=to_delete).delete()
|
||||
Voucher.objects.filter(pk__in=to_delete).delete()
|
||||
if to_update:
|
||||
Voucher.objects.bulk_update(to_update, ['max_usages'])
|
||||
|
||||
LogEntry.bulk_create_and_postprocess(log_entries)
|
||||
messages.success(request, _('The selected vouchers have been deleted or disabled.'))
|
||||
@@ -675,117 +648,3 @@ class VoucherBulkAction(VoucherQueryMixin, EventPermissionRequiredMixin, View):
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
|
||||
class VoucherBulkUpdateView(VoucherQueryMixin, EventPermissionRequiredMixin, FormView):
|
||||
template_name = 'pretixcontrol/vouchers/bulk_edit.html'
|
||||
permission = 'event.vouchers:write'
|
||||
context_object_name = 'vouchers'
|
||||
form_class = VoucherBulkEditForm
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().prefetch_related(None).order_by()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return HttpResponse(status=405)
|
||||
|
||||
@cached_property
|
||||
def is_submitted(self):
|
||||
# Usually, django considers a form "bound" / "submitted" on every POST request. However, this view is always
|
||||
# called with POST method, even if just to pass the selection of objects to work on, so we want to modify
|
||||
# that behavior
|
||||
return '_bulk' in self.request.POST
|
||||
|
||||
def get_form_kwargs(self):
|
||||
initial = {}
|
||||
mixed_values = set()
|
||||
qs = self.get_queryset().annotate()
|
||||
|
||||
fields = (
|
||||
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment', 'max_usages',
|
||||
'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'all_addons_included', 'all_bundles_included',
|
||||
'budget',
|
||||
)
|
||||
for f in fields:
|
||||
existing_values = list(qs.order_by(f).values(f).annotate(c=Count('*')))
|
||||
if len(existing_values) == 1:
|
||||
initial[f] = existing_values[0][f]
|
||||
elif len(existing_values) > 1:
|
||||
mixed_values.add(f)
|
||||
if f == "max_usages":
|
||||
initial[f] = 1
|
||||
else:
|
||||
initial[f] = None
|
||||
|
||||
existing_values = list(qs.order_by("item", "variation", "quota").values("item", "variation", "quota").annotate(c=Count('*')))
|
||||
if len(existing_values) == 1:
|
||||
i = existing_values[0]
|
||||
if i["quota"]:
|
||||
initial["itemvar"] = f'q-{i["quota"]}'
|
||||
elif i["variation"]:
|
||||
initial["itemvar"] = f'{i["item"]}-{i["variation"]}'
|
||||
elif i["item"]:
|
||||
initial["itemvar"] = f'{i["item"]}'
|
||||
else:
|
||||
initial["itemvar"] = None
|
||||
elif len(existing_values) > 1:
|
||||
mixed_values.add("itemvar")
|
||||
initial["itemvar"] = None
|
||||
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['event'] = self.request.event
|
||||
kwargs['prefix'] = 'bulkedit'
|
||||
kwargs['initial'] = initial
|
||||
kwargs['queryset'] = self.get_queryset()
|
||||
kwargs['mixed_values'] = mixed_values
|
||||
if not self.is_submitted:
|
||||
kwargs['data'] = None
|
||||
kwargs['files'] = None
|
||||
return kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:event.vouchers', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
def form_valid(self, form):
|
||||
log_entries = []
|
||||
|
||||
# Main form
|
||||
form.save()
|
||||
data = {
|
||||
k: v
|
||||
for k, v in form.cleaned_data.items()
|
||||
if k in form.changed_data
|
||||
}
|
||||
data['_raw_bulk_data'] = self.request.POST.dict()
|
||||
for obj in self.get_queryset():
|
||||
log_entries.append(
|
||||
obj.log_action('pretix.voucher.changed', data=data, user=self.request.user, save=False)
|
||||
)
|
||||
|
||||
LogEntry.bulk_create_and_postprocess(log_entries)
|
||||
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['vouchers'] = self.get_queryset()
|
||||
ctx['bulk_selected'] = self.request.POST.getlist("_bulk")
|
||||
return ctx
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
is_valid = (
|
||||
self.is_submitted and
|
||||
form.is_valid()
|
||||
)
|
||||
if is_valid:
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
if self.is_submitted:
|
||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||
return self.form_invalid(form)
|
||||
|
||||
@@ -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/>.
|
||||
#
|
||||
import itertools
|
||||
import re
|
||||
from http.cookies import Morsel
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
@@ -50,41 +48,6 @@ def set_cookie_without_samesite(request, response, key, *args, **kwargs):
|
||||
response.cookies[key]['Partitioned'] = True
|
||||
|
||||
|
||||
def thoroughly_delete_cookie(response, cookie_name, **kwargs):
|
||||
""" Deletes different possible versions of a cookie (SameSite, Partitioned) """
|
||||
properties = {"SameSite": ["", 'None'], "Partitioned": ["", True]}
|
||||
for i, values in enumerate(itertools.product(*properties.values())):
|
||||
m = Morsel()
|
||||
m.set(cookie_name, '', '')
|
||||
m.update(kwargs)
|
||||
m.update(zip(properties.keys(), values))
|
||||
m['expires'] = "Thu, 01 Jan 1970 00:00:00 GMT"
|
||||
|
||||
response.cookies[f'___DELETECOOKIE__{i}___{cookie_name}'] = m
|
||||
|
||||
# Make sure settings a cookie afterwards will add a new item in the dictionary, placing
|
||||
# it below our deletion headers.
|
||||
response.cookies.pop(cookie_name, None)
|
||||
|
||||
|
||||
def get_all_values_of_cookie(cookie_header, cookie_name):
|
||||
# like django.http.cookie.parse_cookie, but returns all values of duplicated cookies instead of only the last
|
||||
values = list()
|
||||
if not cookie_header:
|
||||
return values
|
||||
for chunk in cookie_header.split(";"):
|
||||
if "=" in chunk:
|
||||
key, val = chunk.split("=", 1)
|
||||
else:
|
||||
# Assume an empty name per
|
||||
# https://bugzilla.mozilla.org/show_bug.cgi?id=169091
|
||||
key, val = "", chunk
|
||||
key, val = key.strip(), val.strip()
|
||||
if key == cookie_name:
|
||||
values.append(val)
|
||||
return values
|
||||
|
||||
|
||||
# Based on https://www.chromium.org/updates/same-site/incompatible-clients
|
||||
# Copyright 2019 Google LLC.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
+6653
-4597
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
|
||||
"PO-Revision-Date: 2026-07-13 11:28+0000\n"
|
||||
"Last-Translator: ahmed badr <ahmedbadr@oma.com.eg>\n"
|
||||
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
||||
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"ar/>\n"
|
||||
"Language: ar\n"
|
||||
@@ -18,7 +18,7 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
|
||||
"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
|
||||
"X-Generator: Weblate 2026.7.1\n"
|
||||
"X-Generator: Weblate 4.8\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
|
||||
msgid "Marked as paid"
|
||||
@@ -30,110 +30,112 @@ msgstr "تعليق:"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "PayPal"
|
||||
msgstr "باي بال"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "Venmo"
|
||||
msgstr "Venmo"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
#: pretix/static/pretixpresale/js/walletdetection.js
|
||||
msgid "Apple Pay"
|
||||
msgstr "Apple Pay"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "Itaú"
|
||||
msgstr "إيتاو"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "PayPal Credit"
|
||||
msgstr "الائتمان من PayPal"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "Credit Card"
|
||||
msgstr "بطاقة الائتمان"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "PayPal Pay Later"
|
||||
msgstr "الدفع لاحقًا عبر PayPal"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "iDEAL | Wero"
|
||||
msgstr "iDEAL | التحدي"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "SEPA Direct Debit"
|
||||
msgstr "الخصم المباشر في منطقة الدفع الموحدة (SEPA)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "Bancontact"
|
||||
msgstr "بانكونتاكت"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "giropay"
|
||||
msgstr "giropay"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "SOFORT"
|
||||
msgstr "فوراً"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
#, fuzzy
|
||||
#| msgid "Yes"
|
||||
msgid "eps"
|
||||
msgstr "نقاط إضافية"
|
||||
msgstr "نعم"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "MyBank"
|
||||
msgstr "بنكي"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "Przelewy24"
|
||||
msgstr "Przelewy24"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "Verkkopankki"
|
||||
msgstr "الخدمات المصرفية عبر الإنترنت"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "PayU"
|
||||
msgstr "الدفع لك"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "BLIK"
|
||||
msgstr "BLIK"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "Trustly"
|
||||
msgstr "Trustly"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "Zimpler"
|
||||
msgstr "Zimpler"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "Maxima"
|
||||
msgstr "الأعظم"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "OXXO"
|
||||
msgstr "OXXO"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "Boleto"
|
||||
msgstr "تذكرة"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "WeChat Pay"
|
||||
msgstr "WeChat Pay"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "Mercado Pago"
|
||||
msgstr "Mercado Pago"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||
msgid "Continue"
|
||||
msgstr "تابع"
|
||||
msgstr "المتابعة"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js
|
||||
@@ -142,7 +144,7 @@ msgstr "جاري تأكيد الدفع الخاص بك …"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "Payment method unavailable"
|
||||
msgstr "طريقة الدفع غير متاحة"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
|
||||
msgid "Placed orders"
|
||||
@@ -154,11 +156,11 @@ msgstr "الطلبات المدفوعة"
|
||||
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
|
||||
msgid "Attendees (ordered)"
|
||||
msgstr "الحاضرون (حسب الترتيب)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
|
||||
msgid "Attendees (paid)"
|
||||
msgstr "الحاضرون (الذين دفعوا رسوم الاشتراك)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
|
||||
msgid "Total revenue"
|
||||
@@ -190,7 +192,7 @@ msgstr "تبديل قائمة الدخول"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Search results"
|
||||
msgstr "نتائج البحث"
|
||||
msgstr "البحث في النتائج"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "No tickets found"
|
||||
@@ -234,15 +236,15 @@ msgstr "غير مدفوع"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Canceled"
|
||||
msgstr "تم إلغاؤه"
|
||||
msgstr "ملغاة"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Confirmed"
|
||||
msgstr "تم التأكيد"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Approval pending"
|
||||
msgstr "في انتظار الموافقة"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Redeemed"
|
||||
@@ -250,11 +252,11 @@ msgstr "مستخدم"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Cancel"
|
||||
msgstr "إلغاء"
|
||||
msgstr "قم بالإلغاء"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Ticket not paid"
|
||||
msgstr "التذكرة لم يتم دفع ثمنها"
|
||||
msgstr "لم يتم دفع قيمة التذكرة"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
||||
@@ -274,19 +276,23 @@ msgstr "تم تسجيل الخروج"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Ticket already used"
|
||||
msgstr "تم استخدام التذكرة بالفعل"
|
||||
msgstr "تم استخدام التذكرة مسبقا"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Information required"
|
||||
msgstr "المعلومات المطلوبة"
|
||||
msgstr "معلومات مطلوبة"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
#, fuzzy
|
||||
#| msgid "Unknown error."
|
||||
msgid "Unknown ticket"
|
||||
msgstr "تذكرة غير معروفة"
|
||||
msgstr "خطأ غير معروف."
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
#, fuzzy
|
||||
#| msgid "Entry not allowed"
|
||||
msgid "Ticket type not allowed here"
|
||||
msgstr "نوع تذكرة الدخول غير مسموح به هنا"
|
||||
msgstr "إدخال غير مسموح"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Entry not allowed"
|
||||
@@ -294,13 +300,17 @@ msgstr "إدخال غير مسموح"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Ticket code revoked/changed"
|
||||
msgstr "تم إلغاء/تغيير رمز التذكرة"
|
||||
msgstr "تم إلغاء رمز التذكرة أو تبديله"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
#, fuzzy
|
||||
#| msgid "Ticket not paid"
|
||||
msgid "Ticket blocked"
|
||||
msgstr "التذكرة غير محجوبة"
|
||||
msgstr "لم يتم دفع قيمة التذكرة"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
#, fuzzy
|
||||
#| msgid "Ticket not paid"
|
||||
msgid "Ticket not valid at this time"
|
||||
msgstr "لم يتم دفع قيمة التذكرة"
|
||||
|
||||
@@ -310,11 +320,11 @@ msgstr "تم إلغاء الطلب"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Ticket code is ambiguous on list"
|
||||
msgstr "رمز التذكرة غير واضح في القائمة"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Order not approved"
|
||||
msgstr "لم تتم الموافقة على الطلب"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Checked-in Tickets"
|
||||
@@ -338,9 +348,12 @@ msgstr "نعم"
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js
|
||||
#: pretix/static/pretixpresale/js/ui/questions.js
|
||||
msgid "No"
|
||||
msgstr "من"
|
||||
msgstr "لا"
|
||||
|
||||
#: pretix/static/lightbox/js/lightbox.js
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "Close"
|
||||
msgid "close"
|
||||
msgstr "إغلاق"
|
||||
|
||||
@@ -395,7 +408,7 @@ msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js
|
||||
msgid "We are processing your request …"
|
||||
msgstr "نحن نقوم بمعالجة طلبك …"
|
||||
msgstr "جاري معالجة طلبك …"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js
|
||||
msgid ""
|
||||
@@ -408,7 +421,7 @@ msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js
|
||||
msgid "If this takes longer than a few minutes, please contact us."
|
||||
msgstr "إذا استغرق ذلك أكثر من بضع دقائق، يرجى الاتصال بنا."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js
|
||||
msgid "Close message"
|
||||
@@ -424,11 +437,11 @@ msgstr "للنسخ اضغط Ctrl + C!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||
msgid "Edit"
|
||||
msgstr "تحرير"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||
msgid "Visualize"
|
||||
msgstr "تصور"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||
msgid ""
|
||||
@@ -436,13 +449,10 @@ msgid ""
|
||||
"or variations are not contained in any of your rule parts so people with "
|
||||
"these tickets will not get in:"
|
||||
msgstr ""
|
||||
"تقوم القاعدة الخاصة بك دائمًا بالتصفية حسب المنتج أو النوع، لكن المنتجات أو "
|
||||
"الأنواع التالية غير مدرجة في أي من أجزاء القاعدة الخاصة بك، لذا لن يتم قبول "
|
||||
"الأشخاص الذين يحملون هذه التذاكر:"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||
msgid "Please double-check if this was intentional."
|
||||
msgstr "يرجى التأكد مرة أخرى مما إذا كان ذلك مقصودًا أم لا."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "All of the conditions below (AND)"
|
||||
@@ -482,21 +492,21 @@ msgstr "أضف شرطا"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "minutes"
|
||||
msgstr "دقائق"
|
||||
msgstr "الدقائق"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "Duplicate"
|
||||
msgstr "مكرر"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgctxt "entry_status"
|
||||
msgid "present"
|
||||
msgstr "حاضر"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgctxt "entry_status"
|
||||
msgid "absent"
|
||||
msgstr "غائب"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "is one of"
|
||||
@@ -512,11 +522,11 @@ msgstr "بعد"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "="
|
||||
msgstr "="
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "Product"
|
||||
msgstr "المنتج"
|
||||
msgstr "منتج"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "Product variation"
|
||||
@@ -524,7 +534,7 @@ msgstr "نوع المنتج"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "Gate"
|
||||
msgstr "الشارع"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "Current date and time"
|
||||
@@ -532,11 +542,11 @@ msgstr "التاريخ والوقت الحالي"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "Current day of the week (1 = Monday, 7 = Sunday)"
|
||||
msgstr "اليوم الحالي من الأسبوع (1 = الاثنين، 7 = الأحد)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "Current entry status"
|
||||
msgstr "الحالة الحالية للطلب"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "Number of previous entries"
|
||||
@@ -547,40 +557,48 @@ msgid "Number of previous entries since midnight"
|
||||
msgstr "عدد المدخلات السابقة قبل منتصف الليل"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
#, fuzzy
|
||||
#| msgid "Number of previous entries"
|
||||
msgid "Number of previous entries since"
|
||||
msgstr "عدد الإدخالات السابقة منذ"
|
||||
msgstr "عدد المدخلات السابقة"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
#, fuzzy
|
||||
#| msgid "Number of previous entries"
|
||||
msgid "Number of previous entries before"
|
||||
msgstr "عدد الإدخالات السابقة قبل"
|
||||
msgstr "عدد المدخلات السابقة"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "Number of days with a previous entry"
|
||||
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
#, fuzzy
|
||||
#| msgid "Number of days with a previous entry"
|
||||
msgid "Number of days with a previous entry since"
|
||||
msgstr "عدد الأيام التي تحتوي على قيد سابق منذ"
|
||||
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
#, fuzzy
|
||||
#| msgid "Number of days with a previous entry"
|
||||
msgid "Number of days with a previous entry before"
|
||||
msgstr "عدد الأيام التي يوجد فيها قيد سابق قبل ذلك"
|
||||
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "Minutes since last entry (-1 on first entry)"
|
||||
msgstr "عدد الدقائق منذ آخر تسجيل (-1 عند التسجيل الأول)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "Minutes since first entry (-1 on first entry)"
|
||||
msgstr "عدد الدقائق منذ أول دخول (-1 عند أول دخول)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts
|
||||
msgid "Error: Product not found!"
|
||||
msgstr "خطأ: لم يتم العثور على المنتج!"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts
|
||||
msgid "Error: Variation not found!"
|
||||
msgstr "خطأ: لم يتم العثور على النسخة!"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js
|
||||
msgid "Check-in QR"
|
||||
@@ -595,12 +613,16 @@ msgid "Group of objects"
|
||||
msgstr "مجموعة من العناصر"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js
|
||||
#, fuzzy
|
||||
#| msgid "Text object"
|
||||
msgid "Text object (deprecated)"
|
||||
msgstr "كائن نصي (مهمل)"
|
||||
msgstr "عنصر نص"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js
|
||||
#, fuzzy
|
||||
#| msgid "Text object"
|
||||
msgid "Text box"
|
||||
msgstr "مربع نصي"
|
||||
msgstr "عنصر نص"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js
|
||||
msgid "Barcode area"
|
||||
@@ -647,28 +669,28 @@ msgid "Unknown error."
|
||||
msgstr "خطأ غير معروف."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js
|
||||
#, fuzzy
|
||||
#| msgid "Your color has great contrast and is very easy to read!"
|
||||
msgid "Your color has great contrast and will provide excellent accessibility."
|
||||
msgstr "يتميز لونك بتباين رائع ويسهل قراءته للغاية! وسيوفر إمكانية وصول ممتازة."
|
||||
msgstr "اللون يتمتع بتباين كبير وتسهل قراءته!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js
|
||||
#, fuzzy
|
||||
#| msgid "Your color has decent contrast and is probably good-enough to read!"
|
||||
msgid ""
|
||||
"Your color has decent contrast and is sufficient for minimum accessibility "
|
||||
"requirements."
|
||||
msgstr ""
|
||||
"يتميز لونك بتباين جيد، ومن المرجح أنه مناسب للقراءة! وهو كافٍ لتلبية الحد "
|
||||
"الأدنى من متطلبات سهولة الوصول."
|
||||
msgstr "اللون يحظى بتباين معقول ويمكن أن يكون مناسب للقراءة!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js
|
||||
msgid ""
|
||||
"Your color has insufficient contrast to white. Accessibility of your site "
|
||||
"will be impacted."
|
||||
msgstr ""
|
||||
"اللون الذي اخترته لا يتمتع بتباين كافٍ مع اللون الأبيض. وسيؤثر ذلك على إمكانية "
|
||||
"الوصول إلى موقعك."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js
|
||||
msgid "Search query"
|
||||
msgstr "استعلام البحث"
|
||||
msgstr "البحث في الاستفسارات"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js
|
||||
msgid "All"
|
||||
@@ -684,11 +706,11 @@ msgstr "المختارة فقط"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr "أدخل رقم الصفحة بين 1 و %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js
|
||||
msgid "Invalid page number."
|
||||
msgstr "رقم الصفحة غير صحيح."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js
|
||||
msgid "Use a different name internally"
|
||||
@@ -707,8 +729,10 @@ msgid "Calculating default price…"
|
||||
msgstr "حساب السعر الافتراضي…"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/plugins.js
|
||||
#, fuzzy
|
||||
#| msgid "Search results"
|
||||
msgid "No results"
|
||||
msgstr "البحث: لا توجد نتائج"
|
||||
msgstr "البحث في النتائج"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js
|
||||
msgid "Others"
|
||||
@@ -716,7 +740,7 @@ msgstr "غير ذلك"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js
|
||||
msgid "Count"
|
||||
msgstr "العدد"
|
||||
msgstr "احسب"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/subevent.js
|
||||
msgid "(one more date)"
|
||||
@@ -729,12 +753,12 @@ msgstr[4] "أيام عديدة"
|
||||
msgstr[5] "أخرى"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||
#, fuzzy
|
||||
#| msgid "The items in your cart are no longer reserved for you."
|
||||
msgid ""
|
||||
"The items in your cart are no longer reserved for you. You can still "
|
||||
"complete your order as long as they’re available."
|
||||
msgstr ""
|
||||
"لم تعد المنتجات الموجودة في سلة التسوق محجوزة لك. لا يزال بإمكانك إتمام طلبك "
|
||||
"طالما كانت هذه المنتجات متوفرة."
|
||||
msgstr "لم تعد العناصر الموجودة في عربة التسوق الخاصة بك محجوزة لك."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||
msgid "Cart expired"
|
||||
@@ -742,9 +766,13 @@ msgstr "انتهت صلاحية عربة التسوق"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||
msgid "Your cart is about to expire."
|
||||
msgstr "سوف تنتهي صلاحية سلة التسوق الخاصة بك قريبًا."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||
#, fuzzy
|
||||
#| msgid "The items in your cart are reserved for you for one minute."
|
||||
#| msgid_plural ""
|
||||
#| "The items in your cart are reserved for you for {num} minutes."
|
||||
msgid "The items in your cart are reserved for you for one minute."
|
||||
msgid_plural "The items in your cart are reserved for you for {num} minutes."
|
||||
msgstr[0] "سيتم حجز العناصر لك في سلة التسوق لمدة {num}."
|
||||
@@ -755,24 +783,26 @@ msgstr[4] "سيتم حجز العناصر لك في سلة التسوق لدقا
|
||||
msgstr[5] "سيتم حجز العناصر لك في سلة التسوق لمدة {num}."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||
#, fuzzy
|
||||
#| msgid "Cart expired"
|
||||
msgid "Your cart has expired."
|
||||
msgstr "انتهت صلاحية عربة التسوق"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||
#, fuzzy
|
||||
#| msgid "The items in your cart are no longer reserved for you."
|
||||
msgid ""
|
||||
"The items in your cart are no longer reserved for you. You can still "
|
||||
"complete your order as long as they're available."
|
||||
msgstr ""
|
||||
"لم تعد المنتجات الموجودة في سلة التسوق محجوزة لك. لا يزال بإمكانك إتمام طلبك "
|
||||
"طالما كانت هذه المنتجات متوفرة."
|
||||
msgstr "لم تعد العناصر الموجودة في عربة التسوق الخاصة بك محجوزة لك."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||
msgid "Do you want to renew the reservation period?"
|
||||
msgstr "هل ترغب في تمديد فترة الحجز؟"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||
msgid "Renew reservation"
|
||||
msgstr "تجديد الحجز"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
@@ -792,77 +822,82 @@ msgstr "التوقيت المحلي:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/walletdetection.js
|
||||
msgid "Google Pay"
|
||||
msgstr "Google Pay"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgctxt "widget"
|
||||
msgid "Quantity"
|
||||
msgstr "الكمية"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgctxt "widget"
|
||||
msgid "Decrease quantity"
|
||||
msgstr "تقليل الكمية"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgctxt "widget"
|
||||
msgid "Increase quantity"
|
||||
msgstr "زيادة الكمية"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgctxt "widget"
|
||||
msgid "Filter events by"
|
||||
msgstr "تصفية الأحداث حسب"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgctxt "widget"
|
||||
msgid "Filter"
|
||||
msgstr "فلتر"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgctxt "widget"
|
||||
msgid "Price"
|
||||
msgstr "السعر"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "Original price: %s"
|
||||
msgstr "السعر الأصلي: %s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "New price: %s"
|
||||
msgstr "السعر الجديد: %s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
#, fuzzy
|
||||
#| msgid "Selected only"
|
||||
msgctxt "widget"
|
||||
msgid "Select"
|
||||
msgstr "المختارة فقط"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
#, javascript-format
|
||||
#, fuzzy, javascript-format
|
||||
#| msgid "Selected only"
|
||||
msgctxt "widget"
|
||||
msgid "Select %s"
|
||||
msgstr "تم اختيار %s فقط"
|
||||
msgstr "المختارة فقط"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
#, javascript-format
|
||||
#, fuzzy, javascript-format
|
||||
#| msgctxt "widget"
|
||||
#| msgid "See variations"
|
||||
msgctxt "widget"
|
||||
msgid "Select variant %s"
|
||||
msgstr "اختر النوع %s"
|
||||
msgstr "أنظر إلى الاختلافات"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
@@ -905,7 +940,7 @@ msgstr "من %(currency) s %(price)s"
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "Image of %s"
|
||||
msgstr "صورة لـ %s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
@@ -946,21 +981,27 @@ msgstr "متوفرة مع القسيمة فقط"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "currently available: %s"
|
||||
msgctxt "widget"
|
||||
msgid "Not yet available"
|
||||
msgstr "حاليًا غير متوفر: %s"
|
||||
msgstr "متوفر حاليا: %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgctxt "widget"
|
||||
msgid "Not available anymore"
|
||||
msgstr "لم يعد متوفراً"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "currently available: %s"
|
||||
msgctxt "widget"
|
||||
msgid "Currently not available"
|
||||
msgstr "غير متوفر حاليًا: %s"
|
||||
msgstr "متوفر حاليا: %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
@@ -988,20 +1029,24 @@ msgid ""
|
||||
"There are currently a lot of users in this ticket shop. Please open the shop "
|
||||
"in a new tab to continue."
|
||||
msgstr ""
|
||||
"يوجد حالياً عدد كبير من المستخدمين في متجر التذاكر هذا. يرجى فتح المتجر في "
|
||||
"علامة تبويب جديدة للمتابعة."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "Close ticket shop"
|
||||
msgctxt "widget"
|
||||
msgid "Open ticket shop"
|
||||
msgstr "إغلاق/فتح متجر التذاكر"
|
||||
msgstr "إغلاق متجر التذاكر"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "Resume checkout"
|
||||
msgctxt "widget"
|
||||
msgid "Checkout"
|
||||
msgstr "استئناف عملية الدفع"
|
||||
msgstr "استئناف الدفع"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
@@ -1016,9 +1061,6 @@ msgid ""
|
||||
"We could not create your cart, since there are currently too many users in "
|
||||
"this ticket shop. Please click \"Continue\" to retry in a new tab."
|
||||
msgstr ""
|
||||
"لم نتمكن من إنشاء سلة التسوق الخاصة بك، نظرًا لوجود عدد كبير جدًّا من "
|
||||
"المستخدمين حاليًّا في متجر التذاكر هذا. يرجى النقر على «متابعة» لإعادة المحاولة "
|
||||
"في علامة تبويب جديدة."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
@@ -1068,15 +1110,18 @@ msgstr "إغلاق"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "Resume checkout"
|
||||
msgctxt "widget"
|
||||
msgid "Close checkout"
|
||||
msgstr "إغلاق عملية الدفع"
|
||||
msgstr "استئناف الدفع"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgctxt "widget"
|
||||
msgid "You cannot cancel this operation. Please wait for loading to finish."
|
||||
msgstr "لا يمكنك إلغاء هذه العملية. يرجى الانتظار حتى ينتهي التحميل."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
@@ -1086,15 +1131,21 @@ msgstr "استمرار"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "See variations"
|
||||
msgctxt "widget"
|
||||
msgid "Show variants"
|
||||
msgstr "انظر الاختلافات"
|
||||
msgstr "أنظر إلى الاختلافات"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "See variations"
|
||||
msgctxt "widget"
|
||||
msgid "Hide variants"
|
||||
msgstr "إخفاء الاختلافات"
|
||||
msgstr "أنظر إلى الاختلافات"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
@@ -1152,11 +1203,11 @@ msgid ""
|
||||
"add yourself to the waiting list. We will then notify if seats are available "
|
||||
"again."
|
||||
msgstr ""
|
||||
"تم بيع جميع فئات التذاكر أو بعضها حاليًا. إذا أردت، يمكنك إضافة اسمك إلى "
|
||||
"قائمة الانتظار. وسنقوم بإخطارك إذا توفرت مقاعد مرة أخرى."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
#, fuzzy
|
||||
#| msgid "Load more"
|
||||
msgctxt "widget"
|
||||
msgid "Load more"
|
||||
msgstr "تحميل المزيد"
|
||||
@@ -1199,37 +1250,37 @@ msgstr "الأحد"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "Monday"
|
||||
msgstr "الاثنين"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "Tuesday"
|
||||
msgstr "الثلاثاء"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "Wednesday"
|
||||
msgstr "الأربعاء"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "Thursday"
|
||||
msgstr "الخميس"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "Friday"
|
||||
msgstr "الجمعة"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "Saturday"
|
||||
msgstr "السبت"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "Sunday"
|
||||
msgstr "الأحد"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
@@ -1254,7 +1305,7 @@ msgstr "أبريل"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "May"
|
||||
msgstr "هناك"
|
||||
msgstr "مايو"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
|
||||
"PO-Revision-Date: 2026-07-13 11:28+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"PO-Revision-Date: 2026-05-04 07:42+0000\n"
|
||||
"Last-Translator: Martin Gross <gross@rami.io>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
"pretix/pretix-js/de_Informal/>\n"
|
||||
"Language: de_Informal\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 2026.7.1\n"
|
||||
"X-Generator: Weblate 5.17\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
|
||||
msgid "Marked as paid"
|
||||
@@ -430,11 +430,11 @@ msgstr "Drücke Strg+C zum kopieren!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||
msgid "Edit"
|
||||
msgstr "Bearbeiten"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||
msgid "Visualize"
|
||||
msgstr "Visualisieren"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||
msgid ""
|
||||
@@ -442,13 +442,10 @@ msgid ""
|
||||
"or variations are not contained in any of your rule parts so people with "
|
||||
"these tickets will not get in:"
|
||||
msgstr ""
|
||||
"Deine Regeln filtern immer nach Produkt oder Variation, jedoch sind die "
|
||||
"folgenden Produkte oder Variationen nicht in einer Ihrer Regeln enthalten, "
|
||||
"sodass Kunden mit diesen Tickets keinen Eintritt erhalten werden:"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||
msgid "Please double-check if this was intentional."
|
||||
msgstr "Bitte überprüfe, ob dies beabsichtigt war."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "All of the conditions below (AND)"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
|
||||
"PO-Revision-Date: 2026-07-08 16:00+0000\n"
|
||||
"PO-Revision-Date: 2026-07-07 09:22+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"es/>\n"
|
||||
@@ -11453,8 +11453,10 @@ msgstr ""
|
||||
"contacto con usted."
|
||||
|
||||
#: pretix/base/settings.py pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Contact"
|
||||
msgid "Contact URL"
|
||||
msgstr "URL de contacto"
|
||||
msgstr "Contacto"
|
||||
|
||||
#: pretix/base/settings.py pretix/control/forms/event.py
|
||||
msgid ""
|
||||
@@ -11462,10 +11464,6 @@ msgid ""
|
||||
"the email address above. Please note that you still need to add a contact "
|
||||
"email address that will be shared with all emails you send."
|
||||
msgstr ""
|
||||
"Si activas esta opción, el enlace de contacto del pie de página redirigirá "
|
||||
"aquí, en lugar de utilizar la dirección de correo electrónico indicada más "
|
||||
"arriba. Ten en cuenta que aún así tendrás que añadir una dirección de correo "
|
||||
"electrónico de contacto que se incluirá en todos los correos que envíes."
|
||||
|
||||
#: pretix/base/settings.py pretix/control/forms/event.py
|
||||
msgid "Imprint URL"
|
||||
@@ -19948,7 +19946,25 @@ msgstr ""
|
||||
"El equipo de %(instance)s\n"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/email/email_setup.txt
|
||||
#, python-format
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "Hello,\n"
|
||||
#| "\n"
|
||||
#| "Someone requested to use %(address)s as a sender address on %(instance)s. "
|
||||
#| "Once verified, emails sent from %(instance)s can show this address as the "
|
||||
#| "sender.\n"
|
||||
#| "\n"
|
||||
#| "If this was you, enter the following code in the setup form:\n"
|
||||
#| "\n"
|
||||
#| " %(code)s\n"
|
||||
#| "\n"
|
||||
#| "Don't share this code with anyone. The %(instance)s team will never ask "
|
||||
#| "you for it.\n"
|
||||
#| "\n"
|
||||
#| "If you didn't request this, you can safely ignore this email.\n"
|
||||
#| "\n"
|
||||
#| "Thanks, \n"
|
||||
#| "The %(instance)s Team\n"
|
||||
msgid ""
|
||||
"Hello,\n"
|
||||
"\n"
|
||||
@@ -19971,19 +19987,19 @@ msgid ""
|
||||
msgstr ""
|
||||
"Hola,\n"
|
||||
"\n"
|
||||
"Alguien ha solicitado utilizar %(address)s como dirección de remitente en %"
|
||||
"(instance)s. Una vez verificado, los correos electrónicos enviados desde %"
|
||||
"(instance)s podrán mostrar esta dirección como remitente.\n"
|
||||
"Alguien ha solicitado utilizar %(address)s como dirección de remitente en "
|
||||
"%(instance)s. Una vez verificado, los correos electrónicos enviados desde "
|
||||
"%(instance)s podrán mostrar esta dirección como remitente.\n"
|
||||
"\n"
|
||||
"Si ha sido usted quien lo ha solicitado, introduzca el siguiente código en "
|
||||
"el formulario de configuración:\n"
|
||||
"Si has sido tú, introduce el siguiente código en el formulario de "
|
||||
"configuración:\n"
|
||||
"\n"
|
||||
" %(code)s\n"
|
||||
"\n"
|
||||
"No comparta este código con nadie a menos que quiera autorizarle a utilizar "
|
||||
"esta dirección para este fin. El equipo de %(instance)s nunca se lo pedirá.\n"
|
||||
"No compartas este código con nadie. El equipo de %(instance)s nunca te lo "
|
||||
"pedirá.\n"
|
||||
"\n"
|
||||
"Si no ha solicitado esto, puede ignorar este correo electrónico sin ningún "
|
||||
"Si no has solicitado esto, puedes ignorar este correo electrónico sin ningún "
|
||||
"problema.\n"
|
||||
"\n"
|
||||
"Gracias, \n"
|
||||
|
||||
@@ -4,10 +4,10 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
|
||||
"PO-Revision-Date: 2026-07-08 16:00+0000\n"
|
||||
"PO-Revision-Date: 2026-07-07 09:22+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"fr/>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
|
||||
">\n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -1933,7 +1933,7 @@ msgid ""
|
||||
"redeemed."
|
||||
msgstr ""
|
||||
"Ce produit ne sera affiché que si un bon de réduction correspondant au "
|
||||
"produit est validé."
|
||||
"produit est échangé."
|
||||
|
||||
#: pretix/base/exporters/items.py pretix/base/models/items.py
|
||||
msgid "Buying this product requires approval"
|
||||
@@ -4109,8 +4109,8 @@ msgstr ""
|
||||
msgid ""
|
||||
"To verify your email address {email} on {instance}, use the following code:"
|
||||
msgstr ""
|
||||
"Pour valider votre adresse e-mail {email} sur {instance}, utilisez le code "
|
||||
"suivant :"
|
||||
"Pour valider votre adresse e-mail {email} sur\n"
|
||||
"{instance}, utilisez le code suivant :"
|
||||
|
||||
#: pretix/base/models/auth.py
|
||||
msgid "Your confirmation code"
|
||||
@@ -6975,7 +6975,7 @@ msgstr "Nombre de fois que ce bon peut être utilisé."
|
||||
|
||||
#: pretix/base/models/vouchers.py pretix/control/views/vouchers.py
|
||||
msgid "Redeemed"
|
||||
msgstr "Validé"
|
||||
msgstr "Échangé"
|
||||
|
||||
#: pretix/base/models/vouchers.py
|
||||
msgid ""
|
||||
@@ -11542,8 +11542,10 @@ msgstr ""
|
||||
"contacter."
|
||||
|
||||
#: pretix/base/settings.py pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Contact"
|
||||
msgid "Contact URL"
|
||||
msgstr "URL de contact"
|
||||
msgstr "Contact"
|
||||
|
||||
#: pretix/base/settings.py pretix/control/forms/event.py
|
||||
msgid ""
|
||||
@@ -11551,10 +11553,6 @@ msgid ""
|
||||
"the email address above. Please note that you still need to add a contact "
|
||||
"email address that will be shared with all emails you send."
|
||||
msgstr ""
|
||||
"Si vous activez cette option, le lien de contact figurant dans le pied de "
|
||||
"page redirigera vers cette page au lieu d'utiliser l'adresse e-mail indiquée "
|
||||
"ci-dessus. Veuillez noter que vous devrez tout de même ajouter une adresse e-"
|
||||
"mail de contact qui sera mentionnée dans tous les e-mails que vous enverrez."
|
||||
|
||||
#: pretix/base/settings.py pretix/control/forms/event.py
|
||||
msgid "Imprint URL"
|
||||
@@ -20096,7 +20094,25 @@ msgstr ""
|
||||
"L'équipe %(instance)s\n"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/email/email_setup.txt
|
||||
#, python-format
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "Hello,\n"
|
||||
#| "\n"
|
||||
#| "Someone requested to use %(address)s as a sender address on %(instance)s. "
|
||||
#| "Once verified, emails sent from %(instance)s can show this address as the "
|
||||
#| "sender.\n"
|
||||
#| "\n"
|
||||
#| "If this was you, enter the following code in the setup form:\n"
|
||||
#| "\n"
|
||||
#| " %(code)s\n"
|
||||
#| "\n"
|
||||
#| "Don't share this code with anyone. The %(instance)s team will never ask "
|
||||
#| "you for it.\n"
|
||||
#| "\n"
|
||||
#| "If you didn't request this, you can safely ignore this email.\n"
|
||||
#| "\n"
|
||||
#| "Thanks, \n"
|
||||
#| "The %(instance)s Team\n"
|
||||
msgid ""
|
||||
"Hello,\n"
|
||||
"\n"
|
||||
@@ -20120,23 +20136,22 @@ msgstr ""
|
||||
"Bonjour,\n"
|
||||
"\n"
|
||||
"Une personne a demandé à utiliser %(address)s comme adresse d'expéditeur sur "
|
||||
"%(instance)s. Une fois cette demande validée, les e-mails envoyés depuis %"
|
||||
"(instance)s pourront afficher cette adresse comme expéditeur.\n"
|
||||
"%(instance)s. Une fois cette demande validée, les e-mails envoyés depuis "
|
||||
"%(instance)s pourront afficher cette adresse comme expéditeur.\n"
|
||||
"\n"
|
||||
"Si c'est bien vous qui en avez fait la demande, veuillez saisir le code "
|
||||
"suivant dans le formulaire de configuration :\n"
|
||||
"Si c'est vous qui avez fait cette demande, veuillez saisir le code suivant "
|
||||
"dans le formulaire de configuration :\n"
|
||||
"\n"
|
||||
" %(code)s\n"
|
||||
"\n"
|
||||
"Ne communiquez ce code à personne, sauf si vous souhaitez autoriser cette "
|
||||
"personne à utiliser cette adresse à cette fin. L'équipe de %(instance)s ne "
|
||||
"vous le demandera jamais.\n"
|
||||
"Ne communiquez ce code à personne. L'équipe de %(instance)s ne vous le "
|
||||
"demandera jamais.\n"
|
||||
"\n"
|
||||
"Si vous n'en avez pas fait la demande, vous pouvez ignorer cet e-mail en "
|
||||
"Si vous n’en avez pas fait la demande, vous pouvez ignorer cet e-mail en "
|
||||
"toute sécurité.\n"
|
||||
"\n"
|
||||
"Merci, \n"
|
||||
"L'équipe de %(instance)s\n"
|
||||
"L’équipe %(instance)s\n"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/email/forgot.txt
|
||||
#, python-format
|
||||
@@ -36815,19 +36830,17 @@ msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
|
||||
msgid "How to fix this:"
|
||||
msgstr "Comment résoudre ce problème :"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
|
||||
msgid "Close this page."
|
||||
msgstr "Fermer cette page."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
|
||||
msgid ""
|
||||
"Open this website again directly in your main browser (for example Safari or "
|
||||
"Chrome)."
|
||||
msgstr ""
|
||||
"Rouvrez ce site directement dans votre navigateur principal (par exemple "
|
||||
"Safari ou Chrome)."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
|
||||
msgid ""
|
||||
@@ -36835,9 +36848,6 @@ msgid ""
|
||||
"browser window, without refreshing the page, opening new tabs, or switching "
|
||||
"apps part-way through."
|
||||
msgstr ""
|
||||
"Recommencez la procédure de paiement ou de connexion et menez-la à son terme "
|
||||
"dans la même fenêtre de navigateur, sans actualiser la page, ouvrir de "
|
||||
"nouveaux onglets ni changer d'application en cours de route."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
|
||||
msgid ""
|
||||
@@ -36845,10 +36855,6 @@ msgid ""
|
||||
"before retrying can help. As a last step, try using a different browser or "
|
||||
"another device (for example, a desktop or laptop computer)."
|
||||
msgstr ""
|
||||
"Si le problème persiste, il peut être utile de fermer toutes les fenêtres du "
|
||||
"navigateur et d'effacer les cookies avant de réessayer. En dernier recours, "
|
||||
"essayez d'utiliser un autre navigateur ou un autre appareil (par exemple, un "
|
||||
"ordinateur de bureau ou un ordinateur portable)."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/organizers/customer_membership.html
|
||||
msgid "Your membership"
|
||||
|
||||
@@ -7,8 +7,8 @@ msgstr ""
|
||||
"Project-Id-Version: French\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
|
||||
"PO-Revision-Date: 2026-07-08 16:00+0000\n"
|
||||
"Last-Translator: Joram Schwartzmann <schwartzmann@pretix.eu>\n"
|
||||
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"fr/>\n"
|
||||
"Language: fr\n"
|
||||
@@ -244,7 +244,7 @@ msgstr "En attente d'approbation"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Redeemed"
|
||||
msgstr "Validé"
|
||||
msgstr "Échangé"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Cancel"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
|
||||
"PO-Revision-Date: 2026-07-14 15:00+0000\n"
|
||||
"PO-Revision-Date: 2026-07-07 09:17+0000\n"
|
||||
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
|
||||
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"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 2026.7.1\n"
|
||||
"X-Generator: Weblate 2026.6.1\n"
|
||||
|
||||
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html
|
||||
#: pretix/control/templates/pretixcontrol/events/index.html
|
||||
@@ -11076,8 +11076,10 @@ msgid "We'll show this publicly to allow attendees to contact you."
|
||||
msgstr "参加者があなたに連絡できるように、これを公に表示します。"
|
||||
|
||||
#: pretix/base/settings.py pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Contact"
|
||||
msgid "Contact URL"
|
||||
msgstr "連絡先URL"
|
||||
msgstr "連絡先"
|
||||
|
||||
#: pretix/base/settings.py pretix/control/forms/event.py
|
||||
msgid ""
|
||||
@@ -11085,9 +11087,6 @@ msgid ""
|
||||
"the email address above. Please note that you still need to add a contact "
|
||||
"email address that will be shared with all emails you send."
|
||||
msgstr ""
|
||||
"これを設定すると、フッターの連絡先リンクが上記のメールアドレスではなくこちら"
|
||||
"を指すことになります。追加が必要な連絡先メールアドレスは、送信するすべての電"
|
||||
"子メールで共有されるのでご注意ください。"
|
||||
|
||||
#: pretix/base/settings.py pretix/control/forms/event.py
|
||||
msgid "Imprint URL"
|
||||
@@ -19361,7 +19360,25 @@ msgstr ""
|
||||
"%(instance)sのチーム\n"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/email/email_setup.txt
|
||||
#, python-format
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "Hello,\n"
|
||||
#| "\n"
|
||||
#| "Someone requested to use %(address)s as a sender address on %(instance)s. "
|
||||
#| "Once verified, emails sent from %(instance)s can show this address as the "
|
||||
#| "sender.\n"
|
||||
#| "\n"
|
||||
#| "If this was you, enter the following code in the setup form:\n"
|
||||
#| "\n"
|
||||
#| " %(code)s\n"
|
||||
#| "\n"
|
||||
#| "Don't share this code with anyone. The %(instance)s team will never ask "
|
||||
#| "you for it.\n"
|
||||
#| "\n"
|
||||
#| "If you didn't request this, you can safely ignore this email.\n"
|
||||
#| "\n"
|
||||
#| "Thanks, \n"
|
||||
#| "The %(instance)s Team\n"
|
||||
msgid ""
|
||||
"Hello,\n"
|
||||
"\n"
|
||||
@@ -19384,17 +19401,16 @@ msgid ""
|
||||
msgstr ""
|
||||
"こんにちは、\n"
|
||||
"\n"
|
||||
"誰かが %(address)s を送信者アドレスとして %(instance)s に使用するよう"
|
||||
"リクエストしました。検証が完了すると、%(instance)s から送信されたメールはこの"
|
||||
"アドレスを送信者として表示できます。\n"
|
||||
"誰かが %(address)s を送信者アドレスとして %(instance)s に使用するようリクエス"
|
||||
"トしました。検証が完了すると、%(instance)s から送信されたメールはこのアドレス"
|
||||
"を送信者として表示できます。\n"
|
||||
"\n"
|
||||
"もしこれがあなたであれば、設定フォームに以下のコードを入力してください。\n"
|
||||
"\n"
|
||||
"…%(code)s\n"
|
||||
"\n"
|
||||
"このコードを誰にも共有しないでください。ただし、このアドレスをこの目的で使用"
|
||||
"することを許可したい場合は除きます。%(instance)sのチームは、決してあなたにそ"
|
||||
"れを求めることはありません。\n"
|
||||
"このコードを誰にも共有しないでください。%(instance)sのチームは、決してあなた"
|
||||
"にそれを求めることはありません。\n"
|
||||
"\n"
|
||||
"もしこのメールをご依頼いただいていない場合は、このメールはご無視いただいて構"
|
||||
"いません。\n"
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
from http.cookies import Morsel
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
@@ -51,16 +53,12 @@ from django.middleware.csrf import (
|
||||
from django.shortcuts import render
|
||||
from django.urls import set_urlconf
|
||||
from django.utils.cache import patch_vary_headers
|
||||
from django.utils.decorators import decorator_from_middleware
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.utils.http import http_date
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import Event, Organizer
|
||||
from pretix.helpers.cookies import (
|
||||
get_all_values_of_cookie, set_cookie_without_samesite,
|
||||
thoroughly_delete_cookie,
|
||||
)
|
||||
from pretix.helpers.cookies import set_cookie_without_samesite
|
||||
from pretix.multidomain.models import KnownDomain
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -183,23 +181,9 @@ class SessionMiddleware(BaseSessionMiddleware):
|
||||
# The session should be deleted only if the session is entirely empty
|
||||
is_secure = request.scheme == 'https'
|
||||
if '__Host-' + settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
|
||||
# response.delete_cookie does not work as we might have set a partitioned cookie
|
||||
thoroughly_delete_cookie(
|
||||
response,
|
||||
'__Host-' + settings.SESSION_COOKIE_NAME,
|
||||
path=settings.SESSION_COOKIE_PATH,
|
||||
secure=is_secure,
|
||||
httponly=settings.SESSION_COOKIE_HTTPONLY or None
|
||||
)
|
||||
response.delete_cookie('__Host-' + settings.SESSION_COOKIE_NAME)
|
||||
elif settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
|
||||
# response.delete_cookie does not work as we might have set a partitioned cookie
|
||||
thoroughly_delete_cookie(
|
||||
response,
|
||||
settings.SESSION_COOKIE_NAME,
|
||||
path=settings.SESSION_COOKIE_PATH,
|
||||
secure=is_secure,
|
||||
httponly=settings.SESSION_COOKIE_HTTPONLY or None
|
||||
)
|
||||
response.delete_cookie(settings.SESSION_COOKIE_NAME)
|
||||
else:
|
||||
if accessed:
|
||||
patch_vary_headers(response, ('Cookie',))
|
||||
@@ -216,21 +200,15 @@ class SessionMiddleware(BaseSessionMiddleware):
|
||||
if response.status_code != 500:
|
||||
request.session.save()
|
||||
if is_secure and settings.SESSION_COOKIE_NAME in request.COOKIES: # remove legacy cookie
|
||||
# response.delete_cookie does not work as we might have set a partitioned cookie
|
||||
thoroughly_delete_cookie(
|
||||
response,
|
||||
settings.SESSION_COOKIE_NAME,
|
||||
path=settings.SESSION_COOKIE_PATH,
|
||||
secure=is_secure,
|
||||
httponly=settings.SESSION_COOKIE_HTTPONLY or None
|
||||
)
|
||||
response.delete_cookie(settings.SESSION_COOKIE_NAME)
|
||||
response.delete_cookie(settings.SESSION_COOKIE_NAME, samesite="None")
|
||||
set_cookie_without_samesite(
|
||||
request, response,
|
||||
'__Host-' + settings.SESSION_COOKIE_NAME if is_secure else settings.SESSION_COOKIE_NAME,
|
||||
request.session.session_key, max_age=max_age,
|
||||
expires=expires,
|
||||
path=settings.SESSION_COOKIE_PATH,
|
||||
secure=is_secure,
|
||||
secure=request.scheme == 'https',
|
||||
httponly=settings.SESSION_COOKIE_HTTPONLY or None
|
||||
)
|
||||
return response
|
||||
@@ -275,74 +253,75 @@ class CsrfViewMiddleware(BaseCsrfMiddleware):
|
||||
if request.session.get(CSRF_SESSION_KEY) != request.META["CSRF_COOKIE"]:
|
||||
request.session[CSRF_SESSION_KEY] = request.META["CSRF_COOKIE"]
|
||||
else:
|
||||
is_secure = request.scheme == 'https'
|
||||
# Set the CSRF cookie even if it's already set, so we renew
|
||||
# the expiry timer.
|
||||
|
||||
# remove legacy cookie
|
||||
if request.is_secure() and settings.CSRF_COOKIE_NAME in request.COOKIES:
|
||||
thoroughly_delete_cookie(
|
||||
response,
|
||||
settings.CSRF_COOKIE_NAME,
|
||||
path=settings.CSRF_COOKIE_PATH,
|
||||
secure=request.is_secure(),
|
||||
httponly=settings.CSRF_COOKIE_HTTPONLY
|
||||
)
|
||||
if is_secure and settings.CSRF_COOKIE_NAME in request.COOKIES: # remove legacy cookie
|
||||
response.delete_cookie(settings.CSRF_COOKIE_NAME)
|
||||
response.delete_cookie(settings.CSRF_COOKIE_NAME, samesite="None")
|
||||
|
||||
handle_duplicated_csrftoken(request, response)
|
||||
|
||||
set_cookie_without_samesite(
|
||||
request, response,
|
||||
'__Host-' + settings.CSRF_COOKIE_NAME if request.is_secure() else settings.CSRF_COOKIE_NAME,
|
||||
'__Host-' + settings.CSRF_COOKIE_NAME if is_secure else settings.CSRF_COOKIE_NAME,
|
||||
request.META["CSRF_COOKIE"],
|
||||
max_age=settings.CSRF_COOKIE_AGE,
|
||||
path=settings.CSRF_COOKIE_PATH,
|
||||
secure=request.is_secure(),
|
||||
secure=is_secure,
|
||||
httponly=settings.CSRF_COOKIE_HTTPONLY
|
||||
)
|
||||
# Content varies with the CSRF cookie, so set the Vary header.
|
||||
patch_vary_headers(response, ('Cookie',))
|
||||
|
||||
def process_response(self, request, response):
|
||||
if (
|
||||
not settings.CSRF_USE_SESSIONS
|
||||
and request.is_secure()
|
||||
and settings.CSRF_COOKIE_NAME in response.cookies
|
||||
and response.cookies[settings.CSRF_COOKIE_NAME].value
|
||||
):
|
||||
logger.warning("Usage of djangos CsrfViewMiddleware detected (legacy cookie found in response). "
|
||||
"This may be caused by using csrf_project or requires_csrf_token from django.views.decorators.csrf. "
|
||||
"Use the pretix.multidomain.middlewares equivalent instead.")
|
||||
|
||||
return super().process_response(request, response)
|
||||
|
||||
|
||||
def handle_duplicated_csrftoken(request, response):
|
||||
# Due to a Safari bug, in some browser, two csrftoken cookies can exist:
|
||||
# one unpartitioned, one partitioned. This function generates an additional
|
||||
# Due to a Safari bug, in some browser, two csrftoken cookies with different values
|
||||
# exist: one unpartitioned, one partitioned. This function generates an additional
|
||||
# Set-Cookie header to get rid of the unpartitioned one.
|
||||
|
||||
cookie_name = '__Host-' + settings.CSRF_COOKIE_NAME
|
||||
|
||||
if request.is_secure() and cookie_name in request.COOKIES:
|
||||
if request.scheme == 'https' and cookie_name in request.COOKIES:
|
||||
values = get_all_values_of_cookie(request.headers.get('Cookie'), cookie_name)
|
||||
if len(values) > 1:
|
||||
logger.info('Trying to remove duplicated %s cookies: %r', cookie_name, values)
|
||||
thoroughly_delete_cookie(
|
||||
response,
|
||||
cookie_name,
|
||||
secure=request.is_secure(),
|
||||
path=settings.CSRF_COOKIE_PATH,
|
||||
httponly=settings.CSRF_COOKIE_HTTPONLY
|
||||
)
|
||||
|
||||
# Make sure the set_cookie_without_samesite below will add a new item in the dictionary, placing
|
||||
# it below our deletion header.
|
||||
response.cookies.pop(cookie_name, None)
|
||||
|
||||
# Add the deletion Set-Cookie header to the cookie dict under a wrong name, so it doesn't get
|
||||
# overwritten by the set_cookie_without_samesite call below. This works because the code in
|
||||
# django.core.handlers.wsgi/asgi, that generates the actual Set-Cookie headers, only iterates
|
||||
# over cookie.values(), ignoring the keys.
|
||||
response.cookies['___DELETECOOKIE___' + cookie_name] = make_delete_morsel(cookie_name)
|
||||
|
||||
|
||||
csrf_protect = decorator_from_middleware(CsrfViewMiddleware)
|
||||
def get_all_values_of_cookie(cookie_header, cookie_name):
|
||||
# like django.http.cookie.parse_cookie, but returns all values of duplicated cookies instead of only the last
|
||||
values = list()
|
||||
if not cookie_header:
|
||||
return values
|
||||
for chunk in cookie_header.split(";"):
|
||||
if "=" in chunk:
|
||||
key, val = chunk.split("=", 1)
|
||||
else:
|
||||
# Assume an empty name per
|
||||
# https://bugzilla.mozilla.org/show_bug.cgi?id=169091
|
||||
key, val = "", chunk
|
||||
key, val = key.strip(), val.strip()
|
||||
if key == cookie_name:
|
||||
values.append(val)
|
||||
return values
|
||||
|
||||
|
||||
class _EnsureCsrfToken(CsrfViewMiddleware):
|
||||
# Behave like CsrfViewMiddleware but don't reject requests or log warnings.
|
||||
def _reject(self, request, reason):
|
||||
return None
|
||||
|
||||
|
||||
requires_csrf_token = decorator_from_middleware(_EnsureCsrfToken)
|
||||
def make_delete_morsel(name):
|
||||
m = Morsel()
|
||||
m.set(name, '', '')
|
||||
m['expires'] = datetime.utcfromtimestamp(0).strftime("%a, %d %b %Y %H:%M:%S GMT")
|
||||
m['samesite'] = 'None'
|
||||
m['secure'] = True
|
||||
m['path'] = settings.CSRF_COOKIE_PATH
|
||||
m['httponly'] = settings.CSRF_COOKIE_HTTPONLY
|
||||
return m
|
||||
|
||||
@@ -26,7 +26,6 @@ from collections import defaultdict
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.logentrytypes import EventLogEntryType, log_entry_types
|
||||
@@ -154,7 +153,7 @@ def control_order_position_info(sender: Event, position, request, order: Order,
|
||||
'event': sender,
|
||||
'position': position
|
||||
}
|
||||
return mark_safe(template.render(ctx, request=request).strip())
|
||||
return template.render(ctx, request=request).strip()
|
||||
|
||||
|
||||
@receiver(order_info, dispatch_uid="badges_control_order_info")
|
||||
|
||||
@@ -162,6 +162,7 @@ class XHRView(View):
|
||||
|
||||
paypal_order = prov._create_paypal_order(request, None, cart_total)
|
||||
r = JsonResponse(paypal_order.dict() if paypal_order else {})
|
||||
r._csp_ignore = True
|
||||
return r
|
||||
|
||||
|
||||
|
||||
@@ -162,8 +162,6 @@ class ScheduledMail(models.Model):
|
||||
send_to_orders = self.rule.send_to in (Rule.CUSTOMERS, Rule.BOTH)
|
||||
send_to_attendees = self.rule.send_to in (Rule.ATTENDEES, Rule.BOTH)
|
||||
|
||||
position_ids = op_qs.values_list('id', flat=True)
|
||||
|
||||
for o in orders:
|
||||
with language(o.locale, e.settings.region):
|
||||
positions = list(o.positions.all())
|
||||
@@ -193,29 +191,28 @@ class ScheduledMail(models.Model):
|
||||
positions = [p for p in positions if p.subevent_id == self.subevent_id]
|
||||
|
||||
for p in positions:
|
||||
if p.id in position_ids:
|
||||
if p.attendee_email and (p.attendee_email != o.email or not o_sent):
|
||||
email_ctx = get_email_context(
|
||||
event=e,
|
||||
order=o,
|
||||
invoice_address=ia,
|
||||
position=p,
|
||||
event_or_subevent=self.subevent or e,
|
||||
)
|
||||
p.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
attach_ical=self.rule.attach_ical,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.position.email.sent')
|
||||
elif not o_sent and o.email:
|
||||
email_ctx = get_email_context(
|
||||
event=e,
|
||||
order=o,
|
||||
invoice_address=ia,
|
||||
event_or_subevent=self.subevent or e,
|
||||
)
|
||||
o.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
attach_ical=self.rule.attach_ical,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
|
||||
o_sent = True
|
||||
if p.attendee_email and (p.attendee_email != o.email or not o_sent):
|
||||
email_ctx = get_email_context(
|
||||
event=e,
|
||||
order=o,
|
||||
invoice_address=ia,
|
||||
position=p,
|
||||
event_or_subevent=self.subevent or e,
|
||||
)
|
||||
p.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
attach_ical=self.rule.attach_ical,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.position.email.sent')
|
||||
elif not o_sent and o.email:
|
||||
email_ctx = get_email_context(
|
||||
event=e,
|
||||
order=o,
|
||||
invoice_address=ia,
|
||||
event_or_subevent=self.subevent or e,
|
||||
)
|
||||
o.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
attach_ical=self.rule.attach_ical,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
|
||||
o_sent = True
|
||||
|
||||
self.last_successful_order_id = o.pk
|
||||
|
||||
@@ -273,7 +270,7 @@ class Rule(models.Model, LoggingMixin):
|
||||
|
||||
date_is_absolute = models.BooleanField(default=True, blank=True)
|
||||
offset_to_event_end = models.BooleanField(default=False, blank=True) # no verbose name because not actually
|
||||
offset_is_after = models.BooleanField(default=False, blank=True) # displayed in any forms
|
||||
offset_is_after = models.BooleanField(default=False, blank=True) # displayed in any forms
|
||||
|
||||
send_to = models.CharField(max_length=10, choices=SEND_TO_CHOICES, default=CUSTOMERS, verbose_name=_('Send email to'))
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ def control_order_position_info(sender: Event, position, request, order, **kwarg
|
||||
'position': position,
|
||||
'layouts': layouts,
|
||||
}
|
||||
return template.render(ctx, request=request)
|
||||
return template.render(ctx, request=request).strip()
|
||||
|
||||
|
||||
override_layout = EventPluginSignal()
|
||||
|
||||
@@ -205,8 +205,8 @@ const CSRF_TOKEN = document.querySelector<HTMLInputElement>('input[name=csrfmidd
|
||||
function handleAuthError (response: Response): void {
|
||||
if ([401, 403].includes(response.status)) {
|
||||
window.location.href = '/control/login?next=' + encodeURIComponent(
|
||||
window.location.pathname + window.location.search
|
||||
) + window.location.hash
|
||||
window.location.pathname + window.location.search + window.location.hash
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -161,8 +161,7 @@ voucher_redeem_info = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``voucher``
|
||||
|
||||
This signal is sent out to display additional information on the "redeem a voucher" page.
|
||||
You are expected to return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
This signal is sent out to display additional information on the "redeem a voucher" page
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
@@ -195,7 +194,6 @@ Arguments: ``request``
|
||||
|
||||
This signals allows you to add HTML content to the confirmation page that is presented at the
|
||||
end of the checkout process, just before the order is being created.
|
||||
You are expected to return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event. A ``request``
|
||||
argument will contain the request object.
|
||||
@@ -278,8 +276,7 @@ order_info = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``order``, ``request``
|
||||
|
||||
This signal is sent out to display additional information on the order detail page.
|
||||
Return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
This signal is sent out to display additional information on the order detail page
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
@@ -288,8 +285,7 @@ position_info = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``order``, ``position``, ``request``
|
||||
|
||||
This signal is sent out to display additional information on the position detail page.
|
||||
Return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
This signal is sent out to display additional information on the position detail page
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
@@ -298,8 +294,7 @@ order_info_top = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``order``, ``request``
|
||||
|
||||
This signal is sent out to display additional information on top of the order detail page.
|
||||
Return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
This signal is sent out to display additional information on top of the order detail page
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
@@ -308,8 +303,7 @@ position_info_top = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``order``, ``position``, ``request``
|
||||
|
||||
This signal is sent out to display additional information on top of the position detail page.
|
||||
Return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
This signal is sent out to display additional information on top of the position detail page
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
@@ -355,7 +349,7 @@ This signal is sent out to display additional information on the frontpage above
|
||||
of products and but below a custom frontpage text.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event. The
|
||||
receivers are expected to return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
receivers are expected to return HTML.
|
||||
"""
|
||||
|
||||
render_seating_plan = EventPluginSignal()
|
||||
@@ -367,7 +361,7 @@ You will be passed the ``request`` as a keyword argument. If applicable, a ``sub
|
||||
``voucher`` argument might be given.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event. The
|
||||
receivers are expected to return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
receivers are expected to return HTML.
|
||||
"""
|
||||
|
||||
front_page_bottom = EventPluginSignal()
|
||||
@@ -378,7 +372,7 @@ This signal is sent out to display additional information on the frontpage below
|
||||
of products.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event. The
|
||||
receivers are expected to return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
receivers are expected to return HTML.
|
||||
"""
|
||||
|
||||
front_page_bottom_widget = EventPluginSignal()
|
||||
@@ -389,7 +383,7 @@ This signal is sent out to display additional information on the frontpage below
|
||||
of products if the front page is shown in the widget.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event. The
|
||||
receivers are expected to return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
receivers are expected to return HTML.
|
||||
"""
|
||||
|
||||
checkout_all_optional = EventPluginSignal()
|
||||
@@ -409,7 +403,7 @@ Arguments: ``item``, ``variation``, ``subevent``
|
||||
|
||||
This signal is sent out when the description of an item or variation is rendered and allows you to append
|
||||
additional text to the description. You are passed the ``item``, ``variation`` and ``subevent``. You are
|
||||
expected to return markdown.
|
||||
expected to return HTML.
|
||||
"""
|
||||
|
||||
register_cookie_providers = EventPluginSignal()
|
||||
|
||||
@@ -44,6 +44,7 @@ from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.decorators.csrf import csrf_protect
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.views.generic import FormView, ListView, View
|
||||
|
||||
@@ -100,6 +101,7 @@ class LoginView(RedirectBackMixin, FormView):
|
||||
redirect_authenticated_user = True
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@method_decorator(csrf_protect)
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.organizer.settings.customer_accounts:
|
||||
@@ -211,6 +213,7 @@ class RegistrationView(RedirectBackMixin, FormView):
|
||||
redirect_authenticated_user = True
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@method_decorator(csrf_protect)
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.organizer.settings.customer_accounts:
|
||||
@@ -254,6 +257,7 @@ class SetPasswordView(FormView):
|
||||
template_name = 'pretixpresale/organizers/customer_setpassword.html'
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@method_decorator(csrf_protect)
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.organizer.settings.customer_accounts:
|
||||
@@ -297,6 +301,7 @@ class ResetPasswordView(FormView):
|
||||
template_name = 'pretixpresale/organizers/customer_resetpw.html'
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@method_decorator(csrf_protect)
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.organizer.settings.customer_accounts:
|
||||
@@ -522,6 +527,7 @@ class ChangePasswordView(CustomerAccountBaseMixin, FormView):
|
||||
form_class = ChangePasswordForm
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@method_decorator(csrf_protect)
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.organizer.settings.customer_accounts:
|
||||
@@ -555,6 +561,7 @@ class ChangeInformationView(CustomerAccountBaseMixin, FormView):
|
||||
form_class = ChangeInfoForm
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@method_decorator(csrf_protect)
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.organizer.settings.customer_accounts:
|
||||
@@ -662,6 +669,7 @@ class SSOLoginView(RedirectBackMixin, View):
|
||||
redirect_authenticated_user = True
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@method_decorator(csrf_protect)
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.organizer.settings.customer_accounts:
|
||||
@@ -724,6 +732,7 @@ class SSOLoginReturnView(RedirectBackMixin, View):
|
||||
redirect_authenticated_user = True
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@method_decorator(csrf_protect)
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.organizer.settings.customer_accounts:
|
||||
|
||||
@@ -48,6 +48,7 @@ def theme_css(request, **kwargs):
|
||||
obj = getattr(request, "event", request.organizer)
|
||||
css = get_theme_vars_css(obj, widget=False)
|
||||
resp = HttpResponse(css, content_type="text/css")
|
||||
resp._csp_ignore = True
|
||||
resp["Access-Control-Allow-Origin"] = "*"
|
||||
if "version" in request.GET:
|
||||
resp["Expires"] = http_date(time.time() + 3600 * 24 * 30)
|
||||
|
||||
@@ -164,6 +164,7 @@ def widget_css(request, version, **kwargs):
|
||||
css = f"/* v{version} */\n" + theme_css + widget_css
|
||||
|
||||
resp = FileResponse(css, content_type='text/css')
|
||||
resp._csp_ignore = True
|
||||
resp['Access-Control-Allow-Origin'] = '*'
|
||||
return resp
|
||||
|
||||
@@ -245,6 +246,7 @@ def widget_js(request, version, lang, **kwargs):
|
||||
cached_js = cache.get(cache_prefix)
|
||||
if cached_js and not settings.DEBUG:
|
||||
resp = HttpResponse(cached_js, content_type='text/javascript')
|
||||
resp._csp_ignore = True
|
||||
resp['Access-Control-Allow-Origin'] = '*'
|
||||
return resp
|
||||
|
||||
@@ -276,6 +278,7 @@ def widget_js(request, version, lang, **kwargs):
|
||||
gs.settings.set(checksum_key, checksum)
|
||||
cache.set(cache_prefix, data, 3600 * 4)
|
||||
resp = HttpResponse(data, content_type='text/javascript')
|
||||
resp._csp_ignore = True
|
||||
resp['Access-Control-Allow-Origin'] = '*'
|
||||
return resp
|
||||
|
||||
@@ -411,6 +414,7 @@ class WidgetAPIProductList(EventListMixin, View):
|
||||
self.post_process(data)
|
||||
resp = JsonResponse(data)
|
||||
resp['Access-Control-Allow-Origin'] = '*'
|
||||
resp._csp_ignore = True
|
||||
return resp
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
@@ -262,9 +262,6 @@ MAIL_FROM_NOTIFICATIONS = config.get('mail', 'from_notifications', fallback=MAIL
|
||||
MAIL_FROM_ORGANIZERS = config.get('mail', 'from_organizers', fallback=MAIL_FROM)
|
||||
MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED = config.getboolean('mail', 'custom_sender_verification_required', fallback=True)
|
||||
MAIL_CUSTOM_SENDER_SPF_STRING = config.get('mail', 'custom_sender_spf_string', fallback='')
|
||||
MAIL_CUSTOM_SENDER_DKIM_SELECTOR = config.get('mail', 'custom_sender_dkim_selector', fallback='')
|
||||
MAIL_CUSTOM_SENDER_DKIM_CNAME = config.get('mail', 'custom_sender_dkim_cname', fallback='')
|
||||
MAIL_CUSTOM_SENDER_DMARC_REQUIRED = config.getboolean('mail', 'custom_sender_dmarc_required', fallback=False)
|
||||
MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = config.getboolean('mail', 'custom_smtp_allow_private_networks', fallback=DEBUG)
|
||||
EMAIL_HOST = config.get('mail', 'host', fallback='localhost')
|
||||
EMAIL_PORT = config.getint('mail', 'port', fallback=25)
|
||||
@@ -622,8 +619,6 @@ LOGGING = {
|
||||
'default': {
|
||||
'format': (
|
||||
'%(levelname)s %(asctime)s RequestId=%(request_id)s %(name)s %(module)s %(message)s'
|
||||
if REQUEST_ID_HEADER
|
||||
else '%(levelname)s %(asctime)s %(name)s %(module)s %(message)s'
|
||||
)
|
||||
},
|
||||
},
|
||||
@@ -710,6 +705,7 @@ LOGGING = {
|
||||
},
|
||||
},
|
||||
}
|
||||
CELERY_WORKER_HIJACK_ROOT_LOGGER = False
|
||||
|
||||
SENTRY_ENABLED = False
|
||||
if config.has_option('sentry', 'dsn') and not any(c in sys.argv for c in ('shell', 'shell_scoped', 'shell_plus')):
|
||||
|
||||
+7
File diff suppressed because one or more lines are too long
@@ -113,10 +113,6 @@ function async_task_check_error(jqXHR, textStatus, errorThrown) {
|
||||
"use strict";
|
||||
var respdom = $(jqXHR.responseText);
|
||||
var c = respdom.filter('.container');
|
||||
if (jqXHR.status === 401 && jqXHR.getResponseHeader("X-Login-Url")) {
|
||||
window.location = jqXHR.getResponseHeader("X-Login-Url") + "?next=" + encodeURIComponent(location.pathname + location.search + location.hash);
|
||||
return;
|
||||
}
|
||||
if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) {
|
||||
// This is a failed form validation, let's just use it
|
||||
$("body").data('ajaxing', false);
|
||||
@@ -171,10 +167,6 @@ function async_task_callback(data, jqXHR, status) {
|
||||
function async_task_error(jqXHR, textStatus, errorThrown) {
|
||||
"use strict";
|
||||
$("body").data('ajaxing', false);
|
||||
if (jqXHR.status === 401 && jqXHR.getResponseHeader("X-Login-Url")) {
|
||||
window.location = jqXHR.getResponseHeader("X-Login-Url") + "?next=" + encodeURIComponent(location.pathname + location.search + location.hash);
|
||||
return;
|
||||
}
|
||||
waitingDialog.hide();
|
||||
if (textStatus === "timeout") {
|
||||
alert(gettext("The request took too long. Please try again."));
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
/*globals $, gettext, fabric, PDFJS*/
|
||||
|
||||
window.module = {} // Hack to load ajv-compiled schema without actual module loading
|
||||
|
||||
fabric.Poweredby = fabric.util.createClass(fabric.Image, {
|
||||
type: 'poweredby',
|
||||
|
||||
@@ -222,6 +219,7 @@ var editor = {
|
||||
uploaded_file_id: null,
|
||||
_window_loaded: false,
|
||||
_fabric_loaded: false,
|
||||
schema: null,
|
||||
|
||||
_px2mm: function (v) {
|
||||
return v / editor.pdf_scale / 72 * editor.pdf_page.userUnit * 25.4;
|
||||
@@ -1322,11 +1320,14 @@ var editor = {
|
||||
|
||||
_source_save: function () {
|
||||
try {
|
||||
var Ajv = window.ajv2020
|
||||
var ajv = new Ajv()
|
||||
var validate = ajv.compile(editor.schema)
|
||||
var data = JSON.parse($("#source-textarea").val())
|
||||
var valid = validate30(data)
|
||||
var valid = validate(data)
|
||||
|
||||
if (!valid) {
|
||||
console.log(validate30.errors)
|
||||
console.log(validate.errors)
|
||||
alert("Invalid input syntax. If you're familiar with this, check out the developer console for a full " +
|
||||
"error log. Otherwise, please contact support.")
|
||||
} else {
|
||||
@@ -1462,6 +1463,10 @@ var editor = {
|
||||
editor._update_save_button();
|
||||
});
|
||||
$("#pdf-info-width, #pdf-info-height").bind('change input', editor._paper_size_warning);
|
||||
|
||||
$.getJSON($("#schema-url").text(), function (data) {
|
||||
editor.schema = data;
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -58,14 +58,13 @@ var i18nToString = function (i18nstring) {
|
||||
};
|
||||
|
||||
$(document).ajaxError(function (event, jqXHR, settings, thrownError) {
|
||||
waitingDialog.hide();
|
||||
var c = $(jqXHR.responseText).filter('.container');
|
||||
if (jqXHR.status === 401 && jqXHR.getResponseHeader("X-Login-Url")) {
|
||||
window.location = jqXHR.getResponseHeader("X-Login-Url") + "?next=" + encodeURIComponent(location.pathname + location.search + location.hash);
|
||||
if (jqXHR.responseText && jqXHR.responseText.indexOf("<!-- pretix-login-marker -->") !== -1) {
|
||||
location.href = '/control/login?next=' + encodeURIComponent(location.pathname + location.search + location.hash)
|
||||
} else if (c.length > 0) {
|
||||
waitingDialog.hide();
|
||||
ajaxErrDialog.show(c.first().html());
|
||||
} else if (thrownError !== "abort" && thrownError !== "") {
|
||||
waitingDialog.hide();
|
||||
console.error(event, jqXHR, settings, thrownError);
|
||||
alert(gettext('Unknown error.'));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$comment": "If the schema is changed, regenerate pdf-layout.validate.js with $ ajv compile -s ./pretix/static/schema/pdf-layout.schema.json --spec=draft2020 -o ./pretix/static/schema/pdf-layout.validate.js",
|
||||
"title": "Ticket Layout",
|
||||
"description": "Dynamic elements for a PDF layout",
|
||||
"type": "array",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -258,7 +258,7 @@ def test_list_list(token_client, organizer, event, clist, item, subevent, django
|
||||
res["id"] = clist.pk
|
||||
res["limit_products"] = [item.pk]
|
||||
|
||||
with django_assert_num_queries(9):
|
||||
with django_assert_num_queries(11):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug))
|
||||
assert resp.status_code == 200
|
||||
assert [res] == resp.data['results']
|
||||
@@ -437,7 +437,7 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
|
||||
p3["addon_to"] = p1["id"]
|
||||
|
||||
# All items
|
||||
with django_assert_num_queries(22):
|
||||
with django_assert_num_queries(24):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
|
||||
@@ -1865,7 +1865,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
|
||||
assert resp.status_code == 200
|
||||
event.refresh_from_db()
|
||||
|
||||
with assert_num_queries(10):
|
||||
with assert_num_queries(12):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
|
||||
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=true'
|
||||
.format(organizer.slug, event.slug))
|
||||
@@ -1875,7 +1875,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
|
||||
with scope(organizer=organizer):
|
||||
v0 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-0'))
|
||||
|
||||
with assert_num_queries(12):
|
||||
with assert_num_queries(14):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
|
||||
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=false'
|
||||
.format(organizer.slug, event.slug))
|
||||
@@ -1883,7 +1883,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
|
||||
assert len(resp.data['results']) == 1
|
||||
assert resp.data['results'][0]['voucher']['id'] == v0.pk
|
||||
|
||||
with assert_num_queries(10):
|
||||
with assert_num_queries(12):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
|
||||
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=true'
|
||||
.format(organizer.slug, event.slug))
|
||||
@@ -1894,7 +1894,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
|
||||
v1 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-1'))
|
||||
v2 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-2'))
|
||||
|
||||
with assert_num_queries(14):
|
||||
with assert_num_queries(16):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
|
||||
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=false'
|
||||
.format(organizer.slug, event.slug))
|
||||
|
||||
@@ -425,7 +425,7 @@ def test_item_list(token_client, organizer, event, team, item):
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_item_list_queries(token_client, organizer, event, team, item, item3):
|
||||
with assert_num_queries(16):
|
||||
with assert_num_queries(18):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/'.format(organizer.slug, event.slug))
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
@@ -730,35 +730,6 @@ def test_payment_create_confirmed(token_client, organizer, event, order):
|
||||
assert len(djmail.outbox) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_payment_create_confirmed_after_expiry(token_client, organizer, event, order):
|
||||
djmail.outbox = []
|
||||
order.expires = now() - datetime.timedelta(days=2)
|
||||
order.save()
|
||||
event.settings.payment_term_last = (now() - datetime.timedelta(days=2)).strftime('%Y-%m-%d')
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/payments/'.format(
|
||||
organizer.slug, event.slug, order.code
|
||||
), format='json', data={
|
||||
'provider': 'banktransfer',
|
||||
'state': 'confirmed',
|
||||
'amount': order.total,
|
||||
'send_email': False,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
},
|
||||
"force": True
|
||||
})
|
||||
with scopes_disabled():
|
||||
p = order.payments.last()
|
||||
assert resp.status_code == 201
|
||||
assert p.state == OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||
assert p.info_data == {'foo': 'bar'}
|
||||
order.refresh_from_db()
|
||||
assert order.status == Order.STATUS_PAID
|
||||
assert len(djmail.outbox) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_payment_create_pending(token_client, organizer, event, order):
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/payments/'.format(
|
||||
@@ -1195,10 +1166,10 @@ def test_orderposition_list(
|
||||
'type': 'entry'
|
||||
}]
|
||||
if '/events/' in endpoint:
|
||||
with django_assert_num_queries(16):
|
||||
with django_assert_num_queries(18):
|
||||
resp = token_client.get(endpoint + '?has_checkin=true')
|
||||
else:
|
||||
with django_assert_num_queries(15):
|
||||
with django_assert_num_queries(17):
|
||||
resp = token_client.get(endpoint + '?has_checkin=true')
|
||||
assert [res] == resp.data['results']
|
||||
|
||||
|
||||
@@ -964,8 +964,6 @@ def test_redeemed_is_not_writable(token_client, organizer, event, item):
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_multiple_vouchers(token_client, organizer, event, item):
|
||||
with scopes_disabled():
|
||||
event.quotas.create(name="Q", size=100).items.add(item)
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/vouchers/batch_create/'.format(organizer.slug, event.slug),
|
||||
data=[
|
||||
@@ -1014,8 +1012,6 @@ def test_create_multiple_vouchers(token_client, organizer, event, item):
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_multiple_vouchers_one_invalid(token_client, organizer, event, item):
|
||||
with scopes_disabled():
|
||||
event.quotas.create(name="Q", size=100).items.add(item)
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/vouchers/batch_create/'.format(organizer.slug, event.slug),
|
||||
data=[
|
||||
@@ -1059,8 +1055,6 @@ def test_create_multiple_vouchers_one_invalid(token_client, organizer, event, it
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_multiple_vouchers_duplicate_code(token_client, organizer, event, item):
|
||||
with scopes_disabled():
|
||||
event.quotas.create(name="Q", size=100).items.add(item)
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/vouchers/batch_create/'.format(organizer.slug, event.slug),
|
||||
data=[
|
||||
@@ -1104,8 +1098,6 @@ def test_create_multiple_vouchers_duplicate_code(token_client, organizer, event,
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_multiple_vouchers_autogenerate_codes(token_client, organizer, event, item):
|
||||
with scopes_disabled():
|
||||
event.quotas.create(name="Q", size=100).items.add(item)
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/vouchers/batch_create/'.format(organizer.slug, event.slug),
|
||||
data=[
|
||||
@@ -1165,8 +1157,6 @@ def seat1(item, event):
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_multiple_vouchers_duplicate_seat(token_client, organizer, event, item, seat1, seatingplan):
|
||||
with scopes_disabled():
|
||||
event.quotas.create(name="Q", size=100).items.add(item)
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/vouchers/batch_create/'.format(organizer.slug, event.slug),
|
||||
data=[
|
||||
|
||||
@@ -159,22 +159,6 @@ class OrganizerTest(SoupTest):
|
||||
self.orga1.settings.flush()
|
||||
assert "mail_from" not in self.orga1.settings._cache()
|
||||
|
||||
@staticmethod
|
||||
def _fake_dmarc_record(hostname):
|
||||
return {
|
||||
'test.pretix.dev': 'v=DMARC1; p=quarantine; sp=none; adkim=r; aspf=r;',
|
||||
'bad.pretix.dev': None,
|
||||
'none.pretix.dev': None,
|
||||
}[hostname]
|
||||
|
||||
@staticmethod
|
||||
def _fake_cname_record(hostname):
|
||||
return {
|
||||
'pretix._domainkey.test.pretix.dev': 'test-pretix-dev.dkim.pretix.eu.',
|
||||
'pretix._domainkey.bad.pretix.dev': 'example.org',
|
||||
'pretix._domainkey.none.pretix.dev': None,
|
||||
}[hostname]
|
||||
|
||||
@staticmethod
|
||||
def _fake_spf_record(hostname):
|
||||
return {
|
||||
@@ -188,17 +172,9 @@ class OrganizerTest(SoupTest):
|
||||
'spftest.pretix.dev': None,
|
||||
}[hostname]
|
||||
|
||||
@override_settings(
|
||||
MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False,
|
||||
MAIL_CUSTOM_SENDER_SPF_STRING="include:spftest.pretix.dev include:test2.pretix.dev",
|
||||
MAIL_CUSTOM_SENDER_DKIM_SELECTOR="pretix",
|
||||
MAIL_CUSTOM_SENDER_DKIM_CNAME="dkim.pretix.eu.",
|
||||
MAIL_CUSTOM_SENDER_DMARC_REQUIRED=True,
|
||||
)
|
||||
def test_email_setup_no_verification_spf_dmarc_dkim_success(self):
|
||||
@override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False, MAIL_CUSTOM_SENDER_SPF_STRING="include:spftest.pretix.dev include:test2.pretix.dev")
|
||||
def test_email_setup_no_verification_spf_success(self):
|
||||
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_spf_record", OrganizerTest._fake_spf_record)
|
||||
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_cname_record", OrganizerTest._fake_cname_record)
|
||||
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_dmarc_record", OrganizerTest._fake_dmarc_record)
|
||||
doc = self.post_doc(
|
||||
'/control/organizer/%s/settings/email/setup' % self.orga1.slug,
|
||||
{
|
||||
@@ -237,43 +213,6 @@ class OrganizerTest(SoupTest):
|
||||
# not yet saved
|
||||
assert "mail_from" not in self.orga1.settings._cache()
|
||||
|
||||
@override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False,
|
||||
MAIL_CUSTOM_SENDER_DKIM_SELECTOR="pretix",
|
||||
MAIL_CUSTOM_SENDER_DKIM_CNAME="dkim.pretix.eu.",
|
||||
MAIL_CUSTOM_SENDER_SPF_STRING="")
|
||||
def test_email_setup_no_verification_dkim_warning(self):
|
||||
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_cname_record", OrganizerTest._fake_cname_record)
|
||||
doc = self.post_doc(
|
||||
'/control/organizer/%s/settings/email/setup' % self.orga1.slug,
|
||||
{
|
||||
'mode': 'simple',
|
||||
'simple-mail_from': 'test@bad.pretix.dev',
|
||||
},
|
||||
follow=True
|
||||
)
|
||||
assert doc.select('.alert-danger')
|
||||
self.orga1.settings.flush()
|
||||
# not yet saved
|
||||
assert "mail_from" not in self.orga1.settings._cache()
|
||||
|
||||
@override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False,
|
||||
MAIL_CUSTOM_SENDER_DMARC_REQUIRED=True,
|
||||
MAIL_CUSTOM_SENDER_SPF_STRING="")
|
||||
def test_email_setup_no_verification_dmarc_warning(self):
|
||||
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_dmarc_record", OrganizerTest._fake_dmarc_record)
|
||||
doc = self.post_doc(
|
||||
'/control/organizer/%s/settings/email/setup' % self.orga1.slug,
|
||||
{
|
||||
'mode': 'simple',
|
||||
'simple-mail_from': 'test@bad.pretix.dev',
|
||||
},
|
||||
follow=True
|
||||
)
|
||||
assert doc.select('.alert-danger')
|
||||
self.orga1.settings.flush()
|
||||
# not yet saved
|
||||
assert "mail_from" not in self.orga1.settings._cache()
|
||||
|
||||
def test_email_setup_smtp(self):
|
||||
self.monkeypatch.setattr("pretix.base.email.test_custom_smtp_backend", lambda b, a: None)
|
||||
self.monkeypatch.setattr("socket.gethostbyname", lambda h: "8.8.8.8")
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
import datetime
|
||||
import decimal
|
||||
import json
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core import mail as djmail
|
||||
from django.test import TransactionTestCase
|
||||
@@ -44,8 +43,8 @@ from django_scopes import scopes_disabled
|
||||
from tests.base import SoupTestMixin, extract_form_fields
|
||||
|
||||
from pretix.base.models import (
|
||||
Event, Item, ItemVariation, Order, OrderPosition, Organizer, Quota,
|
||||
SeatingPlan, Team, User, Voucher, WaitingListEntry,
|
||||
Event, Item, ItemVariation, Order, OrderPosition, Organizer, Quota, Team,
|
||||
User, Voucher, WaitingListEntry,
|
||||
)
|
||||
|
||||
|
||||
@@ -135,49 +134,49 @@ class VoucherFormTest(SoupTestMixin, TransactionTestCase):
|
||||
def test_filter_status_valid(self):
|
||||
with scopes_disabled():
|
||||
v = self.event.vouchers.create(item=self.ticket)
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=v' % (self.orga.slug, self.event.slug))
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?status=v' % (self.orga.slug, self.event.slug))
|
||||
assert v.code in doc.content.decode()
|
||||
v.redeemed = 1
|
||||
v.save()
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=v' % (self.orga.slug, self.event.slug))
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?status=v' % (self.orga.slug, self.event.slug))
|
||||
assert v.code not in doc.content.decode()
|
||||
|
||||
def test_filter_status_redeemed(self):
|
||||
with scopes_disabled():
|
||||
v = self.event.vouchers.create(item=self.ticket, redeemed=1)
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=r' % (self.orga.slug, self.event.slug))
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?status=r' % (self.orga.slug, self.event.slug))
|
||||
assert v.code in doc.content.decode()
|
||||
v.redeemed = 0
|
||||
v.save()
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=r' % (self.orga.slug, self.event.slug))
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?status=r' % (self.orga.slug, self.event.slug))
|
||||
assert v.code not in doc.content.decode()
|
||||
|
||||
def test_filter_status_expired(self):
|
||||
with scopes_disabled():
|
||||
v = self.event.vouchers.create(item=self.ticket, valid_until=now() + datetime.timedelta(days=1))
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=e' % (self.orga.slug, self.event.slug))
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?status=e' % (self.orga.slug, self.event.slug))
|
||||
assert v.code not in doc.content.decode()
|
||||
v.valid_until = now() - datetime.timedelta(days=1)
|
||||
v.save()
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=e' % (self.orga.slug, self.event.slug))
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?status=e' % (self.orga.slug, self.event.slug))
|
||||
assert v.code in doc.content.decode()
|
||||
|
||||
def test_filter_tag(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(item=self.ticket, code='ABCDEFG', comment='Foo', tag='bar')
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-tag=bar' % (self.orga.slug, self.event.slug))
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?tag=bar' % (self.orga.slug, self.event.slug))
|
||||
assert 'ABCDEFG' in doc.content.decode()
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-tag=baz' % (self.orga.slug, self.event.slug))
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?tag=baz' % (self.orga.slug, self.event.slug))
|
||||
assert 'ABCDEFG' not in doc.content.decode()
|
||||
|
||||
def test_search_code(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(item=self.ticket, code='ABCDEFG', comment='Foo')
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-search=ABCDEFG' % (self.orga.slug, self.event.slug))
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?search=ABCDEFG' % (self.orga.slug, self.event.slug))
|
||||
assert 'ABCDEFG' in doc.content.decode()
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-search=Foo' % (self.orga.slug, self.event.slug))
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?search=Foo' % (self.orga.slug, self.event.slug))
|
||||
assert 'ABCDEFG' in doc.content.decode()
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?filter-search=12345' % (self.orga.slug, self.event.slug))
|
||||
doc = self.client.get('/control/event/%s/%s/vouchers/?search=12345' % (self.orga.slug, self.event.slug))
|
||||
assert 'ABCDEFG' not in doc.content.decode()
|
||||
|
||||
def test_bulk_rng(self):
|
||||
@@ -807,426 +806,3 @@ class VoucherFormTest(SoupTestMixin, TransactionTestCase):
|
||||
|
||||
assert 'walk-ins' in names
|
||||
assert 'waiting-list' not in names
|
||||
|
||||
|
||||
class VoucherBulkEditFormTest(SoupTestMixin, TransactionTestCase):
|
||||
@scopes_disabled()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||||
self.orga = Organizer.objects.create(name='CCC', slug='ccc')
|
||||
self.event = Event.objects.create(
|
||||
organizer=self.orga, name='30C3', slug='30c3',
|
||||
date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc),
|
||||
)
|
||||
t = Team.objects.create(organizer=self.orga, all_event_permissions=True)
|
||||
t.members.add(self.user)
|
||||
t.limit_events.add(self.event)
|
||||
self.client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
|
||||
self.quota_shirts = Quota.objects.create(event=self.event, name='Shirts', size=2)
|
||||
self.shirt = Item.objects.create(event=self.event, name='T-Shirt', default_price=12)
|
||||
self.quota_shirts.items.add(self.shirt)
|
||||
self.shirt_red = ItemVariation.objects.create(item=self.shirt, default_price=14, value='Red')
|
||||
self.shirt_blue = ItemVariation.objects.create(item=self.shirt, value='Blue')
|
||||
self.quota_shirts.variations.add(self.shirt_red)
|
||||
self.quota_shirts.variations.add(self.shirt_blue)
|
||||
|
||||
self.quota_tickets = Quota.objects.create(event=self.event, name='Tickets', size=2)
|
||||
self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket',
|
||||
default_price=23)
|
||||
self.quota_tickets.items.add(self.ticket)
|
||||
self.url = f'/control/event/{self.orga.slug}/{self.event.slug}/vouchers/bulk_edit'
|
||||
|
||||
def test_simple_edit(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(
|
||||
quota=self.quota_tickets,
|
||||
max_usages=10,
|
||||
price_mode="set",
|
||||
value=13,
|
||||
)
|
||||
self.event.vouchers.create(
|
||||
item=self.ticket,
|
||||
max_usages=10,
|
||||
price_mode="set",
|
||||
value=12,
|
||||
)
|
||||
|
||||
doc = self.post_doc(self.url, {
|
||||
'__ALL': 'on',
|
||||
}, follow=True)
|
||||
fields = extract_form_fields(doc)
|
||||
assert fields.get('bulkedit-max_usages') == '10'
|
||||
assert fields.get('bulkedit-price_mode') == 'set'
|
||||
assert not fields.get('bulkedit-value')
|
||||
fields.update({
|
||||
'_bulk': ['bulkedit__price', 'bulkeditmin_usages', 'bulkedittag', 'bulkeditshow_hidden_items'],
|
||||
'bulkedit-price_mode': 'percent',
|
||||
'bulkedit-value': '15',
|
||||
'bulkedit-min_usages': '3',
|
||||
'bulkedit-tag': 'tagged',
|
||||
'bulkedit-comment': 'This is a comment', # will be ignored, as not included in _bulk
|
||||
'bulkedit-show_hidden_items': '',
|
||||
})
|
||||
doc = self.post_doc(self.url, fields, follow=True)
|
||||
assert doc.select(".alert-success")
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert v.price_mode == "percent"
|
||||
assert v.value == Decimal("15.00")
|
||||
assert v.min_usages == 3
|
||||
assert v.tag == "tagged"
|
||||
assert v.comment == ""
|
||||
assert v.show_hidden_items is False
|
||||
|
||||
def _update_all(self, data: dict, expect_error: str=None):
|
||||
doc = self.post_doc(self.url, {
|
||||
'__ALL': 'on',
|
||||
}, follow=True)
|
||||
fields = extract_form_fields(doc)
|
||||
fields.update(data)
|
||||
doc = self.post_doc(self.url, fields, follow=True)
|
||||
error_texts = [el.text for el in doc.select(".alert-danger, .has-error")]
|
||||
if expect_error:
|
||||
assert doc.select(".alert-danger")
|
||||
assert any(expect_error in t for t in error_texts), error_texts
|
||||
else:
|
||||
assert doc.select(".alert-success"), error_texts
|
||||
|
||||
def test_change_itemvar_to_product(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(quota=self.quota_tickets)
|
||||
self.event.vouchers.create(item=self.ticket)
|
||||
|
||||
self._update_all({
|
||||
'_bulk': ['bulkedititemvar'],
|
||||
'bulkedit-itemvar': f'{self.ticket.pk}',
|
||||
})
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert v.item == self.ticket
|
||||
assert not v.variation
|
||||
assert not v.quota
|
||||
|
||||
def test_change_itemvar_to_variation(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(quota=self.quota_tickets)
|
||||
self.event.vouchers.create(item=self.ticket)
|
||||
|
||||
self._update_all({
|
||||
'_bulk': ['bulkedititemvar'],
|
||||
'bulkedit-itemvar': f'{self.shirt.pk}-{self.shirt_red.pk}',
|
||||
})
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert v.item == self.shirt
|
||||
assert v.variation == self.shirt_red
|
||||
assert not v.quota
|
||||
|
||||
def test_change_itemvar_to_quota(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(quota=self.quota_tickets)
|
||||
self.event.vouchers.create(item=self.ticket)
|
||||
|
||||
self._update_all({
|
||||
'_bulk': ['bulkedititemvar'],
|
||||
'bulkedit-itemvar': f'q-{self.quota_tickets.pk}',
|
||||
})
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert not v.item
|
||||
assert not v.variation
|
||||
assert v.quota == self.quota_tickets
|
||||
|
||||
def test_change_itemvar_to_all(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(quota=self.quota_tickets)
|
||||
self.event.vouchers.create(item=self.ticket)
|
||||
|
||||
self._update_all({
|
||||
'_bulk': ['bulkedititemvar'],
|
||||
'bulkedit-itemvar': '',
|
||||
})
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert not v.item
|
||||
assert not v.variation
|
||||
assert not v.quota
|
||||
|
||||
def test_change_max_usages(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(quota=self.quota_tickets, max_usages=15, redeemed=4)
|
||||
self.event.vouchers.create(item=self.ticket, max_usages=15, redeemed=2)
|
||||
|
||||
self._update_all({
|
||||
'_bulk': ['bulkeditmax_usages'],
|
||||
'bulkedit-max_usages': '3',
|
||||
}, expect_error="already been redeemed 4 times")
|
||||
self._update_all({
|
||||
'_bulk': ['bulkeditmax_usages'],
|
||||
'bulkedit-max_usages': '4',
|
||||
})
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert v.max_usages == 4
|
||||
|
||||
def _requires_one_more_quota(self, data: dict, quota=None):
|
||||
self._update_all(data, expect_error="no sufficient quota")
|
||||
quota = quota or self.quota_tickets
|
||||
quota.size += 1
|
||||
quota.save()
|
||||
self._update_all(data)
|
||||
|
||||
def test_quota_check_change_item(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(item=self.shirt, block_quota=True, max_usages=2, redeemed=1)
|
||||
self.event.vouchers.create(item=self.shirt, block_quota=True, max_usages=3, redeemed=1)
|
||||
self._requires_one_more_quota({
|
||||
'_bulk': ['bulkedititemvar'],
|
||||
'bulkedit-itemvar': f'{self.ticket.pk}',
|
||||
})
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert v.item == self.ticket
|
||||
|
||||
def test_quota_check_change_variation(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=2, redeemed=1)
|
||||
self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=3, redeemed=1)
|
||||
self._requires_one_more_quota({
|
||||
'_bulk': ['bulkedititemvar'],
|
||||
'bulkedit-itemvar': f'{self.shirt.pk}-{self.shirt_red.pk}',
|
||||
}, quota=self.quota_shirts)
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert v.item == self.shirt
|
||||
assert v.variation == self.shirt_red
|
||||
|
||||
def test_quota_check_change_item_with_variations(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=2, redeemed=1)
|
||||
self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=3, redeemed=1)
|
||||
self._requires_one_more_quota({
|
||||
'_bulk': ['bulkedititemvar'],
|
||||
'bulkedit-itemvar': f'{self.shirt.pk}',
|
||||
}, quota=self.quota_shirts)
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert v.item == self.shirt
|
||||
assert not v.variation
|
||||
|
||||
def test_quota_check_change_expired_to_valid(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=2)
|
||||
self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=1, valid_until=now() - datetime.timedelta(days=1))
|
||||
self._requires_one_more_quota({
|
||||
'_bulk': ['bulkeditvalid_until'],
|
||||
'bulkedit-valid_until_0': '',
|
||||
'bulkedit-valid_until_1': '',
|
||||
})
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert not v.valid_until
|
||||
|
||||
def test_quota_check_change_max_usages(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=2)
|
||||
self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=1, redeemed=1)
|
||||
self._requires_one_more_quota({
|
||||
'_bulk': ['bulkeditmax_usages'],
|
||||
'bulkedit-max_usages': '2',
|
||||
})
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert v.max_usages == 2
|
||||
|
||||
def test_quota_check_no_change(self):
|
||||
with scopes_disabled():
|
||||
# Technically overbooked, but we don't have a diff in quota
|
||||
self.event.vouchers.create(item=self.shirt, variation=self.shirt_red, block_quota=True)
|
||||
self.event.vouchers.create(item=self.shirt, variation=self.shirt_red, block_quota=True)
|
||||
self.event.vouchers.create(item=self.shirt, variation=self.shirt_red, block_quota=True)
|
||||
self._update_all({
|
||||
'_bulk': ['bulkedititemvar'],
|
||||
'bulkedit-itemvar': f'{self.shirt.pk}-{self.shirt_blue.pk}',
|
||||
})
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert v.variation == self.shirt_blue
|
||||
|
||||
def test_quota_check_change_subevent(self):
|
||||
with scopes_disabled():
|
||||
self.event.has_subevents = True
|
||||
self.event.save()
|
||||
se1 = self.event.subevents.create(name="Foo", date_from=now())
|
||||
se2 = self.event.subevents.create(name="Bar", date_from=now())
|
||||
self.quota_tickets.subevent = se1
|
||||
self.quota_tickets.save()
|
||||
Quota.objects.create(event=self.event, subevent=se2, name='Tickets', size=3)
|
||||
self.event.vouchers.create(item=self.ticket, block_quota=True, subevent=se2)
|
||||
self.event.vouchers.create(item=self.ticket, block_quota=True, subevent=se2)
|
||||
self.event.vouchers.create(item=self.ticket, block_quota=True, subevent=se2)
|
||||
self._requires_one_more_quota({
|
||||
'_bulk': ['bulkeditsubevent'],
|
||||
'bulkedit-subevent': f'{se1.pk}',
|
||||
})
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert v.subevent == se1
|
||||
|
||||
def test_change_subevent_quota_invalid(self):
|
||||
with scopes_disabled():
|
||||
self.event.has_subevents = True
|
||||
self.event.save()
|
||||
se1 = self.event.subevents.create(name="Foo", date_from=now())
|
||||
se2 = self.event.subevents.create(name="Bar", date_from=now())
|
||||
self.quota_tickets.subevent = se1
|
||||
self.quota_tickets.save()
|
||||
v1 = self.event.vouchers.create(quota=self.quota_tickets, block_quota=True, subevent=se1)
|
||||
self._update_all({
|
||||
'_bulk': ['bulkeditsubevent'],
|
||||
'bulkedit-subevent': f'{se2.pk}',
|
||||
}, expect_error="selected quota does not match the selected subevent")
|
||||
self._update_all({
|
||||
'_bulk': ['bulkeditsubevent'],
|
||||
'bulkedit-subevent': '',
|
||||
}, expect_error="has no date selected")
|
||||
v1.quota = None
|
||||
v1.item = self.ticket
|
||||
v1.save()
|
||||
self._update_all({
|
||||
'_bulk': ['bulkeditsubevent'],
|
||||
'bulkedit-subevent': '',
|
||||
}, expect_error="If you want this voucher to block quota, you need to select a specific date")
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert v.subevent == se1
|
||||
|
||||
def test_change_missing_itemvar_with_block_quota(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(quota=self.quota_tickets, block_quota=True)
|
||||
self.event.vouchers.create(quota=self.quota_tickets, block_quota=True)
|
||||
self._update_all({
|
||||
'_bulk': ['bulkedititemvar'],
|
||||
'bulkedit-itemvar': '',
|
||||
}, expect_error="You need to select a specific product or quota if this voucher should reserve")
|
||||
self._update_all({
|
||||
'_bulk': ['bulkedititemvar', 'bulkeditblock_quota'],
|
||||
'bulkedit-itemvar': '',
|
||||
'bulkedit-block_quota': '',
|
||||
})
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert not v.subevent
|
||||
assert not v.block_quota
|
||||
|
||||
def test_change_subevent_and_quota(self):
|
||||
with scopes_disabled():
|
||||
self.event.has_subevents = True
|
||||
self.event.save()
|
||||
se1 = self.event.subevents.create(name="Foo", date_from=now())
|
||||
se2 = self.event.subevents.create(name="Bar", date_from=now())
|
||||
self.quota_tickets.subevent = se1
|
||||
self.quota_tickets.save()
|
||||
q2 = Quota.objects.create(event=self.event, subevent=se2, name='Tickets', size=3)
|
||||
self.event.vouchers.create(quota=self.quota_tickets, block_quota=True, subevent=se1)
|
||||
self._update_all({
|
||||
'_bulk': ['bulkedititemvar', 'bulkeditsubevent'],
|
||||
'bulkedit-subevent': f'{se2.pk}',
|
||||
'bulkedit-itemvar': f'q-{q2.pk}',
|
||||
})
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert v.subevent == se2
|
||||
assert v.quota == q2
|
||||
|
||||
def test_quota_check_change_block_quota(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(item=self.ticket, max_usages=3)
|
||||
self._requires_one_more_quota({
|
||||
'_bulk': ['bulkeditblock_quota'],
|
||||
'bulkedit-block_quota': 'on',
|
||||
})
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert v.block_quota
|
||||
|
||||
def test_ignore_quota(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(item=self.ticket, max_usages=3)
|
||||
self._update_all({
|
||||
'_bulk': ['bulkeditblock_quota', 'bulkeditallow_ignore_quota'],
|
||||
'bulkedit-block_quota': 'on',
|
||||
'bulkedit-allow_ignore_quota': 'on',
|
||||
})
|
||||
with scopes_disabled():
|
||||
for v in self.event.vouchers.all():
|
||||
assert v.block_quota
|
||||
assert v.allow_ignore_quota
|
||||
|
||||
@scopes_disabled()
|
||||
def _create_seat(self, **kwargs):
|
||||
plan = SeatingPlan.objects.create(
|
||||
name="Plan", organizer=self.orga, layout="{}"
|
||||
)
|
||||
self.event.seating_plan = plan
|
||||
self.event.save()
|
||||
return self.event.seats.create(seat_number="A1", product=self.ticket, seat_guid="A1", **kwargs)
|
||||
|
||||
def test_seated_unsupported(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(item=self.ticket, max_usages=1, seat=self._create_seat())
|
||||
self._update_all({
|
||||
'_bulk': ['bulkeditmax_usages'],
|
||||
'bulkedit-max_usages': '2',
|
||||
}, expect_error="Changing the maximum number of usages in bulk is not supported")
|
||||
self._update_all({
|
||||
'_bulk': ['bulkeditsubevent'],
|
||||
'bulkedit-subevent': '',
|
||||
}, expect_error="Changing the date in bulk is not supported")
|
||||
self._update_all({
|
||||
'_bulk': ['bulkedititemvar'],
|
||||
'bulkedit-itemvar': f'q-{self.quota_tickets.pk}',
|
||||
}, expect_error="Changing the product to a quota is not supported")
|
||||
|
||||
def test_seat_changed_to_valid_needs_to_be_available(self):
|
||||
with scopes_disabled():
|
||||
seat = self._create_seat(blocked=True)
|
||||
self.event.vouchers.create(item=self.ticket, max_usages=1, valid_until=now() - datetime.timedelta(days=1), seat=seat)
|
||||
|
||||
self._update_all({
|
||||
'_bulk': ['bulkeditvalid_until'],
|
||||
'bulkedit-valid_until_0': '',
|
||||
'bulkedit-valid_until_1': '',
|
||||
}, expect_error="not all assigned seats of the vouchers are still available")
|
||||
|
||||
seat.blocked = False
|
||||
seat.save()
|
||||
self._update_all({
|
||||
'_bulk': ['bulkeditvalid_until'],
|
||||
'bulkedit-valid_until_0': '',
|
||||
'bulkedit-valid_until_1': '',
|
||||
})
|
||||
|
||||
def test_seat_changed_to_valid_needs_to_be_available_subevents(self):
|
||||
with scopes_disabled():
|
||||
self.event.has_subevents = True
|
||||
self.event.save()
|
||||
se1 = self.event.subevents.create(name="Foo", date_from=now())
|
||||
seat = self._create_seat(subevent=se1, blocked=True)
|
||||
self.event.vouchers.create(item=self.ticket, max_usages=1, valid_until=now() - datetime.timedelta(days=1), seat=seat, subevent=se1)
|
||||
|
||||
self._update_all({
|
||||
'_bulk': ['bulkeditvalid_until'],
|
||||
'bulkedit-valid_until_0': '',
|
||||
'bulkedit-valid_until_1': '',
|
||||
}, expect_error="not all assigned seats of the vouchers are still available")
|
||||
|
||||
seat.blocked = False
|
||||
seat.save()
|
||||
self._update_all({
|
||||
'_bulk': ['bulkeditvalid_until'],
|
||||
'bulkedit-valid_until_0': '',
|
||||
'bulkedit-valid_until_1': '',
|
||||
})
|
||||
|
||||
@@ -426,97 +426,6 @@ def test_sendmail_rule_checked_in_get_mail(event, order, item):
|
||||
assert len(djmail.outbox) == 1, "email not sent"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@scopes_disabled()
|
||||
def test_sendmail_rule_checked_in_mixed_order(event, order, item):
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
|
||||
order.all_positions.create(item=item, price=13, attendee_email='item2@dummy.test') # p2
|
||||
clist = event.checkin_lists.create(name="Default", all_products=True)
|
||||
|
||||
# receives no mail when checked in
|
||||
djmail.outbox = []
|
||||
perform_checkin(p1, clist, {})
|
||||
assert clist.checkin_count == 1
|
||||
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="checked_in",
|
||||
subject='meow', template='meow meow meow')
|
||||
sendmail_run_rules(None)
|
||||
assert len(djmail.outbox) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@scopes_disabled()
|
||||
def test_sendmail_rule_not_checked_in_mixed_order(event, order, item):
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
|
||||
order.all_positions.create(item=item, price=13, attendee_email='item2@dummy.test') # p2
|
||||
clist = event.checkin_lists.create(name="Default", all_products=True)
|
||||
|
||||
# receives no mail when checked in
|
||||
djmail.outbox = []
|
||||
perform_checkin(p1, clist, {})
|
||||
assert clist.checkin_count == 1
|
||||
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="no_checkin",
|
||||
subject='meow', template='meow meow meow')
|
||||
sendmail_run_rules(None)
|
||||
assert len(djmail.outbox) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@scopes_disabled()
|
||||
def test_sendmail_rule_not_checked_in_mixed_order_position_without_email_not_matching_status(event, order, item, item2):
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
|
||||
p2 = order.all_positions.create(item=item, price=13, attendee_email='item2@dummy.test')
|
||||
clist = event.checkin_lists.create(name="Default", all_products=True)
|
||||
|
||||
# receives no mail when checked in
|
||||
djmail.outbox = []
|
||||
perform_checkin(p1, clist, {})
|
||||
|
||||
# we have no email and we are checked in
|
||||
# we shouldn't trigger a fallback to order.email
|
||||
p3 = order.all_positions.create(item=item, price=13)
|
||||
perform_checkin(p3, clist, {})
|
||||
assert clist.checkin_count == 2
|
||||
|
||||
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="no_checkin",
|
||||
subject='meow', template='meow meow meow', send_to=Rule.ATTENDEES)
|
||||
sendmail_run_rules(None)
|
||||
|
||||
assert len(djmail.outbox) == 1
|
||||
recipients = [m.to for m in djmail.outbox]
|
||||
assert [p2.attendee_email] in recipients # for p2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@scopes_disabled()
|
||||
def test_sendmail_rule_not_checked_in_mixed_order_position_without_email(event, order, item, item2):
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
|
||||
p2 = order.all_positions.create(item=item, price=13, attendee_email='item2@dummy.test')
|
||||
order.all_positions.create(item=item, price=13) # p3
|
||||
order.all_positions.create(item=item, price=13) # p4
|
||||
clist = event.checkin_lists.create(name="Default", all_products=True)
|
||||
|
||||
# receives no mail when checked in
|
||||
djmail.outbox = []
|
||||
perform_checkin(p1, clist, {})
|
||||
assert clist.checkin_count == 1
|
||||
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="no_checkin",
|
||||
subject='meow', template='meow meow meow', send_to=Rule.ATTENDEES)
|
||||
sendmail_run_rules(None)
|
||||
|
||||
assert len(djmail.outbox) == 2
|
||||
recipients = [m.to for m in djmail.outbox]
|
||||
assert [order.email] in recipients # for p3 and p4
|
||||
assert [p2.attendee_email] in recipients # for p2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@scopes_disabled()
|
||||
def run_restriction_test(event, order, restrictions_pass=[], restrictions_fail=[]):
|
||||
|
||||
Reference in New Issue
Block a user