Sendmail rules: Extend filter by order status (#3402)

Add new order status filter settings instead of in form and API, while keeping backwards-compatibility
This commit is contained in:
Phin Wolkwitz
2023-07-21 17:43:19 +02:00
committed by GitHub
parent 26213f2ba9
commit 4b706339ed
10 changed files with 369 additions and 47 deletions

View File

@@ -26,17 +26,33 @@ from rest_framework import viewsets
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import Order
from pretix.plugins.sendmail.models import Rule
class RuleSerializer(I18nAwareModelSerializer):
class Meta:
model = Rule
fields = ['id', 'subject', 'template', 'all_products', 'limit_products', 'include_pending',
fields = ['id', 'subject', 'template', 'all_products', 'limit_products', 'restrict_to_status',
'send_date', 'send_offset_days', 'send_offset_time', 'date_is_absolute',
'offset_to_event_end', 'offset_is_after', 'send_to', 'enabled']
read_only_fields = ['id']
def to_internal_value(self, data):
if "restrict_to_status" not in data:
if "include_pending" in data:
if data["include_pending"]:
data['restrict_to_status'] = [
Order.STATUS_PAID,
'n__not_pending_approval_and_not_valid_if_pending',
'n__valid_if_pending'
]
else:
data['restrict_to_status'] = [Order.STATUS_PAID, 'n__valid_if_pending']
else:
data['restrict_to_status'] = [Order.STATUS_PAID, 'n__valid_if_pending']
return super().to_internal_value(data)
def validate(self, data):
data = super().validate(data)
@@ -56,6 +72,22 @@ class RuleSerializer(I18nAwareModelSerializer):
if not full_data.get('limit_products'):
raise ValidationError('limit_products is required when all_products=False')
rts = full_data.get('restrict_to_status')
if not rts or rts == []:
raise ValidationError('restrict_to_status needs at least one value')
elif rts:
for s in rts:
if s not in [
Order.STATUS_PAID,
Order.STATUS_EXPIRED,
Order.STATUS_CANCELED,
'n__valid_if_pending',
'n__pending_overdue',
'n__pending_approval',
'n__not_pending_approval_and_not_valid_if_pending',
]:
raise ValidationError(f'status {s} not allowed: restrict_to_status may only include valid states')
return full_data
def save(self, **kwargs):
@@ -66,7 +98,7 @@ with scopes_disabled():
class RuleFilter(FilterSet):
class Meta:
model = Rule
fields = ['id', 'all_products', 'include_pending', 'date_is_absolute',
fields = ['id', 'all_products', 'date_is_absolute',
'offset_to_event_end', 'offset_is_after', 'send_to', 'enabled']

View File

@@ -167,7 +167,7 @@ class WaitinglistMailForm(BaseMailForm):
class OrderMailForm(BaseMailForm):
recipients = forms.ChoiceField(
label=pgettext_lazy('sendmail_from', 'Send to'),
label=pgettext_lazy('sendmail_form', 'Send to'),
widget=forms.RadioSelect,
initial='orders',
choices=[]
@@ -186,7 +186,7 @@ class OrderMailForm(BaseMailForm):
required=False
)
checkin_lists = SafeModelMultipleChoiceField(queryset=CheckinList.objects.none(), required=False) # overridden later
not_checked_in = forms.BooleanField(label=pgettext_lazy('sendmail_from', 'Restrict to recipients without check-in'), required=False)
not_checked_in = forms.BooleanField(label=pgettext_lazy('sendmail_form', 'Restrict to recipients without check-in'), required=False)
subevent = forms.ModelChoiceField(
SubEvent.objects.none(),
label=pgettext_lazy('sendmail_form', 'Restrict to a specific event date'),
@@ -255,14 +255,14 @@ class OrderMailForm(BaseMailForm):
('overdue', _('pending with payment overdue'))
)
self.fields['sendto'] = forms.MultipleChoiceField(
label=pgettext_lazy('sendmail_from', 'Restrict to orders with status'),
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', 'na', 'valid_if_pending']
self.initial['sendto'] = ['p', 'valid_if_pending']
elif 'n' in self.initial['sendto']:
self.initial['sendto'].append('pa')
self.initial['sendto'].append('na')
@@ -280,11 +280,11 @@ class OrderMailForm(BaseMailForm):
'event': event.slug,
'organizer': event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('sendmail_from', 'Restrict to recipients with check-in on list')
'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_from', 'Restrict to recipients with check-in on list')
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()
@@ -311,7 +311,7 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
fields = ['subject', 'template', 'attach_ical',
'send_date', 'send_offset_days', 'send_offset_time',
'include_pending', 'all_products', 'limit_products',
'all_products', 'limit_products', 'restrict_to_status',
'send_to', 'enabled']
field_classes = {
@@ -375,6 +375,25 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
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()

View File

@@ -0,0 +1,33 @@
from django.db import migrations
import pretix.base.models.fields
def migrate_status_rules(apps, schema_editor):
rule_model = apps.get_model("sendmail", "Rule")
for r in rule_model.objects.all():
r.restrict_to_status = ['p', 'n__valid_if_pending']
if r.include_pending:
r.restrict_to_status.append('n__not_pending_approval_and_not_valid_if_pending')
r.save()
class Migration(migrations.Migration):
dependencies = [
('sendmail', '0003_rule_attach_ical'),
('pretixbase', '0241_itemmetaproperties_required_values'),
]
operations = [
migrations.AddField(
model_name='rule',
name='restrict_to_status',
field=pretix.base.models.fields.MultiStringField(default=['p', 'n__valid_if_pending']),
),
migrations.RunPython(migrate_status_rules),
migrations.RemoveField(
model_name='rule',
name='include_pending',
),
]

View File

@@ -26,7 +26,7 @@ from django.db import models
from django.db.models import Exists, OuterRef, Q
from django.utils import timezone
from django.utils.formats import date_format
from django.utils.timezone import make_aware
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext_lazy as _, ngettext
from django_scopes import ScopedManager
from i18nfield.fields import I18nCharField, I18nTextField
@@ -34,7 +34,7 @@ from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import (
Event, InvoiceAddress, Item, Order, OrderPosition, SubEvent,
Event, InvoiceAddress, Item, Order, OrderPosition, SubEvent, fields,
)
from pretix.base.models.base import LoggingMixin
from pretix.base.services.mail import SendMailException
@@ -126,13 +126,16 @@ class ScheduledMail(models.Model):
Exists(OrderPosition.objects.filter(order=OuterRef('pk'), item_id__in=limit_products))
)
if self.rule.include_pending:
status_q = Q(status__in=[Order.STATUS_PAID, Order.STATUS_PENDING])
else:
status_q = Q(
Q(status=Order.STATUS_PAID) |
Q(status=Order.STATUS_PENDING, valid_if_pending=True)
)
status_q = Q(status__in=self.rule.restrict_to_status)
if 'n__pending_approval' in self.rule.restrict_to_status:
status_q |= Q(status=Order.STATUS_PENDING, require_approval=True)
if 'n__not_pending_approval_and_not_valid_if_pending' in self.rule.restrict_to_status:
status_q |= Q(status=Order.STATUS_PENDING, require_approval=False, valid_if_pending=False)
if 'n__valid_if_pending' in self.rule.restrict_to_status:
status_q |= Q(status=Order.STATUS_PENDING, require_approval=False, valid_if_pending=True)
if 'n__pending_overdue' in self.rule.restrict_to_status:
status_q |= Q(status=Order.STATUS_PENDING, require_approval=False, valid_if_pending=False,
expires__lt=now())
if self.last_successful_order_id:
orders = orders.filter(
@@ -141,7 +144,6 @@ class ScheduledMail(models.Model):
orders = orders.filter(
status_q,
require_approval=False,
).order_by('pk').select_related('invoice_address').prefetch_related('positions')
send_to_orders = self.rule.send_to in (Rule.CUSTOMERS, Rule.BOTH)
@@ -212,10 +214,9 @@ class Rule(models.Model, LoggingMixin):
all_products = models.BooleanField(default=True, verbose_name=_('All products'))
limit_products = models.ManyToManyField(Item, blank=True, verbose_name=_('Limit products'))
include_pending = models.BooleanField(
default=False,
verbose_name=_('Include pending orders'),
help_text=_('By default, only paid orders will receive the email')
restrict_to_status = fields.MultiStringField(
verbose_name=_("Restrict to orders with status"),
default=['p', 'n__valid_if_pending'],
)
attach_ical = models.BooleanField(

View File

@@ -27,7 +27,7 @@
<fieldset>
<legend>{% trans "Recipients" %}</legend>
{% bootstrap_field form.send_to layout='control' %}
{% bootstrap_field form.include_pending layout='control' %}
{% bootstrap_field form.restrict_to_status layout='control' %}
{% bootstrap_field form.all_products layout='control' %}
{% bootstrap_field form.limit_products layout='horizontal' %}
</fieldset>

View File

@@ -41,7 +41,7 @@
<fieldset>
<legend>{% trans "Recipients" %}</legend>
{% bootstrap_field form.send_to layout='control' %}
{% bootstrap_field form.include_pending layout='control' %}
{% bootstrap_field form.restrict_to_status layout='control' %}
{% bootstrap_field form.all_products layout='control' %}
{% bootstrap_field form.limit_products layout='horizontal' %}
</fieldset>

View File

@@ -345,7 +345,7 @@ class OrderSendView(BaseSenderView):
qs = Order.objects.filter(event=self.request.event)
statusq = Q(status__in=form.cleaned_data['sendto'])
if 'overdue' in form.cleaned_data['sendto']:
statusq |= Q(status=Order.STATUS_PENDING, expires__lt=now())
statusq |= Q(status=Order.STATUS_PENDING, require_approval=False, valid_if_pending=False, expires__lt=now())
if 'pa' in form.cleaned_data['sendto']:
statusq |= Q(status=Order.STATUS_PENDING, require_approval=True)
if 'na' in form.cleaned_data['sendto']: