From 84858bc04802538cf615c8c5b0023f488f16a0e7 Mon Sep 17 00:00:00 2001 From: Phin Wolkwitz Date: Thu, 1 Oct 2026 13:13:55 +0200 Subject: [PATCH] Allow mails to be sent to addon-attendees (Z#23213551) (#6235) Remove restrictions that prevent mails to be sent to addon-product-attendees while reducing amount of mails sent to the same email-addresses Co-authored-by: Richard Schreiber Co-authored-by: Kara Engelhardt --- src/pretix/plugins/sendmail/forms.py | 12 +- src/pretix/plugins/sendmail/models.py | 85 +++--- src/pretix/plugins/sendmail/tasks.py | 40 ++- src/pretix/presale/views/order.py | 1 - src/tests/plugins/sendmail/test_rules.py | 78 +++++ src/tests/plugins/sendmail/test_sendmail.py | 306 ++++++++++++++++++++ 6 files changed, 476 insertions(+), 46 deletions(-) diff --git a/src/pretix/plugins/sendmail/forms.py b/src/pretix/plugins/sendmail/forms.py index f0f0573a40..ac0de8eca7 100644 --- a/src/pretix/plugins/sendmail/forms.py +++ b/src/pretix/plugins/sendmail/forms.py @@ -161,7 +161,12 @@ class OrderMailForm(BaseMailForm): ), label=pgettext_lazy('sendmail_form', 'Restrict to products'), required=True, - queryset=Item.objects.none() + queryset=Item.objects.none(), + help_text=pgettext_lazy( + 'sendmail_form', + 'There may be multiple mails sent out to the same mail address if one order contains multiple attendee ' + 'products for it, if you restrict to products while also restricting mails to attendees only. ' + 'This is intended, as every one of those get linked to their own separate order page restricted to only that product.') ) filter_checkins = forms.BooleanField( label=_('Filter check-in status'), @@ -374,6 +379,11 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm): del self.fields['subevent'] self.fields['limit_products'].queryset = Item.objects.filter(event=self.event) + self.fields['limit_products'].help_text = pgettext_lazy( + 'sendmail_form', + 'There may be multiple mails sent out to the same mail address if one order contains multiple attendee ' + 'products for it, if you restrict to products while also restricting mails to attendees only. ' + 'This is intended, as every one of those get linked to their own separate order page restricted to only that product.') self.fields['schedule_type'] = forms.ChoiceField( label=_('Type of schedule time'), diff --git a/src/pretix/plugins/sendmail/models.py b/src/pretix/plugins/sendmail/models.py index b68916c010..dafa8cf87f 100644 --- a/src/pretix/plugins/sendmail/models.py +++ b/src/pretix/plugins/sendmail/models.py @@ -19,6 +19,7 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # +import logging from datetime import datetime, time, timedelta from dateutil.tz import datetime_exists @@ -39,6 +40,8 @@ from pretix.base.models import ( ) from pretix.base.models.base import LoggingMixin +logger = logging.getLogger(__name__) + class ScheduledMail(models.Model): STATE_SCHEDULED = 'scheduled' @@ -115,7 +118,7 @@ class ScheduledMail(models.Model): filter_orders_by_op = False op_qs = OrderPosition.objects.filter( - order__event=self.event, + order__event=e, canceled=False, ) @@ -167,14 +170,58 @@ class ScheduledMail(models.Model): for o in orders: with language(o.locale, e.settings.region): positions = list(o.positions.all()) - o_sent = False + send_to_order = send_to_orders try: ia = o.invoice_address except InvoiceAddress.DoesNotExist: ia = InvoiceAddress(order=o) - if send_to_orders and o.email: + if send_to_attendees: + parent_op = None + sent_to_positions = set() + for p in positions: + if p.addon_to_id is None: + # this op might have matching add-ons, so save for later + parent_op = p + elif not parent_op or p.addon_to_id != parent_op.id: + # this op is an add-on, but not to the current parent_op + # something got mixed up as add-ons should always come directly after their parent + logger.warning(f"Add-ons are mixed up for position #{p.positionid} in order {o.full_code}") + continue + + if p.id not in position_ids: + # not a matching op, just there for parent_op + continue + + if p.addon_to_id and ( + not p.attendee_email or p.attendee_email == parent_op.attendee_email + ): + # if op is addon and either has no mail or the same as parent => send to parent + p = parent_op + + if p.pk in sent_to_positions: + # this position already got an email + continue + + if not p.attendee_email or p.attendee_email == o.email: + # no email or same as order => send to order + send_to_order = True + continue + + 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') + sent_to_positions.add(p.pk) + + if send_to_order and o.email: email_ctx = get_email_context( event=e, order=o, @@ -184,38 +231,6 @@ class ScheduledMail(models.Model): 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 send_to_attendees: - if not self.rule.all_products: - positions = [p for p in positions if p.item_id in limit_products] - if self.subevent_id: - positions = [p for p in positions if p.subevent_id == self.subevent_id] - - for p in positions: - 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 diff --git a/src/pretix/plugins/sendmail/tasks.py b/src/pretix/plugins/sendmail/tasks.py index 03345dfada..79d259e684 100644 --- a/src/pretix/plugins/sendmail/tasks.py +++ b/src/pretix/plugins/sendmail/tasks.py @@ -31,6 +31,7 @@ # 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. +import logging from datetime import datetime from django.db.models import Exists, OuterRef, Q @@ -43,6 +44,8 @@ from pretix.base.services.mail import mail from pretix.base.services.tasks import ProfiledEventTask from pretix.celery_app import app +logger = logging.getLogger(__name__) + def _chunks(lst, n): """ @@ -70,6 +73,8 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict, except InvoiceAddress.DoesNotExist: ia = InvoiceAddress(order=o) + parent_op = None + sent_to_positions = set() if recipients in ('both', 'attendees'): for p in o.positions.annotate( any_checkins=Exists( @@ -85,10 +90,16 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict, ) ), ).prefetch_related('addons', 'subevent'): - if p.addon_to_id is not None: + + if p.addon_to_id is None: + parent_op = p + elif not parent_op or p.addon_to_id != parent_op.id: + # this op is an add-on, but not to the current parent_op + # something got mixed up as add-ons should always come directly after their parent + logger.warning(f"Add-ons are mixed up for position #{p.positionid} in order {o.full_code}") continue - if p.item_id not in items and not any(a.item_id in items for a in p.addons.all()): + if p.item_id not in items: continue if filter_checkins: @@ -99,13 +110,11 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict, if not allowed: continue - if not p.attendee_email: - if recipients == 'attendees': - send_to_order = True - continue - - if p.attendee_email == o.email and send_to_order: - continue + if p.addon_to_id and ( + not p.attendee_email or p.attendee_email == parent_op.attendee_email + ): + # if op is addon and either has no mail or the same as parent => send to parent + p = parent_op if subevent and p.subevent_id != subevent: continue @@ -116,6 +125,18 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict, if subevents_to and p.subevent.date_from >= subevents_to: continue + if not p.attendee_email: + send_to_order = True + continue + + if p.pk in sent_to_positions: + # this position already got an email + continue + + if p.attendee_email == o.email: + send_to_order = True + continue + with language(o.locale, event.settings.region): email_context = get_email_context(event=event, order=o, invoice_address=ia, position=p) outgoing_mail = mail( @@ -137,6 +158,7 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict, user=user, data=outgoing_mail.log_data(), ) + sent_to_positions.add(p.pk) if send_to_order and o.email: with language(o.locale, event.settings.region): diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index ad2426c66e..0e9812f8b5 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -183,7 +183,6 @@ class OrderPositionDetailMixin(NoSearchIndexViewMixin): def position(self): qs = OrderPosition.objects.filter( order__event=self.request.event, - addon_to__isnull=True, order__code=self.kwargs['order'], positionid=self.kwargs['position'] ).select_related('order', 'order__event') diff --git a/src/tests/plugins/sendmail/test_rules.py b/src/tests/plugins/sendmail/test_rules.py index 290d066f9b..77f180f698 100644 --- a/src/tests/plugins/sendmail/test_rules.py +++ b/src/tests/plugins/sendmail/test_rules.py @@ -199,6 +199,84 @@ def test_sendmail_rule_send_order_vs_pos(send_to, amount_mails, recipients, orde assert djmail.outbox[0].subject == 'Dummy: Foo City @ 11:30' +@pytest.mark.django_db +@pytest.mark.parametrize('send_to,amount_mails,recipients,ticket_mail,addon_mail, products', [ + (Rule.ATTENDEES, 1, ['addon-attendee@dummy.test'], 'attendee@dummy.test', 'addon-attendee@dummy.test', 'addon'), + (Rule.ATTENDEES, 2, ['attendee@dummy.test', 'addon-attendee@dummy.test'], 'attendee@dummy.test', + 'addon-attendee@dummy.test', 'both'), + (Rule.ATTENDEES, 1, ['attendee@dummy.test'], 'attendee@dummy.test', 'attendee@dummy.test', 'both'), + (Rule.ATTENDEES, 1, ['attendee@dummy.test'], 'attendee@dummy.test', None, 'addon'), + (Rule.ATTENDEES, 1, ['attendee@dummy.test'], 'attendee@dummy.test', None, 'both'), + (Rule.ATTENDEES, 1, ['dummy@dummy.test'], None, None, 'addon'), + (Rule.ATTENDEES, 1, ['dummy@dummy.test'], None, None, 'both'), + (Rule.ATTENDEES, 2, ['dummy@dummy.test', 'addon-attendee@dummy.test'], None, 'addon-attendee@dummy.test', 'both'), + (Rule.ATTENDEES, 1, ['dummy@dummy.test'], None, None, 'all'), + (Rule.ATTENDEES, 2, ['dummy@dummy.test', 'addon-attendee@dummy.test'], None, 'addon-attendee@dummy.test', 'all'), +]) +@scopes_disabled() +def test_sendmail_rule_send_addons(send_to, amount_mails, recipients, ticket_mail, addon_mail, products, order, + event, pos, item, item2): + djmail.outbox = [] + + order.status = order.STATUS_PAID + order.save() + + p = pos + p.attendee_email = ticket_mail + p.save() + order.all_positions.create(item=item2, price=0, attendee_email=addon_mail, addon_to=p) + rule = order.event.sendmail_rules.create(date_is_absolute=True, send_date=dt_now - datetime.timedelta(hours=1), + send_to=send_to, subject='meow', template='meow meow meow', + all_products=products == 'all') + if products == 'addon': + rule.limit_products.set([item2]) + if products == 'both': + rule.limit_products.set([item, item2]) + + sendmail_run_rules(None) + + assert len(djmail.outbox) == amount_mails + + _recipients = [mail.to[0] for mail in djmail.outbox] + assert set(recipients) == set(_recipients) + + +@pytest.mark.django_db +@pytest.mark.parametrize('send_to,amount_mails,recipients,ticket_mail,addon_mail, products', [ + (Rule.ATTENDEES, 2, ['attendee@dummy.test', 'addon-attendee@dummy.test'], 'attendee@dummy.test', + 'addon-attendee@dummy.test', 'addon'), + (Rule.ATTENDEES, 2, ['attendee@dummy.test', 'addon-attendee@dummy.test'], 'attendee@dummy.test', + 'addon-attendee@dummy.test', 'both'), +]) +@scopes_disabled() +def test_sendmail_rule_send_addons_one_unp(send_to, amount_mails, recipients, ticket_mail, addon_mail, products, order, + event, pos, item, item2): + djmail.outbox = [] + + order.status = order.STATUS_PAID + order.save() + + p = pos + p.attendee_email = ticket_mail + p.save() + order.all_positions.create(item=item2, price=0, attendee_email=addon_mail, addon_to=p) + order.all_positions.create(item=item2, price=0, addon_to=p) + rule = order.event.sendmail_rules.create(date_is_absolute=True, send_date=dt_now - datetime.timedelta(hours=1), + send_to=send_to, subject='meow', template='meow meow meow', + all_products=False) + if products == 'addon': + rule.limit_products.set([item2]) + if products == 'both': + rule.limit_products.set([item, item2]) + + sendmail_run_rules(None) + + assert len(djmail.outbox) == amount_mails + + _recipients = [mail.to[0] for mail in djmail.outbox] + assert set(recipients) == set(_recipients) + + @pytest.mark.django_db @scopes_disabled() def test_sendmail_rule_send_attendees_unset_mail(order, event, item): diff --git a/src/tests/plugins/sendmail/test_sendmail.py b/src/tests/plugins/sendmail/test_sendmail.py index c971291d03..1d7f198db4 100644 --- a/src/tests/plugins/sendmail/test_sendmail.py +++ b/src/tests/plugins/sendmail/test_sendmail.py @@ -406,6 +406,312 @@ def test_sendmail_attendee_product_filter(logged_in_client, sendmail_url, event, assert '/order/' not in djmail.outbox[0].body +@pytest.mark.django_db +def test_sendmail_attendee_addon_filter(logged_in_client, sendmail_url, event, order, pos): + event.settings.attendee_emails_asked = True + with scopes_disabled(): + addon = Item.objects.create(name='Test addon', event=event, default_price=12) + p = pos + p.attendee_email = 'attendee1@dummy.test' + p.save() + order.positions.create( + item=addon, price=0, attendee_email='add-on-attendee@dummy.test', addon_to=p + ) + + djmail.outbox = [] + response = logged_in_client.post(sendmail_url + 'orders/', + {'sendto': 'na', + 'action': 'send', + 'recipients': 'attendees', + 'items': addon.pk, + 'subject_0': 'Test subject', + 'message_0': 'This is a test file for sending mails.', + }, + follow=True) + assert response.status_code == 200 + assert 'alert-success' in response.rendered_content + assert len(djmail.outbox) == 1 + assert djmail.outbox[0].to == ['add-on-attendee@dummy.test'] + assert '/ticket/' in djmail.outbox[0].body + assert '/order/' not in djmail.outbox[0].body + + +@pytest.mark.django_db +def test_sendmail_attendee_ticket_and_addon_filter(logged_in_client, sendmail_url, event, order, pos): + event.settings.attendee_emails_asked = True + with scopes_disabled(): + addon = Item.objects.create(name='Test addon', event=event, default_price=12) + p = pos + p.attendee_email = 'attendee1@dummy.test' + p.save() + order.positions.create( + item=addon, price=0, attendee_email='add-on-attendee@dummy.test', addon_to=p + ) + + djmail.outbox = [] + response = logged_in_client.post(sendmail_url + 'orders/', + {'sendto': 'na', + 'action': 'send', + 'recipients': 'attendees', + 'items': {addon.pk, p.item_id}, + 'subject_0': 'Test subject', + 'message_0': 'This is a test file for sending mails.', + }, + follow=True) + assert response.status_code == 200 + assert 'alert-success' in response.rendered_content + assert len(djmail.outbox) == 2 + for msg in djmail.outbox: + assert msg.to in [['attendee1@dummy.test'], ['add-on-attendee@dummy.test']] + assert '/ticket/' in msg.body + assert '/order/' not in msg.body + + +@pytest.mark.django_db +def test_sendmail_attendee_ticket_and_same_addon_filter(logged_in_client, sendmail_url, event, order, pos): + event.settings.attendee_emails_asked = True + with scopes_disabled(): + addon = Item.objects.create(name='Test addon', event=event, default_price=12) + p = pos + p.attendee_email = 'attendee1@dummy.test' + p.save() + order.positions.create( + item=addon, price=0, attendee_email='attendee1@dummy.test', addon_to=p + ) + + djmail.outbox = [] + response = logged_in_client.post(sendmail_url + 'orders/', + {'sendto': 'na', + 'action': 'send', + 'recipients': 'attendees', + 'items': {addon.pk, p.item_id}, + 'subject_0': 'Test subject', + 'message_0': 'This is a test file for sending mails.', + }, + follow=True) + assert response.status_code == 200 + assert 'alert-success' in response.rendered_content + assert len(djmail.outbox) == 1 + assert djmail.outbox[0].to == ['attendee1@dummy.test'] + assert '/ticket/' in djmail.outbox[0].body + assert '/order/' not in djmail.outbox[0].body + + +@pytest.mark.django_db +def test_sendmail_attendee_addon_unpersonalized_filter(logged_in_client, sendmail_url, event, order, pos): + event.settings.attendee_emails_asked = True + with scopes_disabled(): + addon = Item.objects.create(name='Test addon', event=event, default_price=12) + p = pos + p.attendee_email = 'attendee1@dummy.test' + p.save() + order.positions.create( + item=addon, price=0, addon_to=p + ) + + djmail.outbox = [] + response = logged_in_client.post(sendmail_url + 'orders/', + {'sendto': 'na', + 'action': 'send', + 'recipients': 'attendees', + 'items': addon.pk, + 'subject_0': 'Test subject', + 'message_0': 'This is a test file for sending mails.', + }, + follow=True) + assert response.status_code == 200 + assert 'alert-success' in response.rendered_content + assert len(djmail.outbox) == 1 + assert djmail.outbox[0].to == ['attendee1@dummy.test'] + assert '/ticket/' in djmail.outbox[0].body + assert '/order/' not in djmail.outbox[0].body + + +@pytest.mark.django_db +def test_sendmail_attendee_ticket_and_addon_unp_filter(logged_in_client, sendmail_url, event, order, pos): + event.settings.attendee_emails_asked = True + with scopes_disabled(): + addon = Item.objects.create(name='Test addon', event=event, default_price=12) + p = pos + p.attendee_email = 'attendee1@dummy.test' + p.save() + order.positions.create( + item=addon, price=0, addon_to=p + ) + + djmail.outbox = [] + response = logged_in_client.post(sendmail_url + 'orders/', + {'sendto': 'na', + 'action': 'send', + 'recipients': 'attendees', + 'items': {addon.pk, p.item_id}, + 'subject_0': 'Test subject', + 'message_0': 'This is a test file for sending mails.', + }, + follow=True) + assert response.status_code == 200 + assert 'alert-success' in response.rendered_content + assert len(djmail.outbox) == 1 + assert djmail.outbox[0].to == ['attendee1@dummy.test'] + assert '/ticket/' in djmail.outbox[0].body + assert '/order/' not in djmail.outbox[0].body + + +@pytest.mark.django_db +def test_sendmail_attendee_ticket_unp_and_addon_filter(logged_in_client, sendmail_url, event, order, pos): + event.settings.attendee_emails_asked = True + with scopes_disabled(): + addon = Item.objects.create(name='Test addon', event=event, default_price=12) + order.positions.create( + item=addon, price=0, attendee_email='add-on-attendee@dummy.test', addon_to=pos + ) + + djmail.outbox = [] + response = logged_in_client.post(sendmail_url + 'orders/', + {'sendto': 'na', + 'action': 'send', + 'recipients': 'attendees', + 'items': {addon.pk, pos.item_id}, + 'subject_0': 'Test subject', + 'message_0': 'This is a test file for sending mails.', + }, + follow=True) + assert response.status_code == 200 + assert 'alert-success' in response.rendered_content + assert len(djmail.outbox) == 2 + for msg in djmail.outbox: + assert msg.to in [[order.email], ['add-on-attendee@dummy.test']] + if msg.to == [order.email]: + assert '/ticket/' not in msg.body + assert '/order/' in msg.body + else: + assert msg.to == ['add-on-attendee@dummy.test'] + assert '/ticket/' in msg.body + assert '/order/' not in msg.body + + +@pytest.mark.django_db +def test_sendmail_attendee_addon_unp_unp_filter(logged_in_client, sendmail_url, event, order, pos): + event.settings.attendee_emails_asked = True + with scopes_disabled(): + addon = Item.objects.create(name='Test addon', event=event, default_price=12) + order.positions.create( + item=addon, price=0, addon_to=pos + ) + + djmail.outbox = [] + response = logged_in_client.post(sendmail_url + 'orders/', + {'sendto': 'na', + 'action': 'send', + 'recipients': 'attendees', + 'items': addon.pk, + 'subject_0': 'Test subject', + 'message_0': 'This is a test file for sending mails.', + }, + follow=True) + assert response.status_code == 200 + assert 'alert-success' in response.rendered_content + assert len(djmail.outbox) == 1 + assert djmail.outbox[0].to == [order.email] + assert '/ticket/' not in djmail.outbox[0].body + assert '/order/' in djmail.outbox[0].body + + +@pytest.mark.django_db +def test_sendmail_attendee_and_addon_unp_unp_filter(logged_in_client, sendmail_url, event, order, pos): + event.settings.attendee_emails_asked = True + with scopes_disabled(): + addon = Item.objects.create(name='Test addon', event=event, default_price=12) + order.positions.create( + item=addon, price=0, addon_to=pos + ) + + djmail.outbox = [] + response = logged_in_client.post(sendmail_url + 'orders/', + {'sendto': 'na', + 'action': 'send', + 'recipients': 'attendees', + 'items': {addon.pk, pos.item_id}, + 'subject_0': 'Test subject', + 'message_0': 'This is a test file for sending mails.', + }, + follow=True) + assert response.status_code == 200 + assert 'alert-success' in response.rendered_content + assert len(djmail.outbox) == 1 + assert djmail.outbox[0].to == [order.email] + assert '/ticket/' not in djmail.outbox[0].body + assert '/order/' in djmail.outbox[0].body + + +@pytest.mark.django_db +def test_sendmail_attendee_two_addons_one_unp_filter(logged_in_client, sendmail_url, event, order, pos): + event.settings.attendee_emails_asked = True + with scopes_disabled(): + p = pos + p.attendee_email = 'attendee1@dummy.test' + p.save() + addon = Item.objects.create(name='Test addon', event=event, default_price=12) + order.positions.create( + item=addon, price=0, attendee_email='add-on-attendee@dummy.test', addon_to=p + ) + order.positions.create( + item=addon, price=0, addon_to=p + ) + + djmail.outbox = [] + response = logged_in_client.post(sendmail_url + 'orders/', + {'sendto': 'na', + 'action': 'send', + 'recipients': 'attendees', + 'items': addon.pk, + 'subject_0': 'Test subject', + 'message_0': 'This is a test file for sending mails.', + }, + follow=True) + assert response.status_code == 200 + assert 'alert-success' in response.rendered_content + assert len(djmail.outbox) == 2 + for msg in djmail.outbox: + assert msg.to in [['attendee1@dummy.test'], ['add-on-attendee@dummy.test']] + assert '/ticket/' in msg.body + assert '/order/' not in msg.body + + +@pytest.mark.django_db +def test_sendmail_attendee_and_two_addons_one_unp_filter(logged_in_client, sendmail_url, event, order, pos): + event.settings.attendee_emails_asked = True + with scopes_disabled(): + p = pos + p.attendee_email = 'attendee1@dummy.test' + p.save() + addon = Item.objects.create(name='Test addon', event=event, default_price=12) + order.positions.create( + item=addon, price=0, attendee_email='add-on-attendee@dummy.test', addon_to=p + ) + order.positions.create( + item=addon, price=0, addon_to=p + ) + + djmail.outbox = [] + response = logged_in_client.post(sendmail_url + 'orders/', + {'sendto': 'na', + 'action': 'send', + 'recipients': 'attendees', + 'items': {addon.pk, p.item_id}, + 'subject_0': 'Test subject', + 'message_0': 'This is a test file for sending mails.', + }, + follow=True) + assert response.status_code == 200 + assert 'alert-success' in response.rendered_content + assert len(djmail.outbox) == 2 + for msg in djmail.outbox: + assert msg.to in [['attendee1@dummy.test'], ['add-on-attendee@dummy.test']] + assert '/ticket/' in msg.body + assert '/order/' not in msg.body + + @pytest.mark.django_db def test_sendmail_attendee_subevent_filter(logged_in_client, sendmail_url, event, item, order, pos): event.settings.attendee_emails_asked = True