Skip to content

Commit

Permalink
feat: Donate button (#2364)
Browse files Browse the repository at this point in the history
* First instance of donate popup

* Remove reverse JS static file

* Create JS reverse URLs on run

* Do the same for test server

* Simplify everything

* Add tests

* Fix tests

* Fix Cypress test

* Factorise Dotmailer methods

* Remove js reverse remnants

* Merge branch 'master' into donate_button
  • Loading branch information
faucomte97 authored Oct 11, 2024
1 parent 3f1c03d commit 2770300
Show file tree
Hide file tree
Showing 25 changed files with 1,080 additions and 586 deletions.
22 changes: 11 additions & 11 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,26 @@ verify_ssl = true
name = "pypi"

[packages]
codeforlife-portal = {path = ".", editable = true}
cfl-common = {path = "./cfl_common", editable = true}
codeforlife-portal = {path = ".", editable = true}

[dev-packages]
black = "*"
django-import-export = "*"
django-selenium-clean = "==1.0.0"
django-test-migrations = "==1.2.0"
responses = "==0.18.0"
selenium = "==4.9.0"
snapshottest = "==1.0.0a1"
pytest-django = "==4.5.2"
isort = "*"
PyPDF2 = "==2.10.6"
pytest = "==8.*"
django-import-export = "*"
pytest-cov = "*"
pytest-xdist = "*"
pytest-django = "==4.5.2"
pytest-mock = "*"
pytest-order = "*"
pytest-xdist = "*"
pyvirtualdisplay = "*"
pytest-mock = "*"
PyPDF2 = "==2.10.6"
black = "*"
isort = "*"
responses = "==0.18.0"
selenium = "==4.9.0"
snapshottest = "==1.0.0a1"

[requires]
python_version = "3.12"
835 changes: 434 additions & 401 deletions Pipfile.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion cfl_common/common/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

# 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", "")
DOTMAILER_TEACHER_ADDRESS_BOOK_URL = getattr(settings, "DOTMAILER_TEACHER_ADDRESS_BOOK_URL", "")
DOTMAILER_STUDENT_ADDRESS_BOOK_URL = getattr(settings, "DOTMAILER_STUDENT_ADDRESS_BOOK_URL", "")
DOTMAILER_NO_ACCOUNT_ADDRESS_BOOK_URL = getattr(settings, "DOTMAILER_NO_ACCOUNT_ADDRESS_BOOK_URL", "")
Expand Down
189 changes: 153 additions & 36 deletions cfl_common/common/helpers/emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@

import jwt
from common import app_settings
from common.mail import campaign_ids, django_send_email, send_dotdigital_email
from common.mail import (
address_book_ids,
campaign_ids,
django_send_email,
send_dotdigital_email,
)
from common.models import Student, Teacher
from django.conf import settings
from django.contrib.auth.models import User
Expand All @@ -15,9 +20,15 @@
from requests import delete, get, post, put
from requests.exceptions import RequestException

NOTIFICATION_EMAIL = "Code For Life Notification <" + app_settings.EMAIL_ADDRESS + ">"
VERIFICATION_EMAIL = "Code For Life Verification <" + app_settings.EMAIL_ADDRESS + ">"
PASSWORD_RESET_EMAIL = "Code For Life Password Reset <" + app_settings.EMAIL_ADDRESS + ">"
NOTIFICATION_EMAIL = (
"Code For Life Notification <" + app_settings.EMAIL_ADDRESS + ">"
)
VERIFICATION_EMAIL = (
"Code For Life Verification <" + app_settings.EMAIL_ADDRESS + ">"
)
PASSWORD_RESET_EMAIL = (
"Code For Life Password Reset <" + app_settings.EMAIL_ADDRESS + ">"
)
INVITE_FROM = "Code For Life Invitation <" + app_settings.EMAIL_ADDRESS + ">"


Expand All @@ -41,7 +52,9 @@ def generate_token_for_email(email: str, new_email: str = ""):
"email": email,
"new_email": new_email,
"email_verification_token": uuid4().hex[:30],
"expires": (timezone.now() + datetime.timedelta(hours=1)).timestamp(),
"expires": (
timezone.now() + datetime.timedelta(hours=1)
).timestamp(),
},
settings.SECRET_KEY,
algorithm="HS256",
Expand All @@ -62,10 +75,21 @@ def send_email(
plaintext_template="email.txt",
html_template="email.html",
):
django_send_email(sender, recipients, subject, text_content, title, replace_url, plaintext_template, html_template)
django_send_email(
sender,
recipients,
subject,
text_content,
title,
replace_url,
plaintext_template,
html_template,
)


