mirror of
https://github.com/pretix/pretix.git
synced 2026-08-10 10:47:00 +00:00
Automated emails: Extend filter by check-in state (#3489)
Co-authored-by: Richard Schreiber <schreiber@rami.io> Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
co-authored by
Richard Schreiber
Raphael Michel
parent
c2b25bad06
commit
4baf317934
@@ -23,10 +23,14 @@ limit_products list of integers List of product
|
||||
restrict_to_status list List of order states to restrict recipients to. Valid
|
||||
entries are ``p`` for paid, ``e`` for expired, ``c`` for canceled,
|
||||
``n__pending_approval`` for pending approval,
|
||||
``n__not_pending_approval_and_not_valid_if_pending`` for payment pending,
|
||||
``n__valid_if_pending`` for payment pending but already confirmed,
|
||||
``n__not_pending_approval_and_not_valid_if_pending`` for payment
|
||||
pending, ``n__valid_if_pending`` for payment pending but already confirmed,
|
||||
and ``n__pending_overdue`` for pending with payment overdue.
|
||||
The default is ``["p", "n__valid_if_pending"]``.
|
||||
checked_in_status string Check-in status to restrict recipients to. Valid strings are:
|
||||
``null`` for no filtering (default), ``checked_in`` for
|
||||
limiting to attendees that are or have been checked in, and
|
||||
``no_checkin`` for limiting to attendees who have not checked in.
|
||||
date_is_absolute boolean If ``true``, the email is set at a specific point in time.
|
||||
send_date datetime If ``date_is_absolute`` is set: Date and time to send the email.
|
||||
send_offset_days integer If ``date_is_absolute`` is not set, this is the number of days
|
||||
@@ -89,6 +93,7 @@ Endpoints
|
||||
"n__not_pending_approval_and_not_valid_if_pending",
|
||||
"n__valid_if_pending"
|
||||
],
|
||||
"checked_in_status": null,
|
||||
"send_date": null,
|
||||
"send_offset_days": 1,
|
||||
"send_offset_time": "18:00",
|
||||
@@ -139,6 +144,7 @@ Endpoints
|
||||
"n__not_pending_approval_and_not_valid_if_pending",
|
||||
"n__valid_if_pending"
|
||||
],
|
||||
"checked_in_status": null,
|
||||
"send_date": null,
|
||||
"send_offset_days": 1,
|
||||
"send_offset_time": "18:00",
|
||||
@@ -180,6 +186,7 @@ Endpoints
|
||||
"n__not_pending_approval_and_not_valid_if_pending",
|
||||
"n__valid_if_pending"
|
||||
],
|
||||
"checked_in_status": "checked_in",
|
||||
"send_date": null,
|
||||
"send_offset_days": 1,
|
||||
"send_offset_time": "18:00",
|
||||
@@ -209,6 +216,7 @@ Endpoints
|
||||
"n__not_pending_approval_and_not_valid_if_pending",
|
||||
"n__valid_if_pending"
|
||||
],
|
||||
"checked_in_status": "checked_in",
|
||||
"send_date": null,
|
||||
"send_offset_days": 1,
|
||||
"send_offset_time": "18:00",
|
||||
@@ -266,6 +274,7 @@ Endpoints
|
||||
"n__not_pending_approval_and_not_valid_if_pending",
|
||||
"n__valid_if_pending"
|
||||
],
|
||||
"checked_in_status": "checked_in",
|
||||
"send_date": null,
|
||||
"send_offset_days": 1,
|
||||
"send_offset_time": "18:00",
|
||||
|
||||
@@ -34,7 +34,7 @@ class RuleSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Rule
|
||||
fields = ['id', 'subject', 'template', 'all_products', 'limit_products', 'restrict_to_status',
|
||||
'send_date', 'send_offset_days', 'send_offset_time', 'date_is_absolute',
|
||||
'checked_in_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']
|
||||
|
||||
@@ -88,6 +88,10 @@ class RuleSerializer(I18nAwareModelSerializer):
|
||||
]:
|
||||
raise ValidationError(f'status {s} not allowed: restrict_to_status may only include valid states')
|
||||
|
||||
if full_data.get('checked_in_status') == "":
|
||||
# even though "blank" is not allowed on this field, "" gets accepted without this check
|
||||
raise ValidationError('empty string not allowed: use null to disable check-in based filtering')
|
||||
|
||||
return full_data
|
||||
|
||||
def save(self, **kwargs):
|
||||
|
||||
@@ -312,7 +312,7 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
|
||||
fields = ['subject', 'template', 'attach_ical',
|
||||
'send_date', 'send_offset_days', 'send_offset_time',
|
||||
'all_products', 'limit_products', 'restrict_to_status',
|
||||
'send_to', 'enabled']
|
||||
'checked_in_status', 'send_to', 'enabled']
|
||||
|
||||
field_classes = {
|
||||
'subevent': SafeModelMultipleChoiceField,
|
||||
@@ -337,6 +337,7 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
|
||||
'data-inverse-dependency': '#id_all_products'},
|
||||
),
|
||||
'send_to': forms.RadioSelect,
|
||||
'checked_in_status': forms.RadioSelect,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.19 on 2023-08-09 11:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sendmail', '0004_rule_restrict_to_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rule',
|
||||
name='checked_in_status',
|
||||
field=models.CharField(max_length=10, null=True),
|
||||
),
|
||||
]
|
||||
@@ -34,7 +34,8 @@ 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, fields,
|
||||
Checkin, Event, InvoiceAddress, Item, Order, OrderPosition, SubEvent,
|
||||
fields,
|
||||
)
|
||||
from pretix.base.models.base import LoggingMixin
|
||||
from pretix.base.services.mail import SendMailException
|
||||
@@ -112,19 +113,30 @@ class ScheduledMail(models.Model):
|
||||
e = self.event
|
||||
|
||||
orders = e.orders.all()
|
||||
limit_products = self.rule.limit_products.values_list('pk', flat=True) if not self.rule.all_products else None
|
||||
|
||||
filter_orders_by_op = False
|
||||
op_qs = OrderPosition.objects.filter(
|
||||
order__event=self.event,
|
||||
canceled=False,
|
||||
)
|
||||
|
||||
if self.subevent:
|
||||
orders = orders.filter(
|
||||
Exists(OrderPosition.objects.filter(order=OuterRef('pk'), subevent=self.subevent))
|
||||
)
|
||||
filter_orders_by_op = True
|
||||
op_qs = op_qs.filter(subevent=self.subevent)
|
||||
elif e.has_subevents:
|
||||
return # This rule should not even exist
|
||||
|
||||
if not self.rule.all_products:
|
||||
orders = orders.filter(
|
||||
Exists(OrderPosition.objects.filter(order=OuterRef('pk'), item_id__in=limit_products))
|
||||
)
|
||||
filter_orders_by_op = True
|
||||
limit_products = self.rule.limit_products.values_list('pk', flat=True)
|
||||
op_qs = op_qs.filter(item_id__in=limit_products)
|
||||
|
||||
if self.rule.checked_in_status == "no_checkin":
|
||||
filter_orders_by_op = True
|
||||
op_qs = op_qs.filter(~Exists(Checkin.objects.filter(position_id=OuterRef('pk'))))
|
||||
elif self.rule.checked_in_status == "checked_in":
|
||||
filter_orders_by_op = True
|
||||
op_qs = op_qs.filter(Exists(Checkin.objects.filter(position_id=OuterRef('pk'))))
|
||||
|
||||
status_q = Q(status__in=self.rule.restrict_to_status)
|
||||
if 'n__pending_approval' in self.rule.restrict_to_status:
|
||||
@@ -142,6 +154,8 @@ class ScheduledMail(models.Model):
|
||||
pk__gt=self.last_successful_order_id
|
||||
)
|
||||
|
||||
if filter_orders_by_op:
|
||||
orders = orders.filter(pk__in=op_qs.values_list('order_id', flat=True))
|
||||
orders = orders.filter(
|
||||
status_q,
|
||||
).order_by('pk').select_related('invoice_address').prefetch_related('positions')
|
||||
@@ -205,6 +219,12 @@ class Rule(models.Model, LoggingMixin):
|
||||
(BOTH, _('Both (all order contact addresses and all attendee email addresses)'))
|
||||
]
|
||||
|
||||
CHECK_IN_STATUS_CHOICES = [
|
||||
(None, _("Everyone")),
|
||||
("checked_in", _("Anyone who is or was checked in")),
|
||||
("no_checkin", _("Anyone who never checked in before"))
|
||||
]
|
||||
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='sendmail_rules')
|
||||
|
||||
@@ -219,6 +239,15 @@ class Rule(models.Model, LoggingMixin):
|
||||
default=['p', 'n__valid_if_pending'],
|
||||
)
|
||||
|
||||
checked_in_status = models.CharField(
|
||||
verbose_name=_("Restrict to check-in status"),
|
||||
default=None,
|
||||
choices=CHECK_IN_STATUS_CHOICES,
|
||||
max_length=10,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
attach_ical = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Attach calendar files"),
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
<legend>{% trans "Recipients" %}</legend>
|
||||
{% bootstrap_field form.send_to layout='control' %}
|
||||
{% bootstrap_field form.restrict_to_status layout='control' %}
|
||||
{% bootstrap_field form.checked_in_status layout='control' %}
|
||||
<hr>
|
||||
{% bootstrap_field form.all_products layout='control' %}
|
||||
{% bootstrap_field form.limit_products layout='horizontal' %}
|
||||
</fieldset>
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
<legend>{% trans "Recipients" %}</legend>
|
||||
{% bootstrap_field form.send_to layout='control' %}
|
||||
{% bootstrap_field form.restrict_to_status layout='control' %}
|
||||
{% bootstrap_field form.checked_in_status layout='control' %}
|
||||
<hr>
|
||||
{% bootstrap_field form.all_products layout='control' %}
|
||||
{% bootstrap_field form.limit_products layout='horizontal' %}
|
||||
</fieldset>
|
||||
|
||||
@@ -39,7 +39,8 @@ TEST_RULE_RES = {
|
||||
'template': {'en': 'foo'},
|
||||
'all_products': True,
|
||||
'limit_products': [],
|
||||
"restrict_to_status": ['p', 'n__valid_if_pending'],
|
||||
'restrict_to_status': ['p', 'n__valid_if_pending'],
|
||||
'checked_in_status': None,
|
||||
'send_date': '2021-07-08T00:00:00Z',
|
||||
'send_offset_days': None,
|
||||
'send_offset_time': None,
|
||||
@@ -160,7 +161,8 @@ def test_sendmail_rule_create_full(token_client, organizer, event, item):
|
||||
'template': {'en': 'foobar'},
|
||||
'all_products': False,
|
||||
'limit_products': [event.items.first().pk],
|
||||
"restrict_to_status": ['p', 'n__not_pending_approval_and_not_valid_if_pending', 'n__valid_if_pending'],
|
||||
'restrict_to_status': ['p', 'n__not_pending_approval_and_not_valid_if_pending', 'n__valid_if_pending'],
|
||||
'checked_in_status': None,
|
||||
'send_offset_days': 3,
|
||||
'send_offset_time': '09:30',
|
||||
'date_is_absolute': False,
|
||||
@@ -174,6 +176,7 @@ def test_sendmail_rule_create_full(token_client, organizer, event, item):
|
||||
assert r.all_products is False
|
||||
assert [i.pk for i in r.limit_products.all()] == [event.items.first().pk]
|
||||
assert r.restrict_to_status == ['p', 'n__not_pending_approval_and_not_valid_if_pending', 'n__valid_if_pending']
|
||||
assert r.checked_in_status is None
|
||||
assert r.send_offset_days == 3
|
||||
assert r.send_offset_time == datetime.time(9, 30)
|
||||
assert r.date_is_absolute is False
|
||||
@@ -348,6 +351,49 @@ def test_sendmail_rule_restrict_recipients(token_client, organizer, event, rule)
|
||||
)
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
@pytest.mark.django_db
|
||||
def test_sendmail_rule_checkin(token_client, organizer, event, rule):
|
||||
valid_states = [None, 'checked_in', 'no_checkin', ]
|
||||
invalid_states = ['', 'foo']
|
||||
|
||||
for s in valid_states:
|
||||
result = create_rule(
|
||||
token_client, organizer, event,
|
||||
data={
|
||||
'subject': {'en': 'meow'},
|
||||
'template': {'en': 'creative text here'},
|
||||
'send_date': '2018-03-17T13:31Z',
|
||||
'checked_in_status': s,
|
||||
},
|
||||
expected_failure=False
|
||||
)
|
||||
assert result.checked_in_status == s
|
||||
|
||||
for s in invalid_states:
|
||||
create_rule(
|
||||
token_client, organizer, event,
|
||||
data={
|
||||
'subject': {'en': 'meow'},
|
||||
'template': {'en': 'creative text here'},
|
||||
'send_date': '2018-03-17T13:31Z',
|
||||
'checked_in_status': s,
|
||||
},
|
||||
expected_failure=True
|
||||
)
|
||||
|
||||
result = create_rule(
|
||||
token_client, organizer, event,
|
||||
data={
|
||||
'subject': {'en': 'meow'},
|
||||
'template': {'en': 'creative text here'},
|
||||
'send_date': '2018-03-17T13:31Z',
|
||||
},
|
||||
expected_failure=False
|
||||
)
|
||||
assert result.checked_in_status is None
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
@pytest.mark.django_db
|
||||
def test_sendmail_rule_change(token_client, organizer, event, rule):
|
||||
|
||||
@@ -29,6 +29,7 @@ from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import InvoiceAddress, Order
|
||||
from pretix.base.services.checkin import perform_checkin
|
||||
from pretix.plugins.sendmail.models import Rule, ScheduledMail
|
||||
from pretix.plugins.sendmail.signals import sendmail_run_rules
|
||||
|
||||
@@ -276,6 +277,100 @@ def test_sendmail_rule_send_correct_products(event, order, item, item2):
|
||||
assert djmail.outbox[0].to[0] == p1.attendee_email
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@scopes_disabled()
|
||||
def test_sendmail_rule_not_checked_in_all_get_mail(event, order, item):
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="all",
|
||||
subject='meow', template='meow meow meow')
|
||||
|
||||
djmail.outbox = []
|
||||
sendmail_run_rules(None)
|
||||
assert len(djmail.outbox) == 1, "email not sent"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@scopes_disabled()
|
||||
def test_sendmail_rule_checked_in_all_get_mail(event, order, item):
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
|
||||
clist = event.checkin_lists.create(name="Default", all_products=True)
|
||||
perform_checkin(p1, clist, {})
|
||||
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="all",
|
||||
subject='meow', template='meow meow meow')
|
||||
|
||||
djmail.outbox = []
|
||||
sendmail_run_rules(None)
|
||||
assert len(djmail.outbox) == 1, "email not sent"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@scopes_disabled()
|
||||
def test_sendmail_rule_not_checked_in_no_mail(event, order, item):
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="checked_in",
|
||||
subject='meow', template='meow meow meow')
|
||||
|
||||
# receives no mail when not checked in
|
||||
djmail.outbox = []
|
||||
sendmail_run_rules(None)
|
||||
assert len(djmail.outbox) == 0, "email sent unexpectedly"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@scopes_disabled()
|
||||
def test_sendmail_rule_not_checked_in_get_mail(event, order, item):
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
|
||||
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="no_checkin",
|
||||
subject='meow', template='meow meow meow')
|
||||
|
||||
# receives mail when not checked in
|
||||
djmail.outbox = []
|
||||
sendmail_run_rules(None)
|
||||
assert len(djmail.outbox) == 1, "email not sent"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@scopes_disabled()
|
||||
def test_sendmail_rule_checked_in_no_mail(event, order, item):
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
|
||||
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) == 0, "email sent unexpectedly"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@scopes_disabled()
|
||||
def test_sendmail_rule_checked_in_get_mail(event, order, item):
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
|
||||
clist = event.checkin_lists.create(name="Default", all_products=True)
|
||||
|
||||
# receives 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, "email not sent"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@scopes_disabled()
|
||||
def run_restriction_test(event, order, restrictions_pass=[], restrictions_fail=[]):
|
||||
|
||||
Reference in New Issue
Block a user