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

fix: move verification emails to Dotdigital #2277

Merged
merged 47 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
6226ef9
fix: move verification emails to Dotdigital
evemartin Mar 28, 2024
d3b94b6
Merge branch 'master' into move-verification-emails-to-dotdigital
evemartin Mar 28, 2024
48cb482
fix: fix dotdigital email tests
evemartin Apr 2, 2024
dc6b581
fix: fix parent email test
evemartin Apr 2, 2024
84a5001
fix: add github secret binding
evemartin Apr 2, 2024
3eaaecb
fix: add chrome to dev container
evemartin Apr 2, 2024
55d6029
fix: attempt to fix under 13 test
evemartin Apr 2, 2024
33d57f4
fix: attempt to fix patching
evemartin Apr 2, 2024
f81868a
fix: continue work on updating tests
evemartin Apr 4, 2024
261cf33
fix: continue to update tests
evemartin Apr 4, 2024
0aa6d29
fix: fix package names
evemartin Apr 4, 2024
d4e95fe
fix: fix test assert
evemartin Apr 4, 2024
3ba97c2
fix: fix assert
evemartin Apr 4, 2024
9651a2a
fix: update tests
evemartin Apr 4, 2024
fcdf86d
fix: rearrange decorators
evemartin Apr 4, 2024
537f333
fix: continue work on tests
evemartin Apr 4, 2024
8add1ec
fix: fix typo
evemartin Apr 4, 2024
ea6239a
fix: testing something out
evemartin Apr 4, 2024
688013b
fix: testing something out
evemartin Apr 4, 2024
3ef54f4
fix: undo chrome changes
evemartin Apr 4, 2024
6dbe672
fix: adding additional mocks
evemartin Apr 4, 2024
f3a1cae
fix: debug tests
evemartin Apr 4, 2024
9ee8e3e
fix: debug tests
evemartin Apr 4, 2024
3305851
fix: debug tests
evemartin Apr 4, 2024
6502383
fix: debug tests
evemartin Apr 4, 2024
bd4942c
fix: debug tests
evemartin Apr 4, 2024
296d1cf
fix: debug tests
evemartin Apr 4, 2024
4327fee
fix: debug tests
evemartin Apr 4, 2024
dd8b086
fix: debug tests
evemartin Apr 4, 2024
a1f0674
fix: debug tests
evemartin Apr 4, 2024
1dc615a
fix: add forgotten import
evemartin Apr 4, 2024
643dfd2
fix: update dotdigital auth
evemartin Apr 9, 2024
5ab9fe3
fix: fix environmental variable
evemartin Apr 9, 2024
1425477
fix: address PR comments
evemartin Apr 9, 2024
566f528
fix: address PR comments
evemartin Apr 9, 2024
6abe884
fix: debug tests
evemartin Apr 9, 2024
e829610
fix: debug tests
evemartin Apr 9, 2024
1bc639f
fix: tidy code, replace verification reminder emails
evemartin Apr 9, 2024
4d3ac93
fix: fix verification reminder email tests
evemartin Apr 9, 2024
cc1e7c1
fix: debug tests
evemartin Apr 9, 2024
4e9cc43
fix: debug tests
evemartin Apr 9, 2024
dac0b40
fix: fix patch mock
evemartin Apr 10, 2024
b845d05
fix: tidy code
evemartin Apr 10, 2024
1ef51af
fix: add id to footer element
evemartin Apr 10, 2024
0834094
fix: remove id
evemartin Apr 10, 2024
90c49ae
fix: dummy commit
evemartin Apr 10, 2024
b8d9d24
fix: undo dummy commit
evemartin Apr 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"ghcr.io/devcontainers/features/python:1": {
"installTools": false,
"version": "3.8"
}
},
"ghcr.io/kreemer/features/chrometesting:1": {}
},
"name": "portal",
"postCreateCommand": "pipenv install --dev",
Expand Down
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,6 @@
"."
],
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false
"python.testing.unittestEnabled": false,
"python.analysis.extraPaths": ["./cfl_common"]
}
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pyvirtualdisplay = "*"
pytest-mock = "*"
PyPDF2 = "==2.10.6"
black = "*"
isort = "*"

