Allow to set multiple confirm texts (#1735)

This commit is contained in:
Felix Rindt
2020-08-04 10:20:55 +02:00
committed by GitHub
parent 896ba5b06b
commit 9b367cb28b
10 changed files with 216 additions and 64 deletions

View File

@@ -565,7 +565,7 @@ class EventSettingsSerializer(serializers.Serializer):
'attendee_addresses_required',
'attendee_company_asked',
'attendee_company_required',
'confirm_text',
'confirm_texts',
'order_email_asked_twice',
'payment_term_days',
'payment_term_last',

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.0.8 on 2020-07-31 10:05
import json
from django.db import migrations
def migrate_confirm_text(apps, schema_editor):
# We now allow creating multiple confirm texts so we migrate the setting for that
# from `confirm_text` to `confirm_texts`
Event_SettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
for store in Event_SettingsStore.objects.filter(key="confirm_text"):
values = json.dumps([json.loads(store.value)]) # convert single value to one-element list
store.key = "confirm_texts"
store.value = values
store.save()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0159_mails_by_sales_channel'),
]
operations = [
migrations.RunPython(migrate_confirm_text, migrations.RunPython.noop),
]

View File

@@ -35,7 +35,7 @@ class RelativeDateWrapper:
def __init__(self, data: Union[datetime.datetime, RelativeDate]):
self.data = data
def date(self, event) -> datetime.datetime:
def date(self, event) -> datetime.date:
from .models import SubEvent
if isinstance(self.data, datetime.date):
@@ -81,8 +81,8 @@ class RelativeDateWrapper:
second=self.data.time.second
)
new_date = new_date.astimezone(tz)
newoffset = new_date.utcoffset()
new_date += oldoffset - newoffset
new_offset = new_date.utcoffset()
new_date += oldoffset - new_offset
return new_date
def to_string(self) -> str:

View File

@@ -27,7 +27,7 @@ from pretix.base.services.locking import LockTimeoutException, NoLockManager
from pretix.base.services.pricing import get_price
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.services.tasks import ProfiledEventTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.settings import PERSON_NAME_SCHEMES, LazyI18nStringList
from pretix.base.signals import validate_cart_addons
from pretix.base.templatetags.rich_text import rich_text
from pretix.celery_app import app
@@ -1240,9 +1240,7 @@ def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, l
@receiver(checkout_confirm_messages, dispatch_uid="cart_confirm_messages")
def confirm_messages(sender, *args, **kwargs):
if not sender.settings.confirm_text:
if not sender.settings.confirm_texts:
return {}
return {
'confirm_text': rich_text(str(sender.settings.confirm_text))
}
confirm_texts = sender.settings.get("confirm_texts", as_type=LazyI18nStringList)
return {'confirm_text_%i' % index: rich_text(str(text)) for index, text in enumerate(confirm_texts)}

View File

