Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #149: Extracted email messages to templates. Fixed some bugs, cleaned up some code. #156

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
22 changes: 11 additions & 11 deletions dds_registration/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from django.contrib import admin, messages
from django.contrib.admin import SimpleListFilter
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.db.models import Q
from django.http import HttpResponse
from django.db.models import Q, QuerySet
from django.http import HttpRequest, HttpResponse

from .forms import (
EventAdminForm,
Expand Down Expand Up @@ -42,7 +42,7 @@ def lookups(self, request, model_admin):
("0", "No"),
)

def queryset(self, request, queryset):
def queryset(self, request: HttpRequest, queryset: QuerySet):
if self.value() == "1":
return queryset.filter(is_staff=False, is_superuser=False)
if self.value() == "0":
Expand Down Expand Up @@ -212,17 +212,17 @@ class PaymentAdmin(admin.ModelAdmin):
actions = ["mark_invoice_paid", "email_invoices", "email_receipts", "download_invoices", "download_receipts"]

@admin.action(description="Mark selected invoices paid")
def mark_invoice_paid(self, request, queryset):
def mark_invoice_paid(self, request: HttpRequest, queryset: QuerySet):
for obj in queryset:
obj.mark_paid()
obj.mark_paid(request)
self.message_user(
request,
f"{queryset.count()} invoices marked as paid",
messages.SUCCESS,
)

@admin.action(description="Email unpaid invoices to user")
def email_invoices(self, request, queryset):
def email_invoices(self, request: HttpRequest, queryset: QuerySet):
qs = queryset.filter(status__in=("CREATED", "ISSUED"))
count = qs.count()

Expand All @@ -235,7 +235,7 @@ def email_invoices(self, request, queryset):
return

for obj in qs:
obj.email_invoice()
obj.email_invoice(request)

self.message_user(
request,
Expand All @@ -244,7 +244,7 @@ def email_invoices(self, request, queryset):
)

@admin.action(description="Download unpaid invoices")
def download_invoices(self, request, queryset):
def download_invoices(self, request: HttpRequest, queryset: QuerySet):
qs = queryset.filter(status__in=("CREATED", "ISSUED"))

if not qs.count():
Expand All @@ -266,7 +266,7 @@ def download_invoices(self, request, queryset):
return response

@admin.action(description="Email receipts for completed payments to user")
def email_receipts(self, request, queryset):
def email_receipts(self, request: HttpRequest, queryset):
qs = queryset.filter(status="PAID")
count = qs.count()

Expand All @@ -279,15 +279,15 @@ def email_receipts(self, request, queryset):
return

for obj in queryset:
obj.email_receipt()
obj.email_receipt(request)
self.message_user(
request,
f"{count} receipt(s) sent",
messages.SUCCESS,
)

@admin.action(description="Download completed payment receipts")
def download_receipts(self, request, queryset):
def download_receipts(self, request: HttpRequest, queryset: QuerySet):
qs = queryset.filter(status="PAID")

if not qs.count():
Expand Down
13 changes: 10 additions & 3 deletions dds_registration/core/helpers/create_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@


def normalize_text(text: str) -> str:
return text.encode('utf-8', 'ignore').decode('utf-8').strip()
return text.encode("utf-8", "ignore").decode("utf-8").strip()


