mirror of
https://github.com/pretix/pretix.git
synced 2026-07-12 06:21:53 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e86500e9fc | |||
| a72e32b82d | |||
| 2e2fe874e1 | |||
| ce8c4c649d | |||
| 430c6dd269 | |||
| 6411648457 | |||
| 005b3864b1 | |||
| 1ee1c604cf | |||
| e425105dbf | |||
| 738b375042 | |||
| d09572d999 | |||
| 634754aacb | |||
| 14c3baa2aa |
@@ -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_17.x | sudo -E bash -
|
||||
curl -sL https://deb.nodesource.com/setup_24.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::
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# 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
@@ -0,0 +1,46 @@
|
||||
# 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,15 +1403,12 @@ 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(
|
||||
('<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
|
||||
)
|
||||
)
|
||||
)
|
||||
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',
|
||||
))
|
||||
|
||||
responses = event_live_issues.send(self)
|
||||
for receiver, response in sorted(responses, key=lambda r: str(r[0])):
|
||||
|
||||
@@ -224,8 +224,6 @@ class Order(LockModel, LoggedModel):
|
||||
"Organizer",
|
||||
related_name="orders",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
@@ -329,7 +327,7 @@ class Order(LockModel, LoggedModel):
|
||||
default="line",
|
||||
)
|
||||
|
||||
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='event__organizer')
|
||||
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='organizer')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Order")
|
||||
@@ -2541,8 +2539,6 @@ class OrderPosition(AbstractPosition):
|
||||
"Organizer",
|
||||
related_name="order_positions",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
order = models.ForeignKey(
|
||||
Order,
|
||||
@@ -2599,7 +2595,7 @@ class OrderPosition(AbstractPosition):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
all = ScopedManager(organizer='order__event__organizer')
|
||||
all = ScopedManager(organizer='organizer')
|
||||
objects = ActivePositionManager()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -936,7 +936,7 @@ class BasePaymentProvider:
|
||||
"""
|
||||
Will be called if the *event administrator* views the details of a payment.
|
||||
|
||||
It should return HTML code containing information regarding the current payment
|
||||
It should return a SafeString containing HTML code, with information regarding the current payment
|
||||
status and, if applicable, next steps.
|
||||
|
||||
The default implementation returns an empty string.
|
||||
@@ -961,7 +961,7 @@ class BasePaymentProvider:
|
||||
"""
|
||||
Will be called if the *event administrator* views the details of a refund.
|
||||
|
||||
It should return HTML code containing information regarding the current refund
|
||||
It should return a SafeString containing HTML code, with information regarding the current refund
|
||||
status and, if applicable, next steps.
|
||||
|
||||
The default implementation returns an empty string.
|
||||
|
||||
@@ -535,8 +535,9 @@ 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 a string that will be displayed to the user
|
||||
as the error message. If you don't, your receiver should return ``None``.
|
||||
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``.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
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
|
||||
@@ -44,7 +45,7 @@ def eventsignal(event: Event, signame: str, **kwargs):
|
||||
_html = []
|
||||
for receiver, response in signal.send(event, **kwargs):
|
||||
if response:
|
||||
_html.append(response)
|
||||
_html.append(conditional_escape(response))
|
||||
return mark_safe("".join(_html))
|
||||
|
||||
|
||||
@@ -63,5 +64,5 @@ def signal(signame: str, request, **kwargs):
|
||||
_html = []
|
||||
for receiver, response in signal.send(request, **kwargs):
|
||||
if response:
|
||||
_html.append(response)
|
||||
_html.append(conditional_escape(response))
|
||||
return mark_safe("".join(_html))
|
||||
|
||||
@@ -1905,12 +1905,6 @@ 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,
|
||||
|
||||
@@ -39,7 +39,8 @@ 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 HTML.
|
||||
page in the backend. You are expected to return a SafeString containing HTML, or
|
||||
a string that will be HTML-escaped.
|
||||
|
||||
The ``sender`` keyword argument will contain the request.
|
||||
"""
|
||||
@@ -129,7 +130,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 HTML.
|
||||
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.
|
||||
An additional keyword argument ``subevent`` *can* contain a sub-event.
|
||||
@@ -172,6 +173,7 @@ 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.
|
||||
"""
|
||||
@@ -209,6 +211,7 @@ 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.
|
||||
"""
|
||||
@@ -219,6 +222,7 @@ 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.
|
||||
"""
|
||||
@@ -265,7 +269,8 @@ order_info = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``order``, ``request``
|
||||
|
||||
This signal is sent out to display additional information on the order detail page
|
||||
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.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
Additionally, the argument ``order`` and ``request`` are available.
|
||||
@@ -275,7 +280,8 @@ order_approve_info = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``order``, ``request``
|
||||
|
||||
This signal is sent out to display additional information on the order approve page
|
||||
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.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
Additionally, the argument ``order`` and ``request`` are available.
|
||||
@@ -286,6 +292,7 @@ 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.
|
||||
@@ -315,6 +322,7 @@ 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.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load compress %}
|
||||
{% load escapejson %}
|
||||
{% block content %}
|
||||
<form class="form-signin" action="" method="post" id="webauthn-form">
|
||||
{% csrf_token %}
|
||||
@@ -30,8 +31,7 @@
|
||||
</form>
|
||||
{% if jsondata %}
|
||||
<script type="text/json" id="webauthn-login">
|
||||
{{ jsondata|safe }}
|
||||
|
||||
{{ jsondata|escapejson }}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% compress js %}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</p>
|
||||
<ul>
|
||||
{% for issue in issues %}
|
||||
<li>{{ issue|safe }}</li>
|
||||
<li>{{ issue }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@
|
||||
</p>
|
||||
<ul>
|
||||
{% for issue in issues %}
|
||||
<li>{{ issue|safe }}</li>
|
||||
<li>{{ issue }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -193,7 +193,6 @@
|
||||
{% 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>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% load wrap_in %}
|
||||
{% block title %}
|
||||
{% trans "Cancel order" %}
|
||||
{% endblock %}
|
||||
@@ -26,7 +27,7 @@
|
||||
{% if form.cancellation_fee %}
|
||||
{% if fee %}
|
||||
{% with fee|money:request.event.currency as f %}
|
||||
<p>{% blocktrans trimmed with fee="<strong>"|add:f|add:"</strong>"|safe %}
|
||||
<p>{% blocktrans trimmed with fee=f|wrap_in:"strong" %}
|
||||
The configured cancellation fee for a self-service cancellation would be {{ fee }} for this
|
||||
order, but for a cancellation performed by you, you need to set the cancellation fee here:
|
||||
{% endblocktrans %}</p>
|
||||
|
||||
@@ -903,7 +903,7 @@
|
||||
<tr>
|
||||
<td colspan="1"></td>
|
||||
<td colspan="5">
|
||||
{{ p.html_info|safe }}
|
||||
{{ p.html_info }}
|
||||
{% if staff_session %}
|
||||
<p>
|
||||
<a href="" class="btn btn-default btn-xs admin-only" data-expandpayment data-id="{{ p.pk }}">
|
||||
@@ -1018,7 +1018,7 @@
|
||||
</dl>
|
||||
{% endif %}
|
||||
{% if r.html_info %}
|
||||
{{ r.html_info|safe }}
|
||||
{{ r.html_info }}
|
||||
{% endif %}
|
||||
{% if staff_session %}
|
||||
<p>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load escapejson %}
|
||||
{% block inner %}
|
||||
<h1>{% trans "Connect to device:" %} {{ device.name }}</h1>
|
||||
|
||||
@@ -18,7 +19,7 @@
|
||||
{% trans "Open the app that you want to connect and optionally reset it to the original state." %}
|
||||
</li>
|
||||
<li>{% trans "Scan the following configuration code:" %}<br><br>
|
||||
<script type="text/json" data-replace-with-qr>{{ qrdata|safe }}</script><br>
|
||||
<script type="application/json" data-replace-with-qr>{{ qrdata|escapejson_dumps }}</script><br>
|
||||
{% trans "If your app/device does not support scanning a QR code, you can also enter the following information:" %}
|
||||
<br>
|
||||
<strong>{% trans "System URL:" %}</strong> <code id="system_url">{{ settings.SITE_URL }}</code>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load escapejson %}
|
||||
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
||||
@@ -32,7 +33,7 @@
|
||||
</li>
|
||||
<li>
|
||||
{% trans "Add a new account to the app by scanning the following barcode:" %}
|
||||
<div class="qrcode-canvas" data-qrdata="#qrdata"></div>
|
||||
<script type="application/json" data-replace-with-qr>{{ qrdata|escapejson_dumps }}</script>
|
||||
<p>
|
||||
<a data-toggle="collapse" href="#no_scan">
|
||||
{% trans "Can't scan the barcode?" %}
|
||||
@@ -81,9 +82,4 @@
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<script type="text/json" id="qrdata">
|
||||
{{ qrdata|safe }}
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
{% load compress %}
|
||||
{% load escapejson %}
|
||||
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
||||
@@ -26,9 +27,7 @@
|
||||
{% trans "Device registration failed." %}
|
||||
</div>
|
||||
<script type="text/json" id="webauthn-enroll">
|
||||
{{ jsondata|safe }}
|
||||
|
||||
|
||||
{{ jsondata|escapejson }}
|
||||
</script>
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{% load bootstrap3 %}
|
||||
{% load compress %}
|
||||
{% load static %}
|
||||
{% load escapejson %}
|
||||
{% block content %}
|
||||
<form class="form-signin" id="webauthn-form" action="" method="post">
|
||||
{% csrf_token %}
|
||||
@@ -43,7 +44,7 @@
|
||||
|
||||
{% if jsondata %}
|
||||
<script type="text/json" id="webauthn-login">
|
||||
{{ jsondata|safe }}
|
||||
{{ jsondata|escapejson }}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% compress js %}
|
||||
|
||||
@@ -49,11 +49,11 @@
|
||||
<td>
|
||||
<strong>
|
||||
{% if t.tag %}
|
||||
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?tag={{ '"'|add:t.tag|add:'"'|urlencode }}">
|
||||
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?filter-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 %}?tag={{ '<>'|urlencode }}">
|
||||
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?filter-tag={{ '<>'|urlencode }}">
|
||||
{% trans "Empty tag" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -1428,11 +1428,16 @@ 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():
|
||||
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
|
||||
]
|
||||
self.object.log_action(
|
||||
'pretix.event.taxrule.changed', user=self.request.user, data={
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
'pretix.event.taxrule.changed', user=self.request.user, data=change_data
|
||||
)
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
@@ -551,10 +551,10 @@ class OrderDetail(OrderView):
|
||||
ctx['refunds'] = self.order.refunds.select_related('payment').order_by('-created')
|
||||
for p in ctx['payments']:
|
||||
if p.payment_provider:
|
||||
p.html_info = (p.payment_provider.payment_control_render(self.request, p) or "").strip()
|
||||
p.html_info = p.payment_provider.payment_control_render(self.request, p) or ""
|
||||
for r in ctx['refunds']:
|
||||
if r.payment_provider:
|
||||
r.html_info = (r.payment_provider.refund_control_render(self.request, r) or "").strip()
|
||||
r.html_info = r.payment_provider.refund_control_render(self.request, r) or ""
|
||||
ctx['invoices'] = list(self.order.invoices.all().select_related('event'))
|
||||
ctx['comment_form'] = CommentForm(initial={
|
||||
'comment': self.order.comment,
|
||||
|
||||
@@ -47,5 +47,5 @@ def escapejson(value):
|
||||
|
||||
@keep_lazy(str, SafeText)
|
||||
def escapejson_attr(value):
|
||||
"""Hex encodes characters for use in a html attributw script."""
|
||||
"""Hex encodes characters for use in a html attribute."""
|
||||
return mark_safe(force_str(value).translate(_json_escapes_attr))
|
||||
|
||||
@@ -162,6 +162,8 @@ 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())
|
||||
@@ -191,28 +193,29 @@ class ScheduledMail(models.Model):
|
||||
positions = [p for p in positions if p.subevent_id == self.subevent_id]
|
||||
|
||||
for p in positions:
|
||||
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.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
|
||||
|
||||
self.last_successful_order_id = o.pk
|
||||
|
||||
@@ -270,7 +273,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'))
|
||||
|
||||
|
||||
@@ -323,7 +323,7 @@ $(function () {
|
||||
}
|
||||
}
|
||||
} else if ($("#stripe_payment_intent_next_action_redirect_url").length) {
|
||||
let payment_intent_next_action_redirect_url = $.trim($("#stripe_payment_intent_next_action_redirect_url").html());
|
||||
let payment_intent_next_action_redirect_url = JSON.parse($("#stripe_payment_intent_next_action_redirect_url").html());
|
||||
pretixstripe.handlePaymentRedirectAction(payment_intent_next_action_redirect_url);
|
||||
} else if ($.trim($("#stripe_payment_intent_action_type").html()) === "promptpay_display_qr_code") {
|
||||
waitingDialog.hide();
|
||||
@@ -432,4 +432,4 @@ $(function () {
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<script type="text/plain" id="stripe_payment_intent_action_type">{{ payment_intent_action_type }}</script>
|
||||
<script type="text/plain" id="stripe_payment_intent_client_secret">{{ payment_intent_client_secret }}</script>
|
||||
{% if payment_intent_next_action_redirect_url %}
|
||||
<script type="text/plain" id="stripe_payment_intent_next_action_redirect_url">{{ payment_intent_next_action_redirect_url|safe }}</script>
|
||||
{{ payment_intent_next_action_redirect_url|json_script:"stripe_payment_intent_next_action_redirect_url" }}
|
||||
{% endif %}
|
||||
{% if payment_intent_redirect_action_handling %}
|
||||
<script type="text/plain" id="stripe_payment_intent_redirect_action_handling">{{ payment_intent_redirect_action_handling }}</script>
|
||||
|
||||
@@ -161,7 +161,8 @@ voucher_redeem_info = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``voucher``
|
||||
|
||||
This signal is sent out to display additional information on the "redeem a voucher" page
|
||||
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.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
@@ -194,6 +195,7 @@ 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.
|
||||
@@ -276,7 +278,8 @@ order_info = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``order``, ``request``
|
||||
|
||||
This signal is sent out to display additional information on the order detail page
|
||||
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.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
@@ -285,7 +288,8 @@ position_info = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``order``, ``position``, ``request``
|
||||
|
||||
This signal is sent out to display additional information on the position detail page
|
||||
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.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
@@ -294,7 +298,8 @@ order_info_top = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``order``, ``request``
|
||||
|
||||
This signal is sent out to display additional information on top of the order detail page
|
||||
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.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
@@ -303,7 +308,8 @@ position_info_top = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``order``, ``position``, ``request``
|
||||
|
||||
This signal is sent out to display additional information on top of the position detail page
|
||||
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.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
@@ -349,7 +355,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 HTML.
|
||||
receivers are expected to return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
"""
|
||||
|
||||
render_seating_plan = EventPluginSignal()
|
||||
@@ -361,7 +367,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 HTML.
|
||||
receivers are expected to return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
"""
|
||||
|
||||
front_page_bottom = EventPluginSignal()
|
||||
@@ -372,7 +378,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 HTML.
|
||||
receivers are expected to return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
"""
|
||||
|
||||
front_page_bottom_widget = EventPluginSignal()
|
||||
@@ -383,7 +389,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 HTML.
|
||||
receivers are expected to return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||
"""
|
||||
|
||||
checkout_all_optional = EventPluginSignal()
|
||||
@@ -403,7 +409,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 HTML.
|
||||
expected to return markdown.
|
||||
"""
|
||||
|
||||
register_cookie_providers = EventPluginSignal()
|
||||
|
||||
@@ -874,14 +874,6 @@ function setup_basics(el) {
|
||||
});
|
||||
});
|
||||
|
||||
el.find(".qrcode-canvas").each(function () {
|
||||
$(this).qrcode(
|
||||
{
|
||||
text: $.trim($($(this).attr("data-qrdata")).html())
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
el.find(".propagated-settings-box").find("input, textarea, select").not("[readonly]")
|
||||
.attr("data-propagated-locked", "true").prop("readonly", true);
|
||||
|
||||
|
||||
@@ -426,6 +426,97 @@ 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