@@ -1,5 +1,6 @@
import json
from collections import OrderedDict
import operator
from collections import OrderedDict, UserList
from datetime import datetime
from decimal import Decimal
from typing import Any
@@ -37,6 +38,20 @@ def country_choice_kwargs():
}
class LazyI18nStringList(UserList):
def __init__(self, init_list=None):
super().__init__()
if init_list is not None:
self.data = [v if isinstance(v, LazyI18nString) else LazyI18nString(v) for v in init_list]
def serialize(self):
return json.dumps([s.data for s in self.data])
@classmethod
def unserialize(cls, s):
return cls(json.loads(s))
DEFAULTS = {
'max_items_per_order': {
'default': '10',
@@ -1127,18 +1142,11 @@ DEFAULTS = {
),
'serializer_class': serializers.URLField,
},
'confirm_text': {
'default': None,
'type': LazyI18nString,
'form_class': I18nFormField,
'serializer_class': I18nField,
'form_kwargs': dict(
label=_('Confirmation text'),
help_text=_('This text needs to be confirmed by the user before a purchase is possible. You could for example '
'link your terms of service here. If you use the Pages feature to publish your terms of service, '
'you don\'t need this setting since you can configure it there.'),
widget=I18nTextarea,
)
'confirm_texts': {
'default': LazyI18nStringList(),
'type': LazyI18nStringList,
'serializer_class': serializers.ListField,
'serializer_kwargs': lambda: dict(child=I18nField()),
},
'mail_html_renderer': {
'default': 'classic',
@@ -1939,6 +1947,9 @@ def i18n_uns(v):
settings_hierarkey.add_type(LazyI18nString,
serialize=lambda s: json.dumps(s.data),
unserialize=i18n_uns)
settings_hierarkey.add_type(LazyI18nStringList,
serialize=operator.methodcaller("serialize"),
unserialize=LazyI18nStringList.unserialize)
settings_hierarkey.add_type(RelativeDateWrapper,
serialize=lambda rdw: rdw.to_string(),
unserialize=lambda s: RelativeDateWrapper.from_string(s))

View File

@@ -518,7 +518,6 @@ class EventSettingsForm(SettingsForm):
'attendee_company_required',
'attendee_addresses_asked',
'attendee_addresses_required',
'confirm_text',
'banner_text',
'banner_text_bottom',
'order_email_asked_twice',
@@ -535,11 +534,6 @@ class EventSettingsForm(SettingsForm):
def __init__(self, *args, **kwargs):
self.event = kwargs['obj']
super().__init__(*args, **kwargs)
self.fields['confirm_text'].widget.attrs['rows'] = '3'
self.fields['confirm_text'].widget.attrs['placeholder'] = _(
'e.g. I hereby confirm that I have read and agree with the event organizer\'s terms of service '
'and agree with them.'
)
self.fields['name_scheme'].choices = (
(k, _('Ask for {fields}, display like {example}').format(
fields=' + '.join(str(vv[1]) for vv in v['fields']),
@@ -1342,3 +1336,25 @@ class ItemMetaPropertyForm(forms.ModelForm):
widgets = {
'default': forms.TextInput()
}
class ConfirmTextForm(I18nForm):
text = I18nFormField(
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
)
class BaseConfirmTextFormSet(I18nFormSetMixin, forms.BaseFormSet):
def __init__(self, *args, **kwargs):
event = kwargs.pop('event', None)
if event:
kwargs['locales'] = event.settings.get('locales')
super().__init__(*args, **kwargs)
ConfirmTextFormset = formset_factory(
ConfirmTextForm,
formset=BaseConfirmTextFormSet,
can_order=True, can_delete=True, extra=0
)

View File

@@ -26,7 +26,11 @@
{% bootstrap_field form.date_to layout="control" %}
<div class="geodata-section">
{% bootstrap_field form.location layout="control" %}
<div class="form-group geodata-group" data-tiles="{{ global_settings.leaflet_tiles|default_if_none:"" }}" data-attrib="{{ global_settings.leaflet_tiles_attribution }}" data-icon="{% static "leaflet/images/marker-icon.png" %}" data-shadow="{% static "leaflet/images/marker-shadow.png" %}">
<div class="form-group geodata-group"
data-tiles="{{ global_settings.leaflet_tiles|default_if_none:"" }}"
data-attrib="{{ global_settings.leaflet_tiles_attribution }}"
data-icon="{% static "leaflet/images/marker-icon.png" %}"
data-shadow="{% static "leaflet/images/marker-shadow.png" %}">
<label class="col-md-3 control-label">
{% trans "Geo coordinates" %}<br>
<span class="optional">{% trans "Optional" %}</span>
@@ -99,7 +103,77 @@
{% bootstrap_field sform.frontpage_text layout="control" %}
{% bootstrap_field sform.presale_has_ended_text layout="control" %}
{% bootstrap_field sform.voucher_explanation_text layout="control" %}
{% bootstrap_field sform.confirm_text layout="control" %}
<div class="form-group">
<label class="col-md-3 control-label">
{% trans "Confirmation text" %}<br>
<span class="optional">{% trans "Optional" %}</span>
</label>
<div class="col-md-9">
<div class="help-block">
{% blocktrans trimmed %}
These texts need to be confirmed by the user before a purchase is possible. You could
for example link your terms of service here. If you use the Pages feature to publish
your terms of service, you don't need this setting since you can configure it there.
{% endblocktrans %}
</div>
<div class="formset" data-formset data-formset-prefix="{{ confirm_texts_formset.prefix }}">
{{ confirm_texts_formset.management_form }}
{% bootstrap_formset_errors confirm_texts_formset %}
<div data-formset-body>
{% for form in confirm_texts_formset %}
<div class="row formset-row" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
</div>
<div class="col-md-10">
{% bootstrap_form_errors form %}
{% bootstrap_field form.text layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn" data-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn" data-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="row formset-row" data-formset-form>
<div class="sr-only">
{{ confirm_texts_formset.empty_form.id }}
{% bootstrap_field confirm_texts_formset.empty_form.DELETE form_group_class="" layout="inline" %}
{% bootstrap_field confirm_texts_formset.empty_form.ORDER form_group_class="" layout="inline" %}
</div>
<div class="col-md-10">
{% bootstrap_field confirm_texts_formset.empty_form.text layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn" data-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn" data-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add confirmation text" %}</button>
</p>
</div>
</div>
</div>
{% bootstrap_field sform.checkout_email_helptext layout="control" %}
{% bootstrap_field sform.banner_text layout="control" %}
{% bootstrap_field sform.banner_text_bottom layout="control" %}
@@ -161,17 +235,18 @@
<legend>{% trans "Item metadata" %}</legend>
<p>
{% blocktrans trimmed %}
You can here define a set of metadata properties (i.e. variables) that you can later set for your
items and re-use in places like ticket layouts. This is an useful timesaver if you create lots and
lots of items.
You can here define a set of metadata properties (i.e. variables) that you can later set for
your items and re-use in places like ticket layouts. This is an useful timesaver if you create
lots and lots of items.
{% endblocktrans %}
</p>
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div class="formset" data-formset
data-formset-prefix="{{ item_meta_property_formset.prefix }}">
{{ item_meta_property_formset.management_form }}
{% bootstrap_formset_errors item_meta_property_formset %}
<div data-formset-body>
{% for form in formset %}
<div class="row" data-formset-form>
{% for form in item_meta_property_formset %}
<div class="row formset-row" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
@@ -192,16 +267,16 @@
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="row" data-formset-form>
<div class="row formset-row" data-formset-form>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
{{ item_meta_property_formset.empty_form.id }}
{% bootstrap_field item_meta_property_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-5">
{% bootstrap_field formset.empty_form.name layout='inline' form_group_class="" %}
{% bootstrap_field item_meta_property_formset.empty_form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-5 col-lg-6">
{% bootstrap_field formset.empty_form.default layout='inline' form_group_class="" %}
{% bootstrap_field item_meta_property_formset.empty_form.default layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 col-lg-1 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
@@ -223,12 +298,12 @@
</button>
<div class="pull-left">
<a href="{% url "control:event.dangerzone" organizer=request.organizer.slug event=request.event.slug %}"
class="btn btn-danger btn-lg">
class="btn btn-danger btn-lg">
<span class="fa fa-trash"></span>
{% trans "Cancel or delete event" %}
</a>
<a href="{% url "control:events.add" %}?clone={{ request.event.pk }}"
class="btn btn-default btn-lg">
class="btn btn-default btn-lg">
<span class="fa fa-copy"></span>
{% trans "Clone event" %}
</a>

View File

@@ -1,4 +1,5 @@
import json
import operator
import re
from collections import OrderedDict
from decimal import Decimal
@@ -40,10 +41,11 @@ from pretix.base.services.invoices import build_preview_invoice_pdf
from pretix.base.signals import register_ticket_outputs
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.control.forms.event import (
CancelSettingsForm, CommentForm, EventDeleteForm, EventMetaValueForm,
EventSettingsForm, EventUpdateForm, InvoiceSettingsForm,
ItemMetaPropertyForm, MailSettingsForm, PaymentSettingsForm, ProviderForm,
QuickSetupForm, QuickSetupProductFormSet, TaxRuleForm, TaxRuleLineFormSet,
CancelSettingsForm, CommentForm, ConfirmTextFormset, EventDeleteForm,
EventMetaValueForm, EventSettingsForm, EventUpdateForm,
InvoiceSettingsForm, ItemMetaPropertyForm, MailSettingsForm,
PaymentSettingsForm, ProviderForm, QuickSetupForm,
QuickSetupProductFormSet, TaxRuleForm, TaxRuleLineFormSet,
TicketSettingsForm, WidgetCodeForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
@@ -54,6 +56,7 @@ from pretix.plugins.stripe.payment import StripeSettingsHolder
from pretix.presale.style import regenerate_css
from ...base.models.items import ItemMetaProperty
from ...base.settings import LazyI18nStringList
from ..logdisplay import OVERVIEW_BANLIST
from . import CreateView, PaginationMixin, UpdateView
@@ -140,7 +143,8 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
context = super().get_context_data(*args, **kwargs)
context['sform'] = self.sform
context['meta_forms'] = self.meta_forms
context['formset'] = self.formset
context['item_meta_property_formset'] = self.item_meta_property_formset
context['confirm_texts_formset'] = self.confirm_texts_formset
return context
@transaction.atomic
@@ -148,13 +152,15 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
self._save_decoupled(self.sform)
self.sform.save()
self.save_meta()
self.save_formset(self.object)
self.save_item_meta_property_formset(self.object)
self.save_confirm_texts_formset(self.object)
change_css = False
if self.sform.has_changed():
self.request.event.log_action('pretix.event.settings', user=self.request.user, data={
k: self.request.event.settings.get(k) for k in self.sform.changed_data
})
if self.sform.has_changed() or self.confirm_texts_formset.has_changed():
data = {k: self.request.event.settings.get(k) for k in self.sform.changed_data}
if self.confirm_texts_formset.has_changed():
data.update(confirm_texts=self.confirm_texts_formset.cleaned_data)
self.request.event.log_action('pretix.event.settings', user=self.request.user, data=data)
display_properties = (
'primary_color', 'theme_color_success', 'theme_color_danger', 'primary_font',
'theme_color_background', 'theme_round_borders',
@@ -194,7 +200,7 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid() and self.sform.is_valid() and all([f.is_valid() for f in self.meta_forms]) and \
self.formset.is_valid():
self.item_meta_property_formset.is_valid() and self.confirm_texts_formset.is_valid():
# reset timezone
zone = timezone(self.sform.cleaned_data['timezone'])
event = form.instance
@@ -212,17 +218,17 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
return tz.localize(dt.replace(tzinfo=None)) if dt is not None else None
@cached_property
def formset(self):
def item_meta_property_formset(self):
formsetclass = inlineformset_factory(
Event, ItemMetaProperty,
form=ItemMetaPropertyForm, can_order=False, can_delete=True, extra=0
)
return formsetclass(self.request.POST if self.request.method == "POST" else None,
return formsetclass(self.request.POST if self.request.method == "POST" else None, prefix="item-meta-property",
instance=self.object, queryset=self.object.item_meta_properties.all())
def save_formset(self, obj):
for form in self.formset.initial_forms:
if form in self.formset.deleted_forms:
def save_item_meta_property_formset(self, obj):
for form in self.item_meta_property_formset.initial_forms:
if form in self.item_meta_property_formset.deleted_forms:
if not form.instance.pk:
continue
form.instance.delete()
@@ -230,14 +236,28 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
elif form.has_changed():
form.save()
for form in self.formset.extra_forms:
for form in self.item_meta_property_formset.extra_forms:
if not form.has_changed():
continue
if self.formset._should_delete_form(form):
if self.item_meta_property_formset._should_delete_form(form):
continue
form.instance.event = obj
form.save()
@cached_property
def confirm_texts_formset(self):
initial = [{"text": text, "ORDER": order} for order, text in
enumerate(self.object.settings.get("confirm_texts", as_type=LazyI18nStringList))]
return ConfirmTextFormset(self.request.POST if self.request.method == "POST" else None, event=self.object,
prefix="confirm-texts", initial=initial)
def save_confirm_texts_formset(self, obj):
obj.settings.confirm_texts = LazyI18nStringList(
form_data['text'].data
for form_data in sorted(self.confirm_texts_formset.cleaned_data, key=operator.itemgetter("ORDER"))
if not form_data.get("DELETE", False)
)
class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
model = Event

View File

@@ -54,6 +54,10 @@ div[data-formset-body], div[data-formset-form], div[data-nested-formset-form], d
margin: 0;
}
.formset-row {
margin-bottom: 10px;
}
.submit-group {
margin: 15px 0 0 0 !important;
padding: 15px;

View File

@@ -11,6 +11,7 @@ from pretix.base.models import Event, Organizer, Team, User
class MailSettingPreviewTest(SoupTest):
@scopes_disabled()
def setUp(self):
super().setUp()
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
self.orga1 = Organizer.objects.create(name='CCC', slug='ccc')
self.orga2 = Organizer.objects.create(name='MRM', slug='mrm')