def create_pdf(
Expand Down Expand Up @@ -87,11 +87,18 @@ def create_pdf(

# Left (client) address column...
pdf.set_xy(left_column_pos, margin_size + top_offset)
pdf.multi_cell(text=normalize_text(client_name), w=left_column_width, align=Align.L, new_x="LEFT", new_y="NEXT", h=line_height)
pdf.multi_cell(
text=normalize_text(client_name), w=left_column_width, align=Align.L, new_x="LEFT", new_y="NEXT", h=line_height
)

pdf.set_y(pdf.get_y() + small_vertical_space)
pdf.multi_cell(
text=normalize_text(client_address), w=left_column_width, align=Align.L, new_x="LEFT", new_y="NEXT", h=line_height
text=normalize_text(client_address),
w=left_column_width,
align=Align.L,
new_x="LEFT",
new_y="NEXT",
h=line_height,
)

left_stop_pos = pdf.get_y()
Expand Down
31 changes: 31 additions & 0 deletions dds_registration/core/helpers/emails.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import re


def prepare_email_message_text(text: str) -> str:
# Remove spaces around
text = text.strip()
# Remove spaces before newlines
text = re.sub(r"[ \t]*[\n\r]", "\n", text)
# Leave max two newlines in a row
text = re.sub(r"\n{3,}", "\n\n", text)
return text


def parse_email_subject_and_content(text: str) -> list[str]:
"""
Email message should contain a subject in the beginning, separated from the content by double newline.
Returns the list consisted of [subject, content]
"""
text = prepare_email_message_text(text)
results = text.split("\n\n", 1)
if len(results) < 2:
raise Exception(
"Email message should constist of a subject and a content, separated from the content by double newline."
)
return results


__all__ = [
prepare_email_message_text,
parse_email_subject_and_content,
]
61 changes: 46 additions & 15 deletions dds_registration/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@
import random
import string
from datetime import date

import requests

from fpdf import FPDF

from django.contrib.sites.models import Site
from django.http import HttpRequest
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.db.models import Model, Q, QuerySet
from django.urls import reverse
from fpdf import FPDF
from django.template.loader import render_to_string

from dds_registration.core.constants.payments import (
site_default_currency,
site_supported_currencies,
)
from .core.helpers.emails import parse_email_subject_and_content

from .core.constants.date_time_formats import dateFormat
from .core.constants.payments import currency_emojis, payment_details_by_currency
Expand Down Expand Up @@ -220,7 +225,7 @@ def mark_obsolete(self):
)
self.save()

def mark_paid(self):
def mark_paid(self, request: HttpRequest):
if self.status == "PAID":
return
self.status = "PAID"
Expand All @@ -235,7 +240,7 @@ def mark_paid(self):
)
},
)
self.email_receipt()
self.email_receipt(request)
self.save()

@property
Expand Down Expand Up @@ -285,29 +290,55 @@ def invoice_pdf(self):
def receipt_pdf(self):
return create_receipt_pdf_from_payment(self)

def email_invoice(self):
def email_invoice(self, request: HttpRequest):
user = User.objects.get(id=self.data["user"]["id"])
# TODO: Issue #149: To extract these (and all other hardcoded here, in `send_email` methods?) texts to template files, with substiting names, urls and emails from settings or preferences values?
context = {
"site": Site.objects.get_current(),
"scheme": "https" if request.is_secure() else "http",
"payment": self,
"user": user,
}
if self.data["kind"] == "membership":
subject = f"DdS Membership Invoice {self.invoice_no}"
message = f"Thanks for signing up for Départ de Sentier membership! Membership fees allow us to write awesome open source code, deploy open infrastructure, and run community events without spending all our time fundraising.\n\nYour membership will run until December 31st, {user.membership.until} (Don't worry, you will get a reminder to renew for another year :).\n\nPlease find attached the membership invoice. Your membership is not in force until the bank transfer is received.\n\nYou can change your invoice details here: https://events.d-d-s.ch{reverse('membership_application')}.\n\nIf you have any questions, please contact [email protected]."
email_template = "dds_registration/payment/emails/invoice_membership.txt.django"
else:
event = Event.objects.get(id=self.data["event"]["id"])
subject = f"DdS Event {event.title} Registration Invoice {self.invoice_no}"
message = f"Thanks for registering for {event.title}! We look forward to seeing your, in person or virtually.\n\nDépart de Sentier runs its events and schools on a cost-neutral basis - i.e. we don't make a profit off the registration fees. They are used for catering, room, hotel, and equipment rental, AV hosting and technician fees, and guest speaker costs. We literally could not run this event without your support.\n\nYou can view your registration status and apply for membership at https://events.d-d-s.ch/profile.\n\nPlease find attached the registration invoice. Your registration is not finalized until the bank transfer is received.\n\nYou can change your invoice details here: https://events.d-d-s.ch{reverse('event_registration', args=(event.code,))}.\n\nIf you have any questions, please contact [email protected]."
context["event"] = Event.objects.get(id=self.data["event"]["id"])
email_template = "dds_registration/payment/emails/invoice_membership.txt.django"
# Parse email message template
text = render_to_string(
template_name=email_template,
context=context,
request=request,
)
# Extract a subject and a message from the template
[subject, message] = parse_email_subject_and_content(text)
user.email_user(
subject=subject,
message=message,
attachment_content=self.invoice_pdf(),
attachment_name=f"DdS Invoice {self.invoice_no}.pdf",
)

