Attach receipt PDF to "payment successful" email #96850

Closed
Anna Sirota wants to merge 5 commits from attach-pdf-to-email into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
12 changed files with 431 additions and 186 deletions
Showing only changes of commit a77fcf9c3c - Show all commits

View File

@ -26,7 +26,7 @@ from looper.admin import (
ACCOUNT_INFORMATION_FIELDSET, ACCOUNT_INFORMATION_FIELDSET,
) )
from looper.admin.filters import EmptyFieldListFilter from looper.admin.filters import EmptyFieldListFilter
import blender_fund_main.email as email import blender_fund_main.email
from . import models, forms from . import models, forms
import looper.admin.reports import looper.admin.reports
import looper.forms import looper.forms
@ -464,7 +464,7 @@ class _RenderEmailPreview:
"""Render the "html_message" attribute of this object.""" """Render the "html_message" attribute of this object."""
iframe_template = Template( iframe_template = Template(
'<iframe sandbox="allow-same-origin" style="min-width: 40vw;" srcdoc="{{ body|safe }}"' '<iframe sandbox="allow-same-origin" style="min-width: 40vw;" srcdoc="{{ body|safe }}"'
'onload="this.style.height=(this.contentWindow.document.body.scrollHeight + 16)+\'px\';">' 'onload="this.style.height=(this.contentWindow.document.body.scrollHeight + 16)+\'px\';">' # noqa: E501
'</iframe>' '</iframe>'
) )
@ -603,7 +603,7 @@ class AutomaticPaymentEmailPreviewAdmin(
mail_name = object_id mail_name = object_id
objects_q = looper.models.Order.objects objects_q = looper.models.Order.objects
_template_function = email.construct_payment_mail _template_function = blender_fund_main.email.construct_payment_mail
if mail_name == 'payment_paid': if mail_name == 'payment_paid':
objects_q = objects_q.filter(status='paid') objects_q = objects_q.filter(status='paid')
elif mail_name == 'payment_soft-failed': elif mail_name == 'payment_soft-failed':
@ -612,7 +612,7 @@ class AutomaticPaymentEmailPreviewAdmin(
objects_q = objects_q.filter(status='failed') objects_q = objects_q.filter(status='failed')
elif mail_name == 'donation_received': elif mail_name == 'donation_received':
objects_q = objects_q.filter(status='paid', subscription__isnull=True) objects_q = objects_q.filter(status='paid', subscription__isnull=True)
_template_function = email.construct_donation_received_mail _template_function = blender_fund_main.email.construct_donation_received_mail
else: else:
raise Exception(f'Unknown payment notification email {mail_name}') raise Exception(f'Unknown payment notification email {mail_name}')
obj = default_obj = objects_q.filter(customer__user__isnull=False).first() obj = default_obj = objects_q.filter(customer__user__isnull=False).first()
@ -666,9 +666,9 @@ class MembershipChangedEmailPreviewAdmin(
"""Construct the Email on th fly from known subscription email templates.""" """Construct the Email on th fly from known subscription email templates."""
mail_name = object_id mail_name = object_id
_template_function = email._construct_membership_mail _template_function = blender_fund_main.email.construct_membership_mail
if mail_name == 'managed_memb_notif': if mail_name == 'managed_memb_notif':
_template_function = email._construct_managed_subscription_mail _template_function = blender_fund_main.email.construct_managed_subscription_mail
objects_q = models.Membership.objects objects_q = models.Membership.objects
if mail_name == 'membership_activated': if mail_name == 'membership_activated':

View File

@ -8,4 +8,4 @@ class BlenderFundMainConfig(AppConfig):
def ready(self): def ready(self):
# Import modules to register signal handlers. # Import modules to register signal handlers.
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
from . import email, signals from . import signals

View File

@ -2,72 +2,20 @@ import logging
import typing import typing
from django.conf import settings from django.conf import settings
from django.contrib.sites.shortcuts import get_current_site
import django.core.mail
from django.dispatch import receiver
from django.template import loader from django.template import loader
from django.urls import reverse from django.urls import reverse
import django.core.mail
from . import models, signals from . import models
from looper.pdf import PDFResponse
import looper.models import looper.models
import looper.signals import looper.signals
from blender_fund_main.utils import absolute_url, is_noreply
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def absolute_url(viewname: str, def construct_membership_mail(membership: models.Membership) -> typing.Tuple[str, str, str]:
args: typing.Optional[tuple] = None,
kwargs: typing.Optional[dict] = None) -> str:
"""Same as django.urls.reverse() but then as absolute URL.
For simplicity this assumes HTTPS is used.
"""
from urllib.parse import urljoin
domain = get_current_site(None).domain
relative_url = reverse(viewname, args=args, kwargs=kwargs)
return urljoin(f'https://{domain}/', relative_url)
def is_noreply(email: str) -> bool:
"""Return True if the email address is a no-reply address."""
return email.startswith('noreply@') or email.startswith('no-reply@')
@receiver(signals.membership_activated)
@receiver(signals.membership_cancelled)
@receiver(signals.membership_created_needs_payment)
def send_mail_membership_status_changed(sender: models.Membership, **kwargs):
"""Send out an email notifying about changed status of a membership."""
email = sender.customer.billing_address.email
if is_noreply(email):
log.debug('Not sending membership-changed notification to no-reply address %s', email)
return
try:
email_body_html, email_body_txt, subject = _construct_membership_mail(membership=sender)
log.debug('Sending membership-changed notification to %s', email)
django.core.mail.send_mail(
subject,
message=email_body_txt,
html_message=email_body_html,
from_email=None, # just use the configured default From-address.
recipient_list=[email],
fail_silently=False,
)
except Exception:
# Template rendering errors shouldn't interfere with the Looper clock, so
# catch all errors here.
log.exception('Error sending membership-changed %r notification to %s',
sender.status, email)
else:
log.info('Sent membership-changed notification to %s', email)
def _construct_membership_mail(membership: models.Membership) -> typing.Tuple[str, str, str]:
"""Construct a mail about a membership. """Construct a mail about a membership.
:return: tuple (html, text, subject) :return: tuple (html, text, subject)
@ -120,51 +68,11 @@ def _construct_membership_mail(membership: models.Membership) -> typing.Tuple[st
return email_body_html, email_body_txt, context['subject'] return email_body_html, email_body_txt, context['subject']
@receiver(looper.signals.automatic_payment_succesful)
@receiver(looper.signals.automatic_payment_soft_failed)
@receiver(looper.signals.automatic_payment_failed)
def automatic_payment_performed(sender: looper.models.Order,
transaction: looper.models.Transaction,
**kwargs):
"""Send out an email notifying about the status of an automated payment."""
email = sender.email
if is_noreply(email):
log.debug('Not sending payment notification to no-reply address %s', email)
return
try:
email_body_html, email_body_txt, subject = construct_payment_mail(
sender=sender, transaction=transaction
)
log.debug('Sending payment %r notification to %s', sender.status, email)
msg = django.core.mail.EmailMultiAlternatives(
subject=subject,
body=email_body_txt,
from_email=None, # just use the configured default From-address.
to=[email],
)
file_data = b'TODO'
file_name = f'blender-development-fund-receipt-{sender.display_number}.pdf'
print(file_name)
file_data = PDFResponse().rendered_content
msg.attach(file_name, file_data, 'application/pdf')
msg.attach_alternative(email_body_html, 'text/html')
msg.send(fail_silently=False)
except Exception:
# Template rendering errors shouldn't interfere with the Looper clock, so
# catch all errors here.
log.exception('Error sending payment %r notification to %s',
sender.status, email)
else:
log.info('Sent %r notification to %s', sender.status, email)
def construct_payment_mail( def construct_payment_mail(
sender: looper.models.Order, transaction: looper.models.Transaction order: looper.models.Order, transaction: looper.models.Transaction
) -> typing.Tuple[str, str, str]: ) -> typing.Tuple[str, str, str]:
membership = sender.subscription.membership membership = order.subscription.membership
customer = sender.customer customer = order.customer
user = customer.user user = customer.user
membership_url = link_membership_url = None membership_url = link_membership_url = None
@ -177,15 +85,15 @@ def construct_payment_mail(
membership_url = absolute_url( membership_url = absolute_url(
'settings_membership_edit', kwargs={'membership_id': membership.pk} 'settings_membership_edit', kwargs={'membership_id': membership.pk}
) )
pay_url = absolute_url('looper:checkout_existing_order', kwargs={'order_id': sender.pk}) pay_url = absolute_url('looper:checkout_existing_order', kwargs={'order_id': order.pk})
receipt_url = absolute_url('settings_receipt', kwargs={'order_id': sender.pk}) receipt_url = absolute_url('settings_receipt', kwargs={'order_id': order.pk})
context = { context = {
'user': user, 'user': user,
'customer': customer, 'customer': customer,
'full_name': customer.billing_address.full_name, 'full_name': customer.billing_address.full_name,
'email': customer.billing_address.email, 'email': customer.billing_address.email,
'order': sender, 'order': order,
'membership': membership, 'membership': membership,
'pay_url': pay_url, 'pay_url': pay_url,
'receipt_url': receipt_url, 'receipt_url': receipt_url,
@ -197,23 +105,23 @@ def construct_payment_mail(
} }
subject: str = loader.render_to_string( subject: str = loader.render_to_string(
f'blender_fund_main/emails/payment_{sender.status}_subject.txt', context).strip() f'blender_fund_main/emails/payment_{order.status}_subject.txt', context).strip()
context['subject'] = subject context['subject'] = subject
email_body_html = loader.render_to_string( email_body_html = loader.render_to_string(
f'blender_fund_main/emails/payment_{sender.status}.html', context) f'blender_fund_main/emails/payment_{order.status}.html', context)
email_body_txt = loader.render_to_string( email_body_txt = loader.render_to_string(
f'blender_fund_main/emails/payment_{sender.status}.txt', context) f'blender_fund_main/emails/payment_{order.status}.txt', context)
return email_body_html, email_body_txt, context['subject'] return email_body_html, email_body_txt, context['subject']
def _construct_managed_subscription_mail(sender: looper.models.Subscription): def construct_managed_subscription_mail(subscription: looper.models.Subscription):
customer = sender.customer customer = subscription.customer
user = customer.user user = customer.user
membership = sender.membership membership = subscription.membership
subs_admin_url = absolute_url('admin:looper_subscription_change', subs_admin_url = absolute_url('admin:looper_subscription_change',
kwargs={'object_id': sender.id}) kwargs={'object_id': subscription.id})
memb_admin_url = absolute_url('admin:blender_fund_main_membership_change', memb_admin_url = absolute_url('admin:blender_fund_main_membership_change',
kwargs={'object_id': membership.id}) kwargs={'object_id': membership.id})
context = { context = {
@ -221,7 +129,7 @@ def _construct_managed_subscription_mail(sender: looper.models.Subscription):
'customer': customer, 'customer': customer,
'full_name': settings.LOOPER_MANAGER_MAIL, 'full_name': settings.LOOPER_MANAGER_MAIL,
'email': settings.LOOPER_MANAGER_MAIL, 'email': settings.LOOPER_MANAGER_MAIL,
'subscription': sender, 'subscription': subscription,
'membership': membership, 'membership': membership,
'subs_admin_url': subs_admin_url, 'subs_admin_url': subs_admin_url,
'memb_admin_url': memb_admin_url, 'memb_admin_url': memb_admin_url,
@ -239,44 +147,6 @@ def _construct_managed_subscription_mail(sender: looper.models.Subscription):
return email_body_html, email_body_txt, context['subject'] return email_body_html, email_body_txt, context['subject']
@receiver(looper.signals.managed_subscription_notification)
def managed_subscription_notification(sender: looper.models.Subscription,
**kwargs):
"""Send out an email notifying a manager about an expiring managed subscription."""
my_log = log.getChild('managed_subscription_notification')
email = settings.LOOPER_MANAGER_MAIL
membership = sender.membership
my_log.debug('Notifying %s about managed subscription %r passing its next_payment date',
email, sender.pk)
try:
email_body_html, email_body_txt, subject = _construct_managed_subscription_mail(
sender=sender
)
except Exception as ex:
# Template rendering errors shouldn't interfere with the Looper clock, so
# catch all errors here.
my_log.exception('Error rendering templates to send notification about managed '
'membership %r to %s: %s', membership.pk, email, ex)
return
try:
django.core.mail.send_mail(
subject,
message=email_body_txt,
html_message=email_body_html,
from_email=None, # just use the configured default From-address.
recipient_list=[email],
fail_silently=False,
)
except OSError as ex:
my_log.exception('Error sending notification mail about managed '
'membership %r to %s: %s', membership.pk, email, ex)
else:
my_log.info('Notified %s about managed subscription %r passing its next_payment date',
email, sender.pk)
def construct_donation_received_mail(order): def construct_donation_received_mail(order):
customer = order.customer customer = order.customer
user = customer.user user = customer.user

View File

@ -13,6 +13,9 @@ import looper.signals
import looper.models import looper.models
from . import models from . import models
import blender_fund_main.tasks as tasks
User = get_user_model() User = get_user_model()
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -344,3 +347,26 @@ def _grant_campaign_badges(sender, instance: models.Campaign, **kwargs):
if action != 'post_add' or not pk_set: if action != 'post_add' or not pk_set:
return return
instance.grant_badges(order_pks=pk_set) instance.grant_badges(order_pks=pk_set)
@receiver(membership_activated)
@receiver(membership_cancelled)
@receiver(membership_created_needs_payment)
def _on_membership_status_changed(sender: models.Membership, **kwargs):
tasks.send_mail_membership_status_changed(membership_id=sender.pk)
@receiver(looper.signals.automatic_payment_succesful)
@receiver(looper.signals.automatic_payment_soft_failed)
@receiver(looper.signals.automatic_payment_failed)
def _on_automatic_payment_performed(
sender: looper.models.Order,
transaction: looper.models.Transaction,
**kwargs,
):
tasks.send_mail_automatic_payment_performed(order_id=sender.pk, transaction_id=transaction.pk)
@receiver(looper.signals.managed_subscription_notification)
def _on_managed_subscription_notification(sender: looper.models.Subscription, **kwargs):
tasks.send_mail_managed_subscription_notification(subscription_id=sender.pk)

View File

@ -6,18 +6,28 @@ import sys
from background_task import background from background_task import background
from background_task.models import Task from background_task.models import Task
from background_task.tasks import TaskSchedule from background_task.tasks import TaskSchedule
from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db.transaction import atomic from django.db.transaction import atomic
import django.core.mail
import looper.stripe_utils from looper.pdf import PDFResponse
from looper.stripe_utils import ( from looper.stripe_utils import (
upsert_order_from_payment_intent_and_product, upsert_order_from_payment_intent_and_product,
upsert_subscription_payment_method_from_setup_intent, upsert_subscription_payment_method_from_setup_intent,
) )
import looper.models import looper.models
import looper.stripe_utils
import stripe import stripe
import blender_fund_main.email as email from blender_fund_main.utils import is_noreply
from blender_fund_main.email import (
construct_managed_subscription_mail,
construct_membership_mail,
construct_payment_mail,
donation_received,
)
import blender_fund_main.models
User = get_user_model() User = get_user_model()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -62,7 +72,7 @@ def handle_payment_intent_succeeded(payload: str):
) )
new_status = order.status new_status = order.status
if order.subscription is None and old_status != new_status and new_status == 'paid': if order.subscription is None and old_status != new_status and new_status == 'paid':
email.donation_received(order) donation_received(order)
@background() @background()
@ -106,3 +116,89 @@ def grant_or_revoke_badge(user_id: int, grant: str = '', revoke: str = ''):
return return
user = User.objects.get(pk=user_id) user = User.objects.get(pk=user_id)
badges.change_badge(user=user, revoke=revoke, grant=grant) badges.change_badge(user=user, revoke=revoke, grant=grant)
@background(schedule={'action': TaskSchedule.RESCHEDULE_EXISTING})
def send_mail_automatic_payment_performed(order_id: int, transaction_id: int):
"""Send out an email notifying about the status of an automated payment."""
order = looper.models.Order.objects.get(pk=order_id)
transaction = looper.models.Transaction.objects.get(pk=transaction_id)
email = order.email
if is_noreply(email):
logger.debug('Not sending payment notification to no-reply address %s', email)
return
email_body_html, email_body_txt, subject = construct_payment_mail(
order=order, transaction=transaction
)
logger.debug('Sending payment %r notification to %s', order.status, email)
msg = django.core.mail.EmailMultiAlternatives(
subject=subject,
body=email_body_txt,
from_email=None, # just use the configured default From-address.
to=[email],
)
file_data = b'TODO'
file_name = f'blender-development-fund-receipt-{order.display_number}.pdf'
print(file_name)
file_data = PDFResponse().rendered_content
msg.attach(file_name, file_data, 'application/pdf')
msg.attach_alternative(email_body_html, 'text/html')
msg.send(fail_silently=False)
logger.info('Sent %r notification to %s', order.status, email)
@background(schedule={'action': TaskSchedule.RESCHEDULE_EXISTING})
def send_mail_membership_status_changed(membership_id: int):
"""Send out an email notifying about changed status of a membership."""
membership = blender_fund_main.models.Membership.objects.get(pk=membership_id)
email = membership.customer.billing_address.email
if is_noreply(email):
logger.debug('Not sending membership-changed notification to no-reply address %s', email)
return
email_body_html, email_body_txt, subject = construct_membership_mail(
membership=membership
)
logger.debug('Sending membership-changed notification to %s', email)
django.core.mail.send_mail(
subject,
message=email_body_txt,
html_message=email_body_html,
from_email=None, # just use the configured default From-address.
recipient_list=[email],
fail_silently=False,
)
logger.info('Sent membership-changed notification to %s', email)
@background(schedule={'action': TaskSchedule.RESCHEDULE_EXISTING})
def send_mail_managed_subscription_notification(subscription_id: int):
"""Send out an email notifying a manager about an expiring managed subscription."""
subscription = looper.models.Subscription.objects.get(pk=subscription_id)
email = settings.LOOPER_MANAGER_MAIL
logger.debug(
'Notifying %s about managed subscription %r passing its next_payment date',
email,
subscription_id,
)
email_body_html, email_body_txt, subject = construct_managed_subscription_mail(
subscription=subscription
)
django.core.mail.send_mail(
subject,
message=email_body_txt,
html_message=email_body_html,
from_email=None, # just use the configured default From-address.
recipient_list=[email],
fail_silently=False,
)
logger.info(
'Notified %s about managed subscription %r passing its next_payment date',
email,
subscription_id,
)

View File

@ -1,13 +1,183 @@
import django.core.mail from unittest.mock import patch
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse from django.urls import reverse
import django.core.mail
from blender_fund_main.email import absolute_url
from looper.models import Subscription from looper.models import Subscription
from looper.tests import AbstractBaseTestCase from looper.tests import AbstractBaseTestCase
from ..models import Membership from ..models import Membership
from blender_fund_main.utils import absolute_url, html_to_text
import blender_fund_main.tasks as tasks
expected_mail_body = '''Dear Erik von Namenstein,
Thank you for joining the Blender Development Fund. You chose to pay
for your Silver membership by bank transfer, which
means that we will be waiting for you to perform this transfer.
Your membership will be activated as soon as we have handled your bank transfer.
When paying, please mention the following:
Blender Fund Membership subs-1
Please send your payment of  10.00 to:
Stichting Blender Foundation
Buikslotermeerplein 161
1025 ET Amsterdam, the Netherlands
Bank: ING Bank
Bijlmerdreef 109
1102 BW Amsterdam, the Netherlands
BIC/Swift code: INGB NL 2A
IBAN: NL45 INGB 0009356121 (for Euro countries)
Tax number NL 8111.66.223
You can always go to https://example.com/settings/membership/1 to view and update your membership.
--
Kind regards,
Blender Foundation
'''
expected_mail_html_text = '''Blender Development Fund membership Bank Payment
Dear Erik von Namenstein,
Thank you for joining the Blender Development Fund. You chose to pay
for your Silver membership by bank transfer, which
means that we will be waiting for you to perform this transfer.
Your membership will be activated as soon as we have handled your bank transfer.
When paying, please mention the following:
Blender Fund Membership subs-1
Please send your payment of  10.00 to:
Stichting Blender Foundation
Buikslotermeerplein 161
1025 ET Amsterdam, the Netherlands
Bank: ING Bank
Bijlmerdreef 109
1102 BW Amsterdam, the Netherlands
BIC/Swift code: INGB NL 2A
IBAN: NL45 INGB 0009356121 (for Euro countries)
Tax number NL 8111.66.223
You can always go to https://example.com/settings/membership/1 to view and update your membership.
--
Kind regards,
Blender Foundation'''
expected_mail_w_token_body = '''Dear Erik von Namenstein,
Thank you for joining the Blender Development Fund. You chose to pay
for your Silver membership by bank transfer, which
means that we will be waiting for you to perform this transfer.
One more step: **to manage or cancel this membership** and claim your Silver badge,
follow the link below and sign in with your Blender ID.
https://example.com/link-membership/{token}/
Your membership will be activated as soon as we have handled your bank transfer.
When paying, please mention the following:
Blender Fund Membership subs-1
Please send your payment of  10.00 to:
Stichting Blender Foundation
Buikslotermeerplein 161
1025 ET Amsterdam, the Netherlands
Bank: ING Bank
Bijlmerdreef 109
1102 BW Amsterdam, the Netherlands
BIC/Swift code: INGB NL 2A
IBAN: NL45 INGB 0009356121 (for Euro countries)
Tax number NL 8111.66.223
--
Kind regards,
Blender Foundation
'''
expected_mail_w_token_html_text = '''Blender Development Fund membership Bank Payment
Dear Erik von Namenstein,
Thank you for joining the Blender Development Fund. You chose to pay
for your Silver membership by bank transfer, which
means that we will be waiting for you to perform this transfer.
One more step: to manage or cancel this membership and claim your Silver badge,
follow the link below and sign in with your Blender ID.
https://example.com/link-membership/{token}/
Your membership will be activated as soon as we have handled your bank transfer.
When paying, please mention the following:
Blender Fund Membership subs-1
Please send your payment of  10.00 to:
Stichting Blender Foundation
Buikslotermeerplein 161
1025 ET Amsterdam, the Netherlands
Bank: ING Bank
Bijlmerdreef 109
1102 BW Amsterdam, the Netherlands
BIC/Swift code: INGB NL 2A
IBAN: NL45 INGB 0009356121 (for Euro countries)
Tax number NL 8111.66.223
--
Kind regards,
Blender Foundation'''
@patch(
'blender_fund_main.tasks.send_mail_membership_status_changed',
new=tasks.send_mail_membership_status_changed.task_function,
)
class CheckoutTestCase(AbstractBaseTestCase): class CheckoutTestCase(AbstractBaseTestCase):
fixtures = ['gateways', 'devfund', 'systemuser'] fixtures = ['gateways', 'devfund', 'systemuser']
@ -60,6 +230,12 @@ class CheckoutTestCase(AbstractBaseTestCase):
self.assertIn(edit_url, the_mail.body) self.assertIn(edit_url, the_mail.body)
alt0_body, alt0_type = the_mail.alternatives[0] alt0_body, alt0_type = the_mail.alternatives[0]
self.assertEqual(alt0_type, 'text/html') self.assertEqual(alt0_type, 'text/html')
self.assertEqual(the_mail.body, expected_mail_body, the_mail.body)
self.assertEqual(
html_to_text(alt0_body),
expected_mail_html_text,
html_to_text(the_mail.alternatives[0][0]),
)
def test_bank_creates_inactive_membership_sends_email_without_an_account(self): def test_bank_creates_inactive_membership_sends_email_without_an_account(self):
membership, the_mail = self._test_bank_creates_inactive_membership() membership, the_mail = self._test_bank_creates_inactive_membership()
@ -71,8 +247,21 @@ class CheckoutTestCase(AbstractBaseTestCase):
kwargs={'membership_id': membership.id}) kwargs={'membership_id': membership.id})
self.assertNotIn(edit_url, the_mail.body) self.assertNotIn(edit_url, the_mail.body)
# but claim link should # but claim link should
token = membership.customer.token.token
link_membership_url = absolute_url( link_membership_url = absolute_url(
'link_membership', 'link_membership',
kwargs={'token': membership.customer.token.token}, kwargs={'token': token},
) )
self.assertIn(link_membership_url, the_mail.body) self.assertIn(link_membership_url, the_mail.body)
self.assertEqual(
the_mail.body,
expected_mail_w_token_body.format(token=token),
the_mail.body,
)
alt0_body, alt0_type = the_mail.alternatives[0]
self.assertEqual(alt0_type, 'text/html')
self.assertEqual(
html_to_text(alt0_body),
expected_mail_w_token_html_text.format(token=token),
html_to_text(alt0_body),
)

View File

@ -15,6 +15,7 @@ from looper.tests.factories import SubscriptionFactory, create_customer_with_bil
import looper.exceptions import looper.exceptions
from blender_fund_main.utils import html_to_text from blender_fund_main.utils import html_to_text
import blender_fund_main.tasks as tasks
expected_managed_email_subj = 'Blender Development Fund managed membership needs attention' expected_managed_email_subj = 'Blender Development Fund managed membership needs attention'
@ -68,7 +69,7 @@ You can always go to https://fund.local:8010/settings/membership/1 to view and u
-- --
Kind regards, Kind regards,
Blender Foundation Blender Foundation
''' ''' # noqa: E501
expected_soft_failed_email_html_stripped = '''Blender Development Fund: payment failed (but we'll try again) expected_soft_failed_email_html_stripped = '''Blender Development Fund: payment failed (but we'll try again)
@ -99,7 +100,7 @@ You can always go to https://fund.local:8010/settings/membership/1 to view and u
-- --
Kind regards, Kind regards,
Blender Foundation''' Blender Foundation''' # noqa: E501
expected_failed_email_body = '''Dear Jane Doe, expected_failed_email_body = '''Dear Jane Doe,
@ -116,7 +117,7 @@ You can always go to https://fund.local:8010/settings/membership/1 to view and u
-- --
Kind regards, Kind regards,
Blender Foundation Blender Foundation
''' ''' # noqa: E501
expected_failed_email_html_stripped = '''Blender Development Fund: payment failed expected_failed_email_html_stripped = '''Blender Development Fund: payment failed
@ -144,7 +145,7 @@ You can always go to https://fund.local:8010/settings/membership/1 to view and u
-- --
Kind regards, Kind regards,
Blender Foundation''' Blender Foundation''' # noqa: E501
expected_payment_received_email_body = '''Dear Jane Doe, expected_payment_received_email_body = '''Dear Jane Doe,
Automatic payment of your Blender Development Fund membership ( 10.00) Automatic payment of your Blender Development Fund membership ( 10.00)
@ -203,7 +204,17 @@ def _write_mail(mail):
f.write(str(content)) f.write(str(content))
@patch(
'blender_fund_main.tasks.send_mail_automatic_payment_performed',
new=tasks.send_mail_automatic_payment_performed.task_function,
)
@patch(
'blender_fund_main.tasks.send_mail_managed_subscription_notification',
new=tasks.send_mail_managed_subscription_notification.task_function,
)
class TestClockEmails(AbstractLooperTestCase): class TestClockEmails(AbstractLooperTestCase):
fixtures = AbstractLooperTestCase.fixtures + ['default_site']
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.subscription = self._create_membership_due_now() self.subscription = self._create_membership_due_now()
@ -286,8 +297,8 @@ class TestClockEmails(AbstractLooperTestCase):
customer = self.subscription.customer customer = self.subscription.customer
user = customer.user user = customer.user
_write_mail(mail) _write_mail(mail)
self.assertEqual(len(mail.outbox), 2) self.assertEqual(len(mail.outbox), 1)
email = mail.outbox[-1] email = mail.outbox[0]
self.assertEqual(email.to, [user.customer.billing_address.email]) self.assertEqual(email.to, [user.customer.billing_address.email])
# TODO(anna): set the correct reply_to # TODO(anna): set the correct reply_to
self.assertEqual(email.reply_to, []) self.assertEqual(email.reply_to, [])
@ -351,9 +362,9 @@ class TestClockEmails(AbstractLooperTestCase):
# Check that an email notification is sent # Check that an email notification is sent
customer = self.subscription.customer customer = self.subscription.customer
user = customer.user user = customer.user
self.assertEqual(len(mail.outbox), 2) self.assertEqual(len(mail.outbox), 1)
_write_mail(mail) _write_mail(mail)
email = mail.outbox[-1] email = mail.outbox[0]
self.assertEqual(email.to, [user.customer.billing_address.email]) self.assertEqual(email.to, [user.customer.billing_address.email])
# TODO(anna): set the correct reply_to # TODO(anna): set the correct reply_to
self.assertEqual(email.reply_to, []) self.assertEqual(email.reply_to, [])
@ -412,8 +423,8 @@ class TestClockEmails(AbstractLooperTestCase):
customer = self.subscription.customer customer = self.subscription.customer
user = customer.user user = customer.user
_write_mail(mail) _write_mail(mail)
self.assertEqual(len(mail.outbox), 2) self.assertEqual(len(mail.outbox), 1)
email = mail.outbox[-1] email = mail.outbox[0]
self.assertEqual(email.to, [user.customer.billing_address.email]) self.assertEqual(email.to, [user.customer.billing_address.email])
# TODO(anna): set the correct reply_to # TODO(anna): set the correct reply_to
self.assertEqual(email.reply_to, []) self.assertEqual(email.reply_to, [])
@ -446,8 +457,8 @@ class TestClockEmails(AbstractLooperTestCase):
# Check that an email notification is sent # Check that an email notification is sent
_write_mail(mail) _write_mail(mail)
self.assertEqual(len(mail.outbox), 2) self.assertEqual(len(mail.outbox), 1)
email = mail.outbox[-1] email = mail.outbox[0]
self.assertEqual(email.to, ['admin@example.com']) self.assertEqual(email.to, ['admin@example.com'])
self.assertEqual(email.from_email, 'webmaster@localhost') self.assertEqual(email.from_email, 'webmaster@localhost')
self.assertEqual(email.subject, expected_managed_email_subj) self.assertEqual(email.subject, expected_managed_email_subj)

View File

@ -137,6 +137,8 @@ class LinkMembershipTest(AbstractLooperTestCase):
self.assertIsNone(order.subscription_id) self.assertIsNone(order.subscription_id)
self.assertEqual(order.customer_id, accountless_customer_id) self.assertEqual(order.customer_id, accountless_customer_id)
some_user = User.objects.create(email='joe@example.com', username='newnickname') some_user = User.objects.create(email='joe@example.com', username='newnickname')
tasks_q = background_task.models.Task.objects.order_by('-pk')
tasks_before = tasks_q.count()
self.client.force_login(some_user) self.client.force_login(some_user)
response = self.client.post(self.url, data={'agree_to_link_membership': True}) response = self.client.post(self.url, data={'agree_to_link_membership': True})
@ -148,18 +150,17 @@ class LinkMembershipTest(AbstractLooperTestCase):
# Successfully claiming a membership should result in a redirect to account settings # Successfully claiming a membership should result in a redirect to account settings
self.subscription.refresh_from_db() self.subscription.refresh_from_db()
tasks_q = background_task.models.Task.objects.all() self.assertEqual(tasks_q.count(), tasks_before + 2)
self.assertEqual(tasks_q.count(), 2)
# A membership badge was grated to the account based on membership level # A membership badge was grated to the account based on membership level
self.assertEqual(tasks_q[0].task_name, 'blender_fund_main.tasks.grant_or_revoke_badge')
self.assertEqual(
tasks_q[0].task_params,
f'[[], {{"grant": "devfund_gold", "revoke": "", "user_id": {some_user.pk}}}]',
)
# A campaign badge was grated to the account based on an campaign order
self.assertEqual(tasks_q[1].task_name, 'blender_fund_main.tasks.grant_or_revoke_badge') self.assertEqual(tasks_q[1].task_name, 'blender_fund_main.tasks.grant_or_revoke_badge')
self.assertEqual( self.assertEqual(
tasks_q[1].task_params, tasks_q[1].task_params,
f'[[], {{"grant": "devfund_gold", "revoke": "", "user_id": {some_user.pk}}}]',
)
# A campaign badge was grated to the account based on an campaign order
self.assertEqual(tasks_q[0].task_name, 'blender_fund_main.tasks.grant_or_revoke_badge')
self.assertEqual(
tasks_q[0].task_params,
f'[[], {{"grant": "campaign-badge", "user_id": {some_user.pk}}}]', f'[[], {{"grant": "campaign-badge", "user_id": {some_user.pk}}}]',
) )

View File

@ -1,11 +1,23 @@
from unittest.mock import patch
from django.contrib.auth.models import User from django.contrib.auth.models import User
import django.core.mail import django.core.mail
from blender_fund_main.email import absolute_url
from looper.tests import AbstractLooperTestCase from looper.tests import AbstractLooperTestCase
import looper.signals import looper.signals
from blender_fund_main.utils import absolute_url
import blender_fund_main.tasks as tasks
@patch(
'blender_fund_main.tasks.send_mail_managed_subscription_notification',
new=tasks.send_mail_managed_subscription_notification.task_function,
)
@patch(
'blender_fund_main.tasks.send_mail_membership_status_changed',
new=tasks.send_mail_membership_status_changed.task_function,
)
class ManagedMembershipNotificationMailTest(AbstractLooperTestCase): class ManagedMembershipNotificationMailTest(AbstractLooperTestCase):
fixtures = ['default_site', 'gateways', 'devfund', 'testuser', 'systemuser'] fixtures = ['default_site', 'gateways', 'devfund', 'testuser', 'systemuser']

View File

@ -1,15 +1,22 @@
from unittest.mock import patch
import logging import logging
import django.core.mail
from django.dispatch import receiver from django.dispatch import receiver
from django.urls import reverse from django.urls import reverse
import django.core.mail
from looper.tests import AbstractLooperTestCase from looper.tests import AbstractLooperTestCase
from .. import models, signals from .. import models, signals
import blender_fund_main.tasks as tasks
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@patch(
'blender_fund_main.tasks.send_mail_membership_status_changed',
new=tasks.send_mail_membership_status_changed.task_function,
)
class MembershipActivationSignalTest(AbstractLooperTestCase): class MembershipActivationSignalTest(AbstractLooperTestCase):
fixtures = AbstractLooperTestCase.fixtures + ['default_site', 'systemuser'] fixtures = AbstractLooperTestCase.fixtures + ['default_site', 'systemuser']

View File

@ -1,11 +1,18 @@
from unittest.mock import patch
from django.contrib.auth.models import User from django.contrib.auth.models import User
import django.core.mail
from django.dispatch import receiver from django.dispatch import receiver
from django.test import override_settings import django.core.mail
from looper.tests import AbstractBaseTestCase from looper.tests import AbstractBaseTestCase
import blender_fund_main.tasks as tasks
@patch(
'blender_fund_main.tasks.send_mail_membership_status_changed',
new=tasks.send_mail_membership_status_changed.task_function,
)
class MembershipActivationSignalTest(AbstractBaseTestCase): class MembershipActivationSignalTest(AbstractBaseTestCase):
fixtures = ['default_site', 'gateways', 'devfund'] fixtures = ['default_site', 'gateways', 'devfund']

View File

@ -1,6 +1,32 @@
import typing
from html.parser import HTMLParser from html.parser import HTMLParser
import re import re
from django.conf import settings
from django.contrib.sites.shortcuts import get_current_site
from django.urls import reverse
def absolute_url(viewname: str,
args: typing.Optional[tuple] = None,
kwargs: typing.Optional[dict] = None) -> str:
"""Same as django.urls.reverse() but then as absolute URL.
For simplicity this assumes HTTPS is used, unless in DEBUG mode.
"""
from urllib.parse import urljoin
proto = 'http' if settings.DEBUG else 'https'
domain = get_current_site(None).domain
relative_url = reverse(viewname, args=args, kwargs=kwargs)
return urljoin(f'{proto}://{domain}/', relative_url)
def is_noreply(email: str) -> bool:
"""Return True if the email address is a no-reply address."""
return email.startswith('noreply@') or email.startswith('no-reply@')
class HTMLFilter(HTMLParser): class HTMLFilter(HTMLParser):
skip_text_of = ('a', 'style') skip_text_of = ('a', 'style')