[requires]
python_version = "3.8"
396 changes: 203 additions & 193 deletions Pipfile.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions cfl_common/common/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
# Email address to source notifications from
EMAIL_ADDRESS = getattr(settings, "EMAIL_ADDRESS", "[email protected]")

# Dotdigital authorization details
DOTDIGITAL_AUTH = getattr(settings, "DOTDIGITAL_AUTH", "")

# Dotmailer URLs for adding users to the newsletter address book
DOTMAILER_CREATE_CONTACT_URL = getattr(settings, "DOTMAILER_CREATE_CONTACT_URL", "")
DOTMAILER_MAIN_ADDRESS_BOOK_URL = getattr(settings, "DOTMAILER_MAIN_ADDRESS_BOOK_URL", "")
Expand Down
72 changes: 27 additions & 45 deletions cfl_common/common/helpers/emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,16 @@
import jwt
from common import app_settings
from common.app_settings import domain
from common.email_messages import (
emailChangeNotificationEmail,
emailChangeVerificationEmail,
emailVerificationNeededEmail,
parentsEmailVerificationNeededEmail,
)
from common.models import Teacher, Student
from common.mail import campaign_ids, send_dotdigital_email
from common.models import Student, Teacher
from django.conf import settings
from django.contrib.auth.models import User
from django.core.mail import EmailMultiAlternatives
from django.http import HttpResponse
from django.template import loader
from django.urls import reverse
from django.utils import timezone
from requests import post, get, put, delete
from requests import delete, get, post, put
from requests.exceptions import RequestException

NOTIFICATION_EMAIL = "Code For Life Notification <" + app_settings.EMAIL_ADDRESS + ">"
Expand Down Expand Up @@ -123,55 +119,37 @@ def send_verification_email(request, user, data, new_email=None, age=None):