def send_verification_email(request, user, data, new_email=None, age=None, school=None):
def send_verification_email(
request, user, data, new_email=None, age=None, school=None
):
"""
Sends emails relating to email address verification.
Expand Down Expand Up @@ -98,48 +122,81 @@ def send_verification_email(request, user, data, new_email=None, age=None, schoo
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"

send_dotdigital_email(
campaign_ids["verify_released_student"], [user.email],
personalization_values={"VERIFICATION_LINK": url, "SCHOOL_NAME": school.name}
campaign_ids["verify_released_student"],
[user.email],
personalization_values={
"VERIFICATION_LINK": url,
"SCHOOL_NAME": school.name,
},
)
else:
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}
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)
add_to_dotmailer(
user.first_name,
user.last_name,
user.email,
address_book_ids["newsletter"],
DotmailerUserType.TEACHER,
)
# if the user is an independent student
else:
if age < 13:
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
send_dotdigital_email(
campaign_ids["verify_new_user_via_parent"],
[user.email],
personalization_values={"FIRST_NAME": user.first_name, "ACTIVATION_LINK": url},
personalization_values={
"FIRST_NAME": user.first_name,
"ACTIVATION_LINK": url,
},
)
else:
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}
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)
add_to_dotmailer(
user.first_name,
user.last_name,
user.email,
address_book_ids["newsletter"],
DotmailerUserType.STUDENT,
)
# verifying change of email address.
else:
verification = generate_token(user, new_email)
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}
campaign_ids["email_change_verification"],
[new_email],
personalization_values={"VERIFICATION_LINK": url},
)


def add_to_dotmailer(first_name: str, last_name: str, email: str, user_type: DotmailerUserType):
def add_to_dotmailer(
first_name: str,
last_name: str,
email: str,
address_book_id: int,
user_type: DotmailerUserType = None,
):
try:
create_contact(first_name, last_name, email)
add_contact_to_address_book(first_name, last_name, email, user_type)
add_contact_to_address_book(
first_name, last_name, email, address_book_id, user_type
)
except RequestException:
return HttpResponse(status=404)

Expand All @@ -157,15 +214,34 @@ def create_contact(first_name, last_name, email):
{"key": "FULLNAME", "value": f"{first_name} {last_name}"},
],
},
"consentFields": [{"fields": [{"key": "DATETIMECONSENTED", "value": datetime.datetime.now().__str__()}]}],
"consentFields": [
{
"fields": [
{
"key": "DATETIMECONSENTED",
"value": datetime.datetime.now().__str__(),
}
]
}
],
"preferences": app_settings.DOTMAILER_DEFAULT_PREFERENCES,
}

post(url, json=body, auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD))
post(
url,
json=body,
auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
)


def add_contact_to_address_book(first_name: str, last_name: str, email: str, user_type: DotmailerUserType):
main_address_book_url = app_settings.DOTMAILER_MAIN_ADDRESS_BOOK_URL
def add_contact_to_address_book(
first_name: str,
last_name: str,
email: str,
address_book_id: int,
user_type: DotmailerUserType = None,
):
main_address_book_url = f"https://r1-api.dotmailer.com/v2/address-books/{address_book_id}/contacts"

body = {
"email": email,
Expand All @@ -178,60 +254,101 @@ def add_contact_to_address_book(first_name: str, last_name: str, email: str, use
],
}

post(main_address_book_url, json=body, auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD))

specific_address_book_url = app_settings.DOTMAILER_NO_ACCOUNT_ADDRESS_BOOK_URL
post(
main_address_book_url,
json=body,
auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
)

if user_type == DotmailerUserType.TEACHER:
specific_address_book_url = app_settings.DOTMAILER_TEACHER_ADDRESS_BOOK_URL
elif user_type == DotmailerUserType.STUDENT:
specific_address_book_url = app_settings.DOTMAILER_STUDENT_ADDRESS_BOOK_URL
if user_type is not None:
specific_address_book_url = (
app_settings.DOTMAILER_NO_ACCOUNT_ADDRESS_BOOK_URL
)

post(specific_address_book_url, json=body, auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD))
if user_type == DotmailerUserType.TEACHER:
specific_address_book_url = (
app_settings.DOTMAILER_TEACHER_ADDRESS_BOOK_URL
)
elif user_type == DotmailerUserType.STUDENT:
specific_address_book_url = (
app_settings.DOTMAILER_STUDENT_ADDRESS_BOOK_URL
)

post(
specific_address_book_url,
json=body,
auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
)


def delete_contact(email: str):
try:
user = get_dotmailer_user_by_email(email)
user_id = user.get("id")
if user_id:
url = app_settings.DOTMAILER_DELETE_USER_BY_ID_URL.replace("ID", str(user_id))
delete(url, auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD))
url = app_settings.DOTMAILER_DELETE_USER_BY_ID_URL.replace(
"ID", str(user_id)
)
delete(
url,
auth=(
app_settings.DOTMAILER_USER,
app_settings.DOTMAILER_PASSWORD,
),
)
except RequestException:
return HttpResponse(status=404)


def get_dotmailer_user_by_email(email):
url = app_settings.DOTMAILER_GET_USER_BY_EMAIL_URL.replace("EMAIL", email)

response = get(url, auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD))
response = get(
url, auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD)
)

return json.loads(response.content)


def add_consent_record_to_dotmailer_user(user):
consent_date_time = datetime.datetime.now().__str__()

url = app_settings.DOTMAILER_PUT_CONSENT_DATA_URL.replace("USER_ID", str(user["id"]))
url = app_settings.DOTMAILER_PUT_CONSENT_DATA_URL.replace(
"USER_ID", str(user["id"])
)
body = {
"contact": {
"email": user["email"],
"optInType": user["optInType"],
"emailType": user["emailType"],
"dataFields": user["dataFields"],
},
"consentFields": [{"fields": [{"key": "DATETIMECONSENTED", "value": consent_date_time}]}],
"consentFields": [
{
"fields": [
{"key": "DATETIMECONSENTED", "value": consent_date_time}
]
}
],
}

put(url, json=body, auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD))
put(
url,
json=body,
auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
)


def send_dotmailer_consent_confirmation_email_to_user(user):
url = app_settings.DOTMAILER_SEND_CAMPAIGN_URL
campaign_id = app_settings.DOTMAILER_THANKS_FOR_STAYING_CAMPAIGN_ID
body = {"campaignID": campaign_id, "contactIds": [str(user["id"])]}

post(url, json=body, auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD))
post(
url,
json=body,
auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
)


def update_indy_email(user, request, data):
Expand Down
5 changes: 5 additions & 0 deletions cfl_common/common/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
"inactive_users_on_website_final_reminder": 1606215,
}

address_book_ids = {
"newsletter": 9705772,
"donors": 37649245,
}


def add_contact(email: str):
"""Add a new contact to Dotdigital."""
Expand Down
1 change: 1 addition & 0 deletions example_project/portal_test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
"common.context_processors.module_name",
"common.context_processors.cookie_management_enabled",
"portal.context_processors.process_newsletter_form",
"portal.context_processors.process_donate_form",
]
},
}
Expand Down
1 change: 1 addition & 0 deletions example_project/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
"common.context_processors.module_name",
"common.context_processors.cookie_management_enabled",
"portal.context_processors.process_newsletter_form",
"portal.context_processors.process_donate_form",
]
}
}
Expand Down
Loading

0 comments on commit 2770300

Please sign in to comment.