forked from CGM_Public/pretix_original
* Add markdown text input widgets * Update src/pretix/static/pretixcontrol/scss/_forms.scss * Improvements --------- Co-authored-by: Raphael Michel <michel@rami.io>
430 lines
19 KiB
Python
430 lines
19 KiB
Python
#
|
|
# This file is part of pretix (Community Edition).
|
|
#
|
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
#
|
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
# this file, see <https://pretix.eu/about/en/license>.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
# <https://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
|
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
|
#
|
|
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
|
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
|
#
|
|
# This file contains Apache-licensed contributions copyrighted by: Alexey Kislitsin, Daniel, Flavia Bastos, Sanket
|
|
# Dasgupta, Sohalt, pajowu
|
|
#
|
|
# 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.
|
|
|
|
from django import forms
|
|
from django.conf import settings
|
|
from django.core.exceptions import ValidationError
|
|
from django.urls import reverse
|
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
|
from django_scopes.forms import SafeModelMultipleChoiceField
|
|
from i18nfield.forms import I18nFormField, I18nTextInput
|
|
|
|
from pretix.base.forms import I18nMarkdownTextarea, I18nModelForm
|
|
from pretix.base.forms.widgets import (
|
|
SplitDateTimePickerWidget, TimePickerWidget,
|
|
)
|
|
from pretix.base.models import CheckinList, Item, Order, SubEvent
|
|
from pretix.control.forms import CachedFileField, SplitDateTimeField
|
|
from pretix.control.forms.widgets import Select2, Select2Multiple
|
|
from pretix.plugins.sendmail.models import Rule
|
|
|
|
from pretix.base.services.placeholders import FormPlaceholderMixin # noqa
|
|
|
|
|
|
class BaseMailForm(FormPlaceholderMixin, forms.Form):
|
|
subject = forms.CharField(label=_("Subject"))
|
|
message = forms.CharField(label=_("Message"))
|
|
attachment = CachedFileField(
|
|
label=_("Attachment"),
|
|
required=False,
|
|
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT,
|
|
help_text=_('Sending an attachment increases the chance of your email not arriving or being sorted into spam folders. We recommend only using PDFs '
|
|
'of no more than 2 MB in size.'),
|
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
event = self.event = kwargs.pop('event')
|
|
context_parameters = kwargs.pop('context_parameters')
|
|
super().__init__(*args, **kwargs)
|
|
self.fields['subject'] = I18nFormField(
|
|
label=_('Subject'),
|
|
widget=I18nTextInput, required=True,
|
|
locales=event.settings.get('locales'),
|
|
)
|
|
self.fields['message'] = I18nFormField(
|
|
label=_('Message'),
|
|
widget=I18nMarkdownTextarea, required=True,
|
|
locales=event.settings.get('locales'),
|
|
)
|
|
self._set_field_placeholders('subject', context_parameters)
|
|
self._set_field_placeholders('message', context_parameters)
|
|
|
|
|
|
class WaitinglistMailForm(BaseMailForm):
|
|
items = forms.ModelMultipleChoiceField(
|
|
widget=forms.CheckboxSelectMultiple(
|
|
attrs={'class': 'scrolling-multiple-choice'}
|
|
),
|
|
label=pgettext_lazy('sendmail_form', 'Waiting for'),
|
|
required=True,
|
|
queryset=Item.objects.none()
|
|
)
|
|
subevent = forms.ModelChoiceField(
|
|
SubEvent.objects.none(),
|
|
label=pgettext_lazy('sendmail_form', 'Restrict to a specific event date'),
|
|
required=False,
|
|
empty_label=pgettext_lazy('subevent', 'All dates')
|
|
)
|
|
subevents_from = forms.SplitDateTimeField(
|
|
widget=SplitDateTimePickerWidget(),
|
|
label=pgettext_lazy('sendmail_form', 'Restrict to event dates starting at or after'),
|
|
required=False,
|
|
)
|
|
subevents_to = forms.SplitDateTimeField(
|
|
widget=SplitDateTimePickerWidget(),
|
|
label=pgettext_lazy('sendmail_form', 'Restrict to event dates starting before'),
|
|
required=False,
|
|
)
|
|
|
|
def clean(self):
|
|
d = super().clean()
|
|
if d.get('subevent') and (d.get('subevents_from') or d.get('subevents_to')):
|
|
raise ValidationError(pgettext_lazy('subevent', 'Please either select a specific date or a date range, not both.'))
|
|
if bool(d.get('subevents_from')) != bool(d.get('subevents_to')):
|
|
raise ValidationError(pgettext_lazy('subevent', 'If you set a date range, please set both a start and an end.'))
|
|
return d
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
event = self.event = kwargs['event']
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.fields['items'].queryset = event.items.all()
|
|
if not self.initial.get('items'):
|
|
self.initial['items'] = event.items.all()
|
|
|
|
if event.has_subevents:
|
|
self.fields['subevent'].queryset = event.subevents.all()
|
|
self.fields['subevent'].widget = Select2(
|
|
attrs={
|
|
'data-model-select2': 'event',
|
|
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
|
'event': event.slug,
|
|
'organizer': event.organizer.slug,
|
|
}),
|
|
'data-placeholder': pgettext_lazy('subevent', 'Date')
|
|
}
|
|
)
|
|
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
|
|
else:
|
|
del self.fields['subevent']
|
|
del self.fields['subevents_from']
|
|
del self.fields['subevents_to']
|
|
|
|
|
|
class OrderMailForm(BaseMailForm):
|
|
recipients = forms.ChoiceField(
|
|
label=pgettext_lazy('sendmail_form', 'Send to'),
|
|
widget=forms.RadioSelect,
|
|
initial='orders',
|
|
choices=[]
|
|
)
|
|
sendto = forms.MultipleChoiceField() # overridden later
|
|
items = forms.ModelMultipleChoiceField(
|
|
widget=forms.CheckboxSelectMultiple(
|
|
attrs={'class': 'scrolling-multiple-choice'}
|
|
),
|
|
label=pgettext_lazy('sendmail_form', 'Restrict to products'),
|
|
required=True,
|
|
queryset=Item.objects.none()
|
|
)
|
|
filter_checkins = forms.BooleanField(
|
|
label=_('Filter check-in status'),
|
|
required=False
|
|
)
|
|
checkin_lists = SafeModelMultipleChoiceField(queryset=CheckinList.objects.none(), required=False) # overridden later
|
|
not_checked_in = forms.BooleanField(label=pgettext_lazy('sendmail_form', 'Restrict to recipients without check-in on any list'), required=False)
|
|
subevent = forms.ModelChoiceField(
|
|
SubEvent.objects.none(),
|
|
label=pgettext_lazy('sendmail_form', 'Restrict to a specific event date'),
|
|
required=False,
|
|
empty_label=pgettext_lazy('subevent', 'All dates')
|
|
)
|
|
subevents_from = forms.SplitDateTimeField(
|
|
widget=SplitDateTimePickerWidget(),
|
|
label=pgettext_lazy('sendmail_form', 'Restrict to event dates starting at or after'),
|
|
required=False,
|
|
)
|
|
subevents_to = forms.SplitDateTimeField(
|
|
widget=SplitDateTimePickerWidget(),
|
|
label=pgettext_lazy('sendmail_form', 'Restrict to event dates starting before'),
|
|
required=False,
|
|
)
|
|
created_from = forms.SplitDateTimeField(
|
|
widget=SplitDateTimePickerWidget(),
|
|
label=pgettext_lazy('sendmail_form', 'Restrict to orders created at or after'),
|
|
required=False,
|
|
)
|
|
created_to = forms.SplitDateTimeField(
|
|
widget=SplitDateTimePickerWidget(),
|
|
label=pgettext_lazy('sendmail_form', 'Restrict to orders created before'),
|
|
required=False,
|
|
)
|
|
attach_tickets = forms.BooleanField(
|
|
label=_("Attach tickets"),
|
|
help_text=_("Will be ignored if tickets exceed a given size limit to ensure email deliverability."),
|
|
required=False
|
|
)
|
|
attach_ical = forms.BooleanField(
|
|
label=_("Attach calendar files"),
|
|
required=False
|
|
)
|
|
|
|
def clean(self):
|
|
d = super().clean()
|
|
if d.get('subevent') and (d.get('subevents_from') or d.get('subevents_to')):
|
|
raise ValidationError(pgettext_lazy('subevent', 'Please either select a specific date or a date range, not both.'))
|
|
if bool(d.get('subevents_from')) != bool(d.get('subevents_to')):
|
|
raise ValidationError(pgettext_lazy('subevent', 'If you set a date range, please set both a start and an end.'))
|
|
return d
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
event = self.event = kwargs['event']
|
|
super().__init__(*args, **kwargs)
|
|
|
|
recp_choices = [
|
|
('orders', _('Everyone who placed an order'))
|
|
]
|
|
if event.settings.attendee_emails_asked:
|
|
recp_choices += [
|
|
('attendees', _('Every attendee (falling back to the order contact when no attendee email address is '
|
|
'given)')),
|
|
('both', _('Both (all order contact addresses and all attendee email addresses)'))
|
|
]
|
|
self.fields['recipients'].choices = recp_choices
|
|
|
|
choices = [(e, l) for e, l in Order.STATUS_CHOICE if e != 'n']
|
|
choices.insert(0, ('valid_if_pending', _('payment pending but already confirmed')))
|
|
choices.insert(0, ('na', _('payment pending (except unapproved or already confirmed)')))
|
|
choices.insert(0, ('pa', _('approval pending')))
|
|
if not event.settings.get('payment_term_expire_automatically', as_type=bool):
|
|
choices.append(
|
|
('overdue', _('pending with payment overdue'))
|
|
)
|
|
self.fields['sendto'] = forms.MultipleChoiceField(
|
|
label=pgettext_lazy('sendmail_form', 'Restrict to orders with status'),
|
|
widget=forms.CheckboxSelectMultiple(
|
|
attrs={'class': 'scrolling-multiple-choice no-search'}
|
|
),
|
|
choices=choices
|
|
)
|
|
if not self.initial.get('sendto'):
|
|
self.initial['sendto'] = ['p', 'valid_if_pending']
|
|
elif 'n' in self.initial['sendto']:
|
|
self.initial['sendto'].append('pa')
|
|
self.initial['sendto'].append('na')
|
|
self.initial['sendto'].append('valid_if_pending')
|
|
|
|
self.fields['items'].queryset = event.items.all()
|
|
if not self.initial.get('items'):
|
|
self.initial['items'] = event.items.all()
|
|
|
|
self.fields['checkin_lists'].queryset = event.checkin_lists.all()
|
|
self.fields['checkin_lists'].widget = Select2Multiple(
|
|
attrs={
|
|
'data-model-select2': 'generic',
|
|
'data-select2-url': reverse('control:event.orders.checkinlists.select2', kwargs={
|
|
'event': event.slug,
|
|
'organizer': event.organizer.slug,
|
|
}),
|
|
'data-placeholder': pgettext_lazy('sendmail_form', 'Restrict to recipients with check-in on list')
|
|
}
|
|
)
|
|
self.fields['checkin_lists'].widget.choices = self.fields['checkin_lists'].choices
|
|
self.fields['checkin_lists'].label = pgettext_lazy('sendmail_form', 'Restrict to recipients with check-in on list')
|
|
|
|
if event.has_subevents:
|
|
self.fields['subevent'].queryset = event.subevents.all()
|
|
self.fields['subevent'].widget = Select2(
|
|
attrs={
|
|
'data-model-select2': 'event',
|
|
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
|
'event': event.slug,
|
|
'organizer': event.organizer.slug,
|
|
}),
|
|
'data-placeholder': pgettext_lazy('subevent', 'Date')
|
|
}
|
|
)
|
|
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
|
|
else:
|
|
del self.fields['subevent']
|
|
del self.fields['subevents_from']
|
|
del self.fields['subevents_to']
|
|
|
|
|
|
class RuleForm(FormPlaceholderMixin, I18nModelForm):
|
|
class Meta:
|
|
model = Rule
|
|
|
|
fields = ['subject', 'template', 'attach_ical', 'send_date',
|
|
'send_offset_days', 'send_offset_time', 'subevent',
|
|
'all_products', 'limit_products', 'restrict_to_status',
|
|
'checked_in_status', 'send_to', 'enabled']
|
|
|
|
field_classes = {
|
|
'subevent': SafeModelMultipleChoiceField,
|
|
'limit_products': SafeModelMultipleChoiceField,
|
|
'send_date': SplitDateTimeField,
|
|
}
|
|
|
|
widgets = {
|
|
'send_date': SplitDateTimePickerWidget(attrs={
|
|
'data-display-dependency': '#id_schedule_type_0',
|
|
}),
|
|
'send_offset_days': forms.NumberInput(attrs={
|
|
'data-display-dependency': '#id_schedule_type_1,#id_schedule_type_2,#id_schedule_type_3,'
|
|
'#id_schedule_type_4',
|
|
}),
|
|
'send_offset_time': TimePickerWidget(attrs={
|
|
'data-display-dependency': '#id_schedule_type_1,#id_schedule_type_2,#id_schedule_type_3,'
|
|
'#id_schedule_type_4',
|
|
}),
|
|
'limit_products': forms.CheckboxSelectMultiple(
|
|
attrs={'class': 'scrolling-multiple-choice',
|
|
'data-inverse-dependency': '#id_all_products'},
|
|
),
|
|
'send_to': forms.RadioSelect,
|
|
'checked_in_status': forms.RadioSelect,
|
|
'template': I18nMarkdownTextarea,
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
instance = kwargs.get('instance')
|
|
|
|
if instance:
|
|
if instance.date_is_absolute:
|
|
dia = "abs"
|
|
else:
|
|
dia = "rel"
|
|
dia += "_a" if instance.offset_is_after else "_b"
|
|
dia += "_e" if instance.offset_to_event_end else "_s"
|
|
|
|
else:
|
|
dia = "abs"
|
|
|
|
kwargs.setdefault('initial', {})
|
|
kwargs['initial']['schedule_type'] = dia
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.fields['subevent'] = forms.ModelChoiceField(
|
|
SubEvent.objects.none(),
|
|
label=pgettext_lazy('sendmail_form', 'Restrict to a specific event date'),
|
|
required=False,
|
|
empty_label=pgettext_lazy('subevent', 'All dates')
|
|
)
|
|
|
|
if self.event.has_subevents:
|
|
self.fields['subevent'].queryset = self.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,
|
|
}),
|
|
'data-placeholder': pgettext_lazy('subevent', 'Date')
|
|
}
|
|
)
|
|
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
|
|
else:
|
|
del self.fields['subevent']
|
|
|
|
self.fields['limit_products'].queryset = Item.objects.filter(event=self.event)
|
|
|
|
self.fields['schedule_type'] = forms.ChoiceField(
|
|
label=_('Type of schedule time'),
|
|
widget=forms.RadioSelect,
|
|
choices=[
|
|
('abs', _('Absolute')),
|
|
('rel_b_s', _('Relative, before event start')),
|
|
('rel_b_e', _('Relative, before event end')),
|
|
('rel_a_s', _('Relative, after event start')),
|
|
('rel_a_e', _('Relative, after event end'))
|
|
]
|
|
)
|
|
|
|
self._set_field_placeholders('subject', ['event', 'order'])
|
|
self._set_field_placeholders('template', ['event', 'order'])
|
|
|
|
choices = [(e, l) for e, l in Order.STATUS_CHOICE if e != 'n']
|
|
choices.insert(0, ('n__valid_if_pending', _('payment pending but already confirmed')))
|
|
choices.insert(0, ('n__not_pending_approval_and_not_valid_if_pending',
|
|
_('payment pending (except unapproved or already confirmed)')))
|
|
choices.insert(0, ('n__pending_approval', _('approval pending')))
|
|
if not self.event.settings.get('payment_term_expire_automatically', as_type=bool):
|
|
choices.append(
|
|
('p__overdue', _('pending with payment overdue'))
|
|
)
|
|
self.fields['restrict_to_status'] = forms.MultipleChoiceField(
|
|
label=pgettext_lazy('sendmail_from', 'Restrict to orders with status'),
|
|
widget=forms.CheckboxSelectMultiple(
|
|
attrs={'class': 'scrolling-multiple-choice no-search'}
|
|
),
|
|
choices=choices
|
|
)
|
|
if not self.initial.get('restrict_to_status'):
|
|
self.initial['restrict_to_status'] = ['p', 'n__valid_if_pending']
|
|
|
|
def clean(self):
|
|
d = super().clean()
|
|
|
|
dia = d.get('schedule_type')
|
|
if dia == 'abs':
|
|
if not d.get('send_date'):
|
|
raise ValidationError({'send_date': _('Please specify the send date')})
|
|
d['date_is_absolute'] = True
|
|
d['send_offset_days'] = d['send_offset_time'] = None
|
|
else:
|
|
if not (d.get('send_offset_days') is not None and d.get('send_offset_time') is not None):
|
|
raise ValidationError(_('Please specify the offset days and time'))
|
|
d['offset_is_after'] = '_a' in dia
|
|
d['offset_to_event_end'] = '_e' in dia
|
|
d['date_is_absolute'] = False
|
|
d['send_date'] = None
|
|
|
|
if d.get('all_products'):
|
|
# having products checked while the option is ignored is probably counterintuitive
|
|
d['limit_products'] = Item.objects.none()
|
|
else:
|
|
if not d.get('limit_products'):
|
|
raise ValidationError({'limit_products': _('Please specify a product')})
|
|
|
|
self.instance.offset_is_after = d.get('offset_is_after', False)
|
|
self.instance.offset_to_event_end = d.get('offset_to_event_end', False)
|
|
self.instance.date_is_absolute = d.get('date_is_absolute', False)
|
|
|
|
return d
|