Attach receipt PDF to "payment successful" email #96850
@ -26,7 +26,7 @@ from looper.admin import (
|
||||
ACCOUNT_INFORMATION_FIELDSET,
|
||||
)
|
||||
from looper.admin.filters import EmptyFieldListFilter
|
||||
import blender_fund_main.email as email
|
||||
import blender_fund_main.email
|
||||
from . import models, forms
|
||||
import looper.admin.reports
|
||||
import looper.forms
|
||||
@ -464,7 +464,7 @@ class _RenderEmailPreview:
|
||||
"""Render the "html_message" attribute of this object."""
|
||||
iframe_template = Template(
|
||||
'<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>'
|
||||
)
|
||||
|
||||
@ -603,7 +603,7 @@ class AutomaticPaymentEmailPreviewAdmin(
|
||||
|
||||
mail_name = object_id
|
||||
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':
|
||||
objects_q = objects_q.filter(status='paid')
|
||||
elif mail_name == 'payment_soft-failed':
|
||||
@ -612,7 +612,7 @@ class AutomaticPaymentEmailPreviewAdmin(
|
||||
objects_q = objects_q.filter(status='failed')
|
||||
elif mail_name == 'donation_received':
|
||||
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:
|
||||
raise Exception(f'Unknown payment notification email {mail_name}')
|
||||
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."""
|
||||
|
||||
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':
|
||||
_template_function = email._construct_managed_subscription_mail
|
||||
_template_function = blender_fund_main.email.construct_managed_subscription_mail
|
||||
|
||||
objects_q = models.Membership.objects
|
||||
if mail_name == 'membership_activated':
|
||||
|
@ -8,4 +8,4 @@ class BlenderFundMainConfig(AppConfig):
|
||||
def ready(self):
|
||||
# Import modules to register signal handlers.
|
||||
# noinspection PyUnresolvedReferences
|
||||
from . import email, signals
|
||||
from . import signals
|
||||
|
@ -2,72 +2,20 @@ import logging
|
||||
import typing
|
||||
|
||||
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.urls import reverse
|
||||
import django.core.mail
|
||||
|
||||
from . import models, signals
|
||||
from looper.pdf import PDFResponse
|
||||
from . import models
|
||||
import looper.models
|
||||
import looper.signals
|
||||
|
||||
from blender_fund_main.utils import absolute_url, is_noreply
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
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]:
|
||||
def construct_membership_mail(membership: models.Membership) -> typing.Tuple[str, str, str]:
|
||||
"""Construct a mail about a membership.
|
||||
|
||||
: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']
|
||||
|
||||
|
||||
@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(
|
||||
sender: looper.models.Order, transaction: looper.models.Transaction
|
||||
order: looper.models.Order, transaction: looper.models.Transaction
|
||||
) -> typing.Tuple[str, str, str]:
|
||||
membership = sender.subscription.membership
|
||||
customer = sender.customer
|
||||
membership = order.subscription.membership
|
||||
customer = order.customer
|
||||
user = customer.user
|
||||
|
||||
membership_url = link_membership_url = None
|
||||
@ -177,15 +85,15 @@ def construct_payment_mail(
|
||||
membership_url = absolute_url(
|
||||
'settings_membership_edit', kwargs={'membership_id': membership.pk}
|
||||
)
|
||||
pay_url = absolute_url('looper:checkout_existing_order', kwargs={'order_id': sender.pk})
|
||||
receipt_url = absolute_url('settings_receipt', 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': order.pk})
|
||||
|
||||
context = {
|
||||
'user': user,
|
||||
'customer': customer,
|
||||
'full_name': customer.billing_address.full_name,
|
||||
'email': customer.billing_address.email,
|
||||
'order': sender,
|
||||
'order': order,
|
||||
'membership': membership,
|
||||
'pay_url': pay_url,
|
||||
'receipt_url': receipt_url,
|
||||
@ -197,23 +105,23 @@ def construct_payment_mail(
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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(
|
||||
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']
|
||||
|
||||
|
||||
def _construct_managed_subscription_mail(sender: looper.models.Subscription):
|
||||
customer = sender.customer
|
||||
def construct_managed_subscription_mail(subscription: looper.models.Subscription):
|
||||
customer = subscription.customer
|
||||
user = customer.user
|
||||
membership = sender.membership
|
||||
membership = subscription.membership
|
||||
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',
|
||||
kwargs={'object_id': membership.id})
|
||||
context = {
|
||||
@ -221,7 +129,7 @@ def _construct_managed_subscription_mail(sender: looper.models.Subscription):
|
||||
'customer': customer,
|
||||
'full_name': settings.LOOPER_MANAGER_MAIL,
|
||||
'email': settings.LOOPER_MANAGER_MAIL,
|
||||
'subscription': sender,
|
||||
'subscription': subscription,
|
||||
'membership': membership,
|
||||
'subs_admin_url': subs_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']
|
||||
|
||||
|
||||
@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):
|
||||
customer = order.customer
|
||||
user = customer.user
|
||||
|
@ -13,6 +13,9 @@ import looper.signals
|
||||
import looper.models
|
||||
from . import models
|
||||
|
||||
import blender_fund_main.tasks as tasks
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
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:
|
||||
return
|
||||
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)
|
||||
|
@ -6,18 +6,28 @@ import sys
|
||||
from background_task import background
|
||||
from background_task.models import Task
|
||||
from background_task.tasks import TaskSchedule
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.transaction import atomic
|
||||
import django.core.mail
|
||||
|
||||
import looper.stripe_utils
|
||||
from looper.pdf import PDFResponse
|
||||
from looper.stripe_utils import (
|
||||
upsert_order_from_payment_intent_and_product,
|
||||
upsert_subscription_payment_method_from_setup_intent,
|
||||
)
|
||||
import looper.models
|
||||
import looper.stripe_utils
|
||||
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()
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -62,7 +72,7 @@ def handle_payment_intent_succeeded(payload: str):
|
||||
)
|
||||
new_status = order.status
|
||||
if order.subscription is None and old_status != new_status and new_status == 'paid':
|
||||
email.donation_received(order)
|
||||
donation_received(order)
|
||||
|
||||
|
||||
@background()
|
||||
@ -106,3 +116,89 @@ def grant_or_revoke_badge(user_id: int, grant: str = '', revoke: str = ''):
|
||||
return
|
||||
user = User.objects.get(pk=user_id)
|
||||
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,
|
||||
)
|
||||
|
@ -1,13 +1,183 @@
|
||||
import django.core.mail
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
import django.core.mail
|
||||
|
||||
from blender_fund_main.email import absolute_url
|
||||
from looper.models import Subscription
|
||||
from looper.tests import AbstractBaseTestCase
|
||||
|
||||
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):
|
||||
fixtures = ['gateways', 'devfund', 'systemuser']
|
||||
|
||||
@ -60,6 +230,12 @@ class CheckoutTestCase(AbstractBaseTestCase):
|
||||
self.assertIn(edit_url, the_mail.body)
|
||||
alt0_body, alt0_type = the_mail.alternatives[0]
|
||||
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):
|
||||
membership, the_mail = self._test_bank_creates_inactive_membership()
|
||||
@ -71,8 +247,21 @@ class CheckoutTestCase(AbstractBaseTestCase):
|
||||
kwargs={'membership_id': membership.id})
|
||||
self.assertNotIn(edit_url, the_mail.body)
|
||||
# but claim link should
|
||||
token = membership.customer.token.token
|
||||
link_membership_url = absolute_url(
|
||||
'link_membership',
|
||||
kwargs={'token': membership.customer.token.token},
|
||||
kwargs={'token': token},
|
||||
)
|
||||
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),
|
||||
)
|
||||
|
@ -15,6 +15,7 @@ from looper.tests.factories import SubscriptionFactory, create_customer_with_bil
|
||||
import looper.exceptions
|
||||
|
||||
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'
|
||||
@ -68,7 +69,7 @@ You can always go to https://fund.local:8010/settings/membership/1 to view and u
|
||||
--
|
||||
Kind regards,
|
||||
Blender Foundation
|
||||
'''
|
||||
''' # noqa: E501
|
||||
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,
|
||||
Blender Foundation'''
|
||||
Blender Foundation''' # noqa: E501
|
||||
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,
|
||||
Blender Foundation
|
||||
'''
|
||||
''' # noqa: E501
|
||||
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,
|
||||
Blender Foundation'''
|
||||
Blender Foundation''' # noqa: E501
|
||||
expected_payment_received_email_body = '''Dear Jane Doe,
|
||||
|
||||
Automatic payment of your Blender Development Fund membership (€ 10.00)
|
||||
@ -203,7 +204,17 @@ def _write_mail(mail):
|
||||
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):
|
||||
fixtures = AbstractLooperTestCase.fixtures + ['default_site']
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.subscription = self._create_membership_due_now()
|
||||
@ -286,8 +297,8 @@ class TestClockEmails(AbstractLooperTestCase):
|
||||
customer = self.subscription.customer
|
||||
user = customer.user
|
||||
_write_mail(mail)
|
||||
self.assertEqual(len(mail.outbox), 2)
|
||||
email = mail.outbox[-1]
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
email = mail.outbox[0]
|
||||
self.assertEqual(email.to, [user.customer.billing_address.email])
|
||||
# TODO(anna): set the correct reply_to
|
||||
self.assertEqual(email.reply_to, [])
|
||||
@ -351,9 +362,9 @@ class TestClockEmails(AbstractLooperTestCase):
|
||||
# Check that an email notification is sent
|
||||
customer = self.subscription.customer
|
||||
user = customer.user
|
||||
self.assertEqual(len(mail.outbox), 2)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
_write_mail(mail)
|
||||
email = mail.outbox[-1]
|
||||
email = mail.outbox[0]
|
||||
self.assertEqual(email.to, [user.customer.billing_address.email])
|
||||
# TODO(anna): set the correct reply_to
|
||||
self.assertEqual(email.reply_to, [])
|
||||
@ -412,8 +423,8 @@ class TestClockEmails(AbstractLooperTestCase):
|
||||
customer = self.subscription.customer
|
||||
user = customer.user
|
||||
_write_mail(mail)
|
||||
self.assertEqual(len(mail.outbox), 2)
|
||||
email = mail.outbox[-1]
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
email = mail.outbox[0]
|
||||
self.assertEqual(email.to, [user.customer.billing_address.email])
|
||||
# TODO(anna): set the correct reply_to
|
||||
self.assertEqual(email.reply_to, [])
|
||||
@ -446,8 +457,8 @@ class TestClockEmails(AbstractLooperTestCase):
|
||||
|
||||
# Check that an email notification is sent
|
||||
_write_mail(mail)
|
||||
self.assertEqual(len(mail.outbox), 2)
|
||||
email = mail.outbox[-1]
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
email = mail.outbox[0]
|
||||
self.assertEqual(email.to, ['admin@example.com'])
|
||||
self.assertEqual(email.from_email, 'webmaster@localhost')
|
||||
self.assertEqual(email.subject, expected_managed_email_subj)
|
||||
|
@ -137,6 +137,8 @@ class LinkMembershipTest(AbstractLooperTestCase):
|
||||
self.assertIsNone(order.subscription_id)
|
||||
self.assertEqual(order.customer_id, accountless_customer_id)
|
||||
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)
|
||||
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
|
||||
self.subscription.refresh_from_db()
|
||||
|
||||
tasks_q = background_task.models.Task.objects.all()
|
||||
self.assertEqual(tasks_q.count(), 2)
|
||||
self.assertEqual(tasks_q.count(), tasks_before + 2)
|
||||
# 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_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}}}]',
|
||||
)
|
||||
|
||||
|
@ -1,11 +1,23 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
import django.core.mail
|
||||
|
||||
from blender_fund_main.email import absolute_url
|
||||
from looper.tests import AbstractLooperTestCase
|
||||
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):
|
||||
fixtures = ['default_site', 'gateways', 'devfund', 'testuser', 'systemuser']
|
||||
|
||||
|
@ -1,15 +1,22 @@
|
||||
from unittest.mock import patch
|
||||
import logging
|
||||
|
||||
import django.core.mail
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
import django.core.mail
|
||||
|
||||
from looper.tests import AbstractLooperTestCase
|
||||
from .. import models, signals
|
||||
|
||||
import blender_fund_main.tasks as tasks
|
||||
|
||||
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):
|
||||
fixtures = AbstractLooperTestCase.fixtures + ['default_site', 'systemuser']
|
||||
|
||||
|
@ -1,11 +1,18 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
import django.core.mail
|
||||
from django.dispatch import receiver
|
||||
from django.test import override_settings
|
||||
import django.core.mail
|
||||
|
||||
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):
|
||||
fixtures = ['default_site', 'gateways', 'devfund']
|
||||
|
||||
|
@ -1,6 +1,32 @@
|
||||
import typing
|
||||
|
||||
from html.parser import HTMLParser
|
||||
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):
|
||||
skip_text_of = ('a', 'style')
|
||||
|
Loading…
Reference in New Issue
Block a user