def email_receipt(self):
def email_receipt(self, request: HttpRequest):
user = User.objects.get(id=self.data["user"]["id"])
kind = "Membership" if self.data["kind"] == "membership" else "Event"
context = {
"kind": "Membership" if self.data["kind"] == "membership" else "Event",
"site": Site.objects.get_current(),
"scheme": "https" if request.is_secure() else "http",
"payment": self,
"user": user,
}
email_template = "dds_registration/payment/emails/receipt.txt.django"
# Parse email message template
text = render_to_string(
template_name=email_template,
context=context,
request=request,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You had to make a lot of changes elsewhere to get the request here, but I don't understand why it is needed.

)
# Extract a subject and a message from the template
[subject, message] = parse_email_subject_and_content(text)
user.email_user(
subject=f"DdS {kind} Receipt {self.invoice_no}",
message="Thanks! A receipt for your event or membership payment is attached. You can always find more information about your item at your your profile: https://events.d-d-s.ch/profile.\n\nWe really appreciate your support. If you have any questions, please contact [email protected].",
subject=subject,
message=message,
attachment_content=self.receipt_pdf(),
attachment_name=f"DdS receipt {self.invoice_no}.pdf",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ You have registered and already paid for {{ event.title }}!

{% endif %}The payment option you have chosen: {% with option=registration.option %}{{ option.item }}{% if option.price %} ({% if option.currency %}{{ option.currency }} {% endif %}{{ option.price }}){% endif %}{% endwith %}.

{% endcomment %}Your invoice is attached, but can also be download anytime at {{ scheme }}://{{site.domain}}{% url 'billing_event_invoice_download' event_code=event.code %}.
{% endcomment %}Your invoice is attached, but can also be download anytime at {{ scheme }}://{{ site.domain }}{% url 'billing_event_invoice_download' event_code=event.code %}.

If you have questions or comments, you can reach us at {{ settings.DEFAULT_FROM_EMAIL }}.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
DdS Event {{ event.title }} Registration Invoice {{ payment.invoice_no }}


Thanks for registering for {{ event.title }}! We look forward to seeing your, in person or virtually.

Départ de Sentier runs its events and schools on a cost-neutral basis - i.e. we don't make a profit off the registration fees. They are used for catering, room, hotel, and equipment rental, AV hosting and technician fees, and guest speaker costs. We literally could not run this event without your support.

You can view your registration status and apply for membership at {{ scheme }}://{{ site.domain }}{% url 'profile' %}.

Please find attached the registration invoice. Your registration is not finalized until the bank transfer is received.

You can change your invoice details here: {{ scheme }}://{{ site.domain }}{% url 'event_registration' event_code=event.code %}.

If you have any questions, please contact {{ settings.DEFAULT_CONTACT_EMAIL }}.

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
DdS Membership Invoice {{ payment.invoice_no }}


Thanks for signing up for Départ de Sentier membership!

Membership fees allow us to write awesome open source code, deploy open infrastructure, and run community events without spending all our time fundraising.

Your membership will run until December 31st, {{ user.membership.until }} (Don't worry, you will get a reminder to renew for another year :).

Please find attached the membership invoice. Your membership is not in force until the bank transfer is received.

You can change your invoice details here: {{ scheme }}://{{ site.domain }}{% url 'membership_application' %}.

If you have any questions, please contact {{ settings.DEFAULT_CONTACT_EMAIL }}.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
DdS {{ kind }} Receipt {{ payment.invoice_no }}


Thanks!

A receipt for your event or membership payment is attached.

You can always find more information about your item at your your profile: {{ scheme }}://{{ site.domain }}{% url 'profile' %}.

We really appreciate your support.

If you have any questions, please contact {{ settings.DEFAULT_CONTACT_EMAIL }}.
2 changes: 1 addition & 1 deletion dds_registration/urls/payments_urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.urls import path

from ..views.billing_stripe import payment_stripe, payment_stripe_success
from ..views.payment_stripe import payment_stripe, payment_stripe_success
from ..views.payment_utils import invoice_download, receipt_download

urlpatterns = [
Expand Down
2 changes: 1 addition & 1 deletion dds_registration/views/event_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def event_registration(request: HttpRequest, event_code: str):
registration.save()

if payment.data["method"] == "INVOICE":
payment.email_invoice()
payment.email_invoice(request)
payment.status = "ISSUED"
payment.save()
messages.success(
Expand Down
10 changes: 8 additions & 2 deletions dds_registration/views/helpers/stripe_amounts.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from ...core.constants.payments import site_supported_currencies


def get_stripe_amount_for_currency(amount: int, currency: str, convert_basic_unit: bool = True) -> int:
"""Stripe fees vary by country.

Note: Assumes amount in stripe basic unit!!!"""
if convert_basic_unit:
amount = get_stripe_basic_unit(amount=amount, currency=currency)
# NOTE: Must match the list of currencies in `site_supported_currencies`
if currency == "USD":
# Stripe fee is 2.9%, currency conversion loss is 1%
return round(30 + (1 + 0.029 + 0.01) * amount)
Expand All @@ -24,14 +28,16 @@ def get_stripe_basic_unit(amount: float, currency: str) -> int:
"""Stripe wants numbers in lower divisible unit.

This varies by country."""
if currency in ("CAD", "CHF", "EUR", "CAD"):
currency_ids = dict(site_supported_currencies).keys()
if currency in currency_ids:
return round(amount * 100)
else:
raise ValueError(f"Unrecognized currency {currency}")


def convert_from_stripe_units(amount: float, currency: str) -> float:
if currency in ("CAD", "CHF", "EUR", "CAD"):
currency_ids = dict(site_supported_currencies).keys()
if currency in currency_ids:
return round(amount / 100, 2)
else:
raise ValueError(f"Unrecognized currency {currency}")
2 changes: 1 addition & 1 deletion dds_registration/views/membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def membership_application(request: HttpRequest):
if payment.data["method"] == "INVOICE":
payment.status = "ISSUED"
payment.save()
payment.email_invoice()
payment.email_invoice(request)
messages.success(
request,
f"Your membership has been created! An invoice has been sent to {request.user.email} from [email protected]. The invoice can also be downloaded from your profile. Please note your membership is not in force until the invoice is paid.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def payment_stripe(request: HttpRequest, payment_id: int):
payment.data["currency"], stripe_amount, request.user.email, {"payment_id": payment.id}
)

template = "dds_registration/billing/stripe_payment.html.django"
template = "dds_registration/payment/stripe_payment.html.django"

try:
membership = Membership.objects.get(user=request.user)
Expand Down Expand Up @@ -78,7 +78,7 @@ def payment_stripe_success(request: HttpRequest, payment_id: int):
return redirect("profile")

payment.data["price"] = payment.data.pop("stripe_charge_in_progress")
payment.mark_paid()
payment.mark_paid(request)

if payment.data["kind"] == "membership":
messages.success(request, "Awesome, your membership is paid, and you are good to go!")
Expand Down
Loading
Loading