# if the user is a teacher
if age is None:
message = emailVerificationNeededEmail(request, verification)
send_email(
VERIFICATION_EMAIL,
[user.email],
message["subject"],
message["message"],
message["subject"],
replace_url=message["url"],
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"

send_dotdigital_email(
campaign_ids["verify_new_user"], [user.email], personalization_values={"VERIFICATION_LINK": url}
)

if _newsletter_ticked(data):
add_to_dotmailer(user.first_name, user.last_name, user.email, DotmailerUserType.TEACHER)
# if the user is an independent student
else:
if age < 13:
message = parentsEmailVerificationNeededEmail(request, user, verification)
send_email(
VERIFICATION_EMAIL,
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
send_dotdigital_email(
campaign_ids["verify_new_user_via_parent"],
[user.email],
message["subject"],
message["message"],
message["subject"],
replace_url=message["url"],
personalization_values={"FIRST_NAME": user.first_name, "ACTIVATION_LINK": url},
)
else:
message = emailVerificationNeededEmail(request, verification)
send_email(
VERIFICATION_EMAIL,
[user.email],
message["subject"],
message["message"],
message["subject"],
replace_url=message["url"],
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
send_dotdigital_email(
campaign_ids["verify_new_user"], [user.email], personalization_values={"VERIFICATION_LINK": url}
)

if _newsletter_ticked(data):
add_to_dotmailer(user.first_name, user.last_name, user.email, DotmailerUserType.STUDENT)
# verifying change of email address.
else:
verification = generate_token(user, new_email)

message = emailChangeVerificationEmail(request, verification)
send_email(
VERIFICATION_EMAIL,
[new_email],
message["subject"],
message["message"],
message["subject"],
replace_url=message["url"],
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
send_dotdigital_email(
campaign_ids["email_change_verification"], [new_email], personalization_values={"VERIFICATION_LINK": url}
)


Expand Down Expand Up @@ -281,8 +259,11 @@ def update_indy_email(user, request, data):
changing_email = True
users_with_email = User.objects.filter(email=new_email)

message = emailChangeNotificationEmail(request, new_email)
send_email(VERIFICATION_EMAIL, [user.email], message["subject"], message["message"], message["subject"])
send_dotdigital_email(
campaign_ids["email_change_notification"],
[user.email],
personalization_values={"NEW_EMAIL_ADDRESS": new_email},
)

# email is available
if not users_with_email.exists():
Expand All @@ -299,9 +280,10 @@ def update_email(user: Teacher or Student, request, data):
changing_email = True
users_with_email = User.objects.filter(email=new_email)

message = emailChangeNotificationEmail(request, new_email)
send_email(
VERIFICATION_EMAIL, [user.new_user.email], message["subject"], message["message"], message["subject"]
send_dotdigital_email(
campaign_ids["email_change_notification"],
[user.new_user.email],
personalization_values={"NEW_EMAIL_ADDRESS": new_email},
)

# email is available
Expand Down
115 changes: 115 additions & 0 deletions cfl_common/common/mail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import os
import typing as t
from dataclasses import dataclass

import requests
from common import app_settings

campaign_ids = {
"email_change_verification": 1551594,
"email_change_notification": 1551600,
"verify_new_user": 1551577,
"verify_new_user_via_parent": 1551587,
}


def add_contact(email: str):
"""Add a new contact to Dotdigital."""
# TODO: implement


def remove_contact(email: str):
"""Remove an existing contact from Dotdigital."""
# TODO: implement


@dataclass
class EmailAttachment:
"""An email attachment for a Dotdigital triggered campaign."""

file_name: str
mime_type: str
content: str


# pylint: disable-next=too-many-arguments
def send_dotdigital_email(
campaign_id: int,
to_addresses: t.List[str],
cc_addresses: t.Optional[t.List[str]] = None,
bcc_addresses: t.Optional[t.List[str]] = None,
from_address: t.Optional[str] = None,
personalization_values: t.Optional[t.Dict[str, str]] = None,
metadata: t.Optional[str] = None,
attachments: t.Optional[t.List[EmailAttachment]] = None,
region: str = "r1",
auth: t.Optional[str] = None,
timeout: int = 30,
):
# pylint: disable=line-too-long
"""Send a triggered email campaign using DotDigital's API.

https://developer.dotdigital.com/reference/send-transactional-email-using-a-triggered-campaign

Args:
campaign_id: The ID of the triggered campaign, which needs to be included within the request body.
to_addresses: The email address(es) to send to.
cc_addresses: The CC email address or address to to send to. separate email addresses with a comma. Maximum: 100.
bcc_addresses: The BCC email address or address to to send to. separate email addresses with a comma. Maximum: 100.
from_address: The From address for your email. Note: The From address must already be added to your account. Otherwise, your account's default From address is used.
personalization_values: Each personalisation value is a key-value pair; the placeholder name of the personalization value needs to be included in the request body.
metadata: The metadata for your email. It can be either a single value or a series of values in a JSON object.
attachments: A Base64 encoded string. All attachment types are supported. Maximum file size: 15 MB.
region: The Dotdigital region id your account belongs to e.g. r1, r2 or r3.
auth: The authorization header used to enable API access. If None, the value will be retrieved from the DOTDIGITAL_AUTH environment variable.
timeout: Send timeout to avoid hanging.

Raises:
AssertionError: If failed to send email.
"""
# pylint: enable=line-too-long

if auth is None:
auth = app_settings.DOTDIGITAL_AUTH

body = {
"campaignId": campaign_id,
"toAddresses": to_addresses,
}
if cc_addresses is not None:
body["ccAddresses"] = cc_addresses
if bcc_addresses is not None:
body["bccAddresses"] = bcc_addresses
if from_address is not None:
body["fromAddress"] = from_address
if personalization_values is not None:
body["personalizationValues"] = [
{
"name": key,
"value": value,
}
for key, value in personalization_values.items()
]
if metadata is not None:
body["metadata"] = metadata
if attachments is not None:
body["attachments"] = [
{
"fileName": attachment.file_name,
"mimeType": attachment.mime_type,
"content": attachment.content,
}
for attachment in attachments
]

response = requests.post(
url=f"https://{region}-api.dotdigital.com/v2/email/triggered-campaign",
json=body,
headers={
"accept": "text/plain",
"authorization": auth,
},
timeout=timeout,
)

assert response.ok, "Failed to send email." f" Reason: {response.reason}." f" Text: {response.text}."
40 changes: 8 additions & 32 deletions cfl_common/common/tests/utils/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,14 @@
from builtins import str


def follow_verify_email_link_to_onboarding(page, email):
_follow_verify_email_link(page, email)
def follow_verify_email_link_to_onboarding(page, url):
page.browser.get(url)

return go_to_teacher_login_page(page.browser)


def follow_verify_email_link_to_teacher_dashboard(page, email):
_follow_verify_email_link(page, email)

return go_to_teacher_dashboard_page(page.browser)


def follow_verify_email_link_to_login(page, email, user_type):
_follow_verify_email_link(page, email)
def follow_verify_email_link_to_login(page, url, user_type):
page.browser.get(url)

if user_type == "teacher":
return go_to_teacher_login_page(page.browser)
Expand All @@ -32,15 +26,6 @@ def follow_duplicate_account_link_to_login(page, email, user_type):
return go_to_independent_student_login_page(page.browser)


def _follow_verify_email_link(page, email):
message = str(email.message())
prefix = '<p>Please go to <a href="'
i = str.find(message, prefix) + len(prefix)
suffix = '" rel="nofollow">'
j = str.find(message, suffix, i)
page.browser.get(message[i:j])


def _follow_duplicate_account_email_link(page, email):
message = str(email.message())
prefix = 'please login: <a href="'
Expand All @@ -60,27 +45,18 @@ def follow_reset_email_link(browser, email):
return PasswordResetPage(browser)


def follow_change_email_link_to_dashboard(page, email):
_follow_change_email_link(page, email)
def follow_change_email_link_to_dashboard(page, url):
page.browser.get(url)

return go_to_teacher_login_page(page.browser)


def follow_change_email_link_to_independent_dashboard(page, email):
_follow_change_email_link(page, email)
def follow_change_email_link_to_independent_dashboard(page, url):
page.browser.get(url)

return go_to_independent_student_login_page(page.browser)


def _follow_change_email_link(page, email):
message = str(email.message())
prefix = "please go to "
i = str.find(message, prefix) + len(prefix)
suffix = " to verify"
j = str.find(message, suffix, i)
page.browser.get(message[i:j])


def go_to_teacher_login_page(browser):
from portal.tests.pageObjects.portal.teacher_login_page import TeacherLoginPage

Expand Down
16 changes: 8 additions & 8 deletions cfl_common/common/tests/utils/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from common.helpers.generators import generate_login_id
from common.models import Class, Student
from django.core import mail
from unittest.mock import Mock, patch

from . import email

Expand Down Expand Up @@ -118,24 +119,23 @@ def signup_duplicate_independent_student_fail(page, duplicate_email=None):
return page, name, username, email_address, password


def create_independent_student(page):
@patch("common.helpers.emails.send_dotdigital_email")
def create_independent_student(page, mock_send_dotdigital_email):
page = page.go_to_signup_page()

name, username, email_address, password = generate_independent_student_details()
page = page.independent_student_signup(name, email_address, password=password, confirm_password=password)

page = page.return_to_home_page()

page = email.follow_verify_email_link_to_login(page, mail.outbox[0], "independent")
mail.outbox = []
verification_url = mock_send_dotdigital_email.call_args.kwargs["personalization_values"]["VERIFICATION_LINK"]

return page, name, username, email_address, password
page = email.follow_verify_email_link_to_login(page, verification_url, "independent")

return page, name, username, email_address, password

def verify_email(page):
assert len(mail.outbox) > 0

page = email.follow_verify_email_link_to_login(page, mail.outbox[0], "independent")
mail.outbox = []
def verify_email(page, verification_url):
page = email.follow_verify_email_link_to_login(page, verification_url, "independent")

return page
Loading
Loading