cfl-common 5.3.0__py3-none-any.whl → 8.9.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cfl_common-8.9.15.dist-info/METADATA +47 -0
- cfl_common-8.9.15.dist-info/RECORD +99 -0
- {cfl_common-5.3.0.dist-info → cfl_common-8.9.15.dist-info}/WHEEL +1 -1
- common/app_settings.py +35 -5
- common/csp_config.py +85 -0
- common/fixtures/aimmo_characters.json +30 -30
- common/fixtures/aimmo_characters2.json +1 -1
- common/fixtures/aimmo_characters3.json +35 -0
- common/helpers/data_migration_loader.py +3 -4
- common/helpers/emails.py +228 -108
- common/helpers/generators.py +1 -1
- common/helpers/organisation.py +10 -0
- common/mail.py +201 -0
- common/migrations/0002_emailverification.py +1 -3
- common/migrations/0005_add_worksheets.py +2 -13
- common/migrations/0007_add_pdf_names_to_first_two_worksheets.py +2 -14
- common/migrations/0008_unlock_worksheet_3.py +1 -6
- common/migrations/0011_student_login_id.py +3 -3
- common/migrations/0012_usersession.py +39 -0
- common/migrations/0013_class_school.py +42 -0
- common/migrations/0014_login_type.py +29 -0
- common/migrations/0015_dailyactivity.py +31 -0
- common/migrations/0016_joinreleasestudent.py +42 -0
- common/migrations/0017_copy_email_to_username.py +18 -0
- common/migrations/0018_update_aimmo_character_image_path.py +15 -0
- common/migrations/0019_aimmocharacter_alt.py +16 -0
- common/migrations/0020_class_is_active_and_null_access_code.py +23 -0
- common/migrations/0021_school_is_active.py +28 -0
- common/migrations/0022_school_cleanup.py +29 -0
- common/migrations/0023_userprofile_aimmo_badges.py +22 -0
- common/migrations/0024_teacher_invited_by.py +25 -0
- common/migrations/0025_schoolteacherinvitation.py +47 -0
- common/migrations/0026_teacher_remove_join_request.py +22 -0
- common/migrations/0027_class_created_by.py +25 -0
- common/migrations/0028_coding_club_downloads.py +23 -0
- common/migrations/0029_dynamicelement.py +22 -0
- common/migrations/0030_add_maintenance_banner.py +25 -0
- common/migrations/0031_improve_admin_panel.py +56 -0
- common/migrations/0032_dailyactivity_level_control_submits.py +18 -0
- common/migrations/0033_password_reset_tracking_fields.py +23 -0
- common/migrations/0034_dailyactivity_daily_school_student_lockout_reset.py +18 -0
- common/migrations/0035_rename_lockout_fields.py +27 -0
- common/migrations/0036_rename_awaiting_email_verification_userprofile_is_verified.py +17 -0
- common/migrations/0037_migrate_email_verification.py +21 -0
- common/migrations/0038_delete_emailverification.py +16 -0
- common/migrations/0039_copy_email_to_username.py +18 -0
- common/migrations/0040_school_county.py +18 -0
- common/migrations/0041_populate_gb_counties.py +27 -0
- common/migrations/0042_totalactivity.py +25 -0
- common/migrations/0043_add_total_activity.py +30 -0
- common/migrations/0044_update_activity_models.py +33 -0
- common/migrations/0045_otp.py +23 -0
- common/migrations/0046_alter_school_country.py +19 -0
- common/migrations/0047_delete_school_postcode.py +16 -0
- common/migrations/0048_unique_school_names.py +42 -0
- common/migrations/0049_anonymise_orphan_users.py +29 -0
- common/migrations/0050_anonymise_orphan_schools.py +30 -0
- common/migrations/0051_verify_returning_users.py +26 -0
- common/migrations/0052_add_cse_fields.py +68 -0
- common/migrations/0053_clean_class_data.py +24 -0
- common/migrations/0054_delete_aimmo_models.py +20 -0
- common/migrations/0055_alter_schoolteacherinvitation_token.py +18 -0
- common/migrations/0056_set_non_school_teachers_as_non_admins.py +25 -0
- common/migrations/0057_teacher_teacher__is_admin.py +19 -0
- common/migrations/0058_userprofile_google_refresh_token_and_more.py +24 -0
- common/models.py +347 -63
- common/permissions.py +20 -8
- common/static/common/img/RR_logo.svg +336 -0
- common/static/common/img/brain.svg +1 -0
- common/templates/common/onetrust_cookies_consent_notice.html +6 -6
- common/tests/test_migration_anonymise_orphan_schools.py +30 -0
- common/tests/test_migration_anonymise_orphan_users.py +30 -0
- common/tests/test_migration_blocked_time.py +3 -11
- common/tests/test_migration_remove_teacher_title.py +1 -3
- common/tests/test_migration_unique_school_names.py +33 -0
- common/tests/test_migration_verify_returning_users.py +59 -0
- common/tests/test_models.py +49 -43
- common/tests/utils/classes.py +1 -3
- common/tests/utils/email.py +11 -49
- common/tests/utils/organisation.py +10 -14
- common/tests/utils/student.py +14 -67
- common/tests/utils/teacher.py +16 -38
- common/tests/utils/user.py +1 -3
- cfl_common-5.3.0.dist-info/METADATA +0 -20
- cfl_common-5.3.0.dist-info/RECORD +0 -48
- common/email_messages.py +0 -218
- common/fixtures/unlock_worksheet3.json +0 -20
- common/fixtures/worksheets.json +0 -98
- common/fixtures/worksheets2.json +0 -110
- common/tests/test_migration_aimmo_characters.py +0 -31
- common/tests/test_migration_worksheets.py +0 -49
- {cfl_common-5.3.0.dist-info → cfl_common-8.9.15.dist-info}/top_level.txt +0 -0
common/helpers/emails.py
CHANGED
|
@@ -1,107 +1,190 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import json
|
|
3
|
+
from enum import Enum, auto
|
|
3
4
|
from uuid import uuid4
|
|
4
5
|
|
|
6
|
+
import jwt
|
|
5
7
|
from common import app_settings
|
|
6
|
-
from common.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
from common.mail import (
|
|
9
|
+
address_book_ids,
|
|
10
|
+
campaign_ids,
|
|
11
|
+
django_send_email,
|
|
12
|
+
send_dotdigital_email,
|
|
11
13
|
)
|
|
12
|
-
from common.models import
|
|
13
|
-
from django.
|
|
14
|
+
from common.models import Student, Teacher
|
|
15
|
+
from django.conf import settings
|
|
16
|
+
from django.contrib.auth.models import User
|
|
14
17
|
from django.http import HttpResponse
|
|
15
|
-
from django.
|
|
18
|
+
from django.urls import reverse
|
|
16
19
|
from django.utils import timezone
|
|
17
|
-
from requests import
|
|
20
|
+
from requests import delete, get, post, put
|
|
18
21
|
from requests.exceptions import RequestException
|
|
19
22
|
|
|
20
23
|
NOTIFICATION_EMAIL = "Code For Life Notification <" + app_settings.EMAIL_ADDRESS + ">"
|
|
21
24
|
VERIFICATION_EMAIL = "Code For Life Verification <" + app_settings.EMAIL_ADDRESS + ">"
|
|
22
|
-
PASSWORD_RESET_EMAIL =
|
|
23
|
-
"Code For Life Password Reset <" + app_settings.EMAIL_ADDRESS + ">"
|
|
24
|
-
)
|
|
25
|
+
PASSWORD_RESET_EMAIL = "Code For Life Password Reset <" + app_settings.EMAIL_ADDRESS + ">"
|
|
25
26
|
INVITE_FROM = "Code For Life Invitation <" + app_settings.EMAIL_ADDRESS + ">"
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
class DotmailerUserType(Enum):
|
|
30
|
+
TEACHER = auto()
|
|
31
|
+
STUDENT = auto()
|
|
32
|
+
NO_ACCOUNT = auto()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def generate_token(user, new_email="", preverified=False):
|
|
36
|
+
if preverified:
|
|
37
|
+
user.userprofile.is_verified = preverified
|
|
38
|
+
user.userprofile.save()
|
|
39
|
+
|
|
40
|
+
return generate_token_for_email(user.email, new_email)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def generate_token_for_email(email: str, new_email: str = ""):
|
|
44
|
+
return jwt.encode(
|
|
45
|
+
{
|
|
46
|
+
"email": email,
|
|
47
|
+
"new_email": new_email,
|
|
48
|
+
"email_verification_token": uuid4().hex[:30],
|
|
49
|
+
"expires": (timezone.now() + datetime.timedelta(hours=1)).timestamp(),
|
|
50
|
+
},
|
|
51
|
+
settings.SECRET_KEY,
|
|
52
|
+
algorithm="HS256",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _newsletter_ticked(data):
|
|
57
|
+
return "newsletter_ticked" in data and data["newsletter_ticked"]
|
|
58
|
+
|
|
59
|
+
|
|
28
60
|
def send_email(
|
|
29
61
|
sender,
|
|
30
62
|
recipients,
|
|
31
63
|
subject,
|
|
32
64
|
text_content,
|
|
33
|
-
|
|
65
|
+
title,
|
|
66
|
+
replace_url=None,
|
|
34
67
|
plaintext_template="email.txt",
|
|
35
68
|
html_template="email.html",
|
|
36
69
|
):
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# render templates
|
|
48
|
-
plaintext_body = plaintext.render(plaintext_email_context)
|
|
49
|
-
html_body = html.render(html_email_context)
|
|
50
|
-
|
|
51
|
-
# make message using templates
|
|
52
|
-
message = EmailMultiAlternatives(subject, plaintext_body, sender, recipients)
|
|
53
|
-
message.attach_alternative(html_body, "text/html")
|
|
54
|
-
|
|
55
|
-
message.send()
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def generate_token(user, email="", preverified=False):
|
|
59
|
-
return EmailVerification.objects.create(
|
|
60
|
-
user=user,
|
|
61
|
-
email=email,
|
|
62
|
-
token=uuid4().hex[:30],
|
|
63
|
-
expiry=timezone.now() + datetime.timedelta(hours=1),
|
|
64
|
-
verified=preverified,
|
|
70
|
+
django_send_email(
|
|
71
|
+
sender,
|
|
72
|
+
recipients,
|
|
73
|
+
subject,
|
|
74
|
+
text_content,
|
|
75
|
+
title,
|
|
76
|
+
replace_url,
|
|
77
|
+
plaintext_template,
|
|
78
|
+
html_template,
|
|
65
79
|
)
|
|
66
80
|
|
|
67
81
|
|
|
68
|
-
def send_verification_email(request, user, new_email=None):
|
|
69
|
-
"""
|
|
82
|
+
def send_verification_email(request, user, data, new_email=None, age=None, school=None):
|
|
83
|
+
"""
|
|
84
|
+
Sends emails relating to email address verification.
|
|
70
85
|
|
|
71
|
-
|
|
72
|
-
|
|
86
|
+
On registration:
|
|
87
|
+
- if the user is under 13, send a verification email addressed to the parent / guardian
|
|
88
|
+
- if the user is over 13, send a regular verification email
|
|
89
|
+
- if the user is a student who just got released, send a verification email explaining the situation
|
|
90
|
+
- if the user is a student who has requested to sign up to the newsletter, handle their Dotmailer subscription
|
|
73
91
|
|
|
74
|
-
|
|
92
|
+
On email address update:
|
|
93
|
+
- sends an email to the old address alerting the user that an email change request has occurred
|
|
94
|
+
- sends an email to the new address requesting email address verification
|
|
75
95
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
:param request: The Django form request
|
|
97
|
+
:param user: The user object concerned with the email verification
|
|
98
|
+
:param data: The data inputted by the user in the Django form.
|
|
99
|
+
:param new_email: New email the user wants to associate to their account - if not provided, it means the user is
|
|
100
|
+
registering a new account
|
|
101
|
+
:param age: The user's age (collected only for the purposes of this function and if the user is an independent
|
|
102
|
+
student)
|
|
103
|
+
"""
|
|
83
104
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
)
|
|
105
|
+
# verifying first email address (registration or unverified login attempt)
|
|
106
|
+
if not new_email:
|
|
107
|
+
verification = generate_token(user)
|
|
88
108
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
if age is None:
|
|
110
|
+
# if the user is a released student
|
|
111
|
+
if hasattr(user, "new_student") and school is not None:
|
|
112
|
+
url = f"{app_settings.domain(request)}{reverse('verify_email', kwargs={'token': verification})}"
|
|
113
|
+
|
|
114
|
+
send_dotdigital_email(
|
|
115
|
+
campaign_ids["verify_released_student"],
|
|
116
|
+
[user.email],
|
|
117
|
+
personalization_values={
|
|
118
|
+
"VERIFICATION_LINK": url,
|
|
119
|
+
"SCHOOL_NAME": school.name,
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
url = f"{app_settings.domain(request)}{reverse('verify_email', kwargs={'token': verification})}"
|
|
124
|
+
|
|
125
|
+
send_dotdigital_email(
|
|
126
|
+
campaign_ids["verify_new_user"],
|
|
127
|
+
[user.email],
|
|
128
|
+
personalization_values={"VERIFICATION_LINK": url},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if _newsletter_ticked(data):
|
|
132
|
+
add_to_dotmailer(
|
|
133
|
+
user.first_name,
|
|
134
|
+
user.last_name,
|
|
135
|
+
user.email,
|
|
136
|
+
address_book_ids["newsletter"],
|
|
137
|
+
DotmailerUserType.TEACHER,
|
|
138
|
+
)
|
|
139
|
+
# if the user is an independent student
|
|
140
|
+
else:
|
|
141
|
+
if age < 13:
|
|
142
|
+
url = f"{app_settings.domain(request)}{reverse('verify_email', kwargs={'token': verification})}"
|
|
143
|
+
send_dotdigital_email(
|
|
144
|
+
campaign_ids["verify_new_user_via_parent"],
|
|
145
|
+
[user.email],
|
|
146
|
+
personalization_values={
|
|
147
|
+
"FIRST_NAME": user.first_name,
|
|
148
|
+
"ACTIVATION_LINK": url,
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
url = f"{app_settings.domain(request)}{reverse('verify_email', kwargs={'token': verification})}"
|
|
153
|
+
send_dotdigital_email(
|
|
154
|
+
campaign_ids["verify_new_user"],
|
|
155
|
+
[user.email],
|
|
156
|
+
personalization_values={"VERIFICATION_LINK": url},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if _newsletter_ticked(data):
|
|
160
|
+
add_to_dotmailer(
|
|
161
|
+
user.first_name,
|
|
162
|
+
user.last_name,
|
|
163
|
+
user.email,
|
|
164
|
+
address_book_ids["newsletter"],
|
|
165
|
+
DotmailerUserType.STUDENT,
|
|
166
|
+
)
|
|
167
|
+
# verifying change of email address.
|
|
168
|
+
else:
|
|
169
|
+
verification = generate_token(user, new_email)
|
|
170
|
+
url = f"{app_settings.domain(request)}{reverse('verify_email', kwargs={'token': verification})}"
|
|
171
|
+
send_dotdigital_email(
|
|
172
|
+
campaign_ids["email_change_verification"],
|
|
173
|
+
[new_email],
|
|
174
|
+
personalization_values={"VERIFICATION_LINK": url},
|
|
92
175
|
)
|
|
93
176
|
|
|
94
177
|
|
|
95
|
-
def
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
178
|
+
def add_to_dotmailer(
|
|
179
|
+
first_name: str,
|
|
180
|
+
last_name: str,
|
|
181
|
+
email: str,
|
|
182
|
+
address_book_id: int,
|
|
183
|
+
user_type: DotmailerUserType = None,
|
|
184
|
+
):
|
|
102
185
|
try:
|
|
103
186
|
create_contact(first_name, last_name, email)
|
|
104
|
-
add_contact_to_address_book(first_name, last_name, email)
|
|
187
|
+
add_contact_to_address_book(first_name, last_name, email, address_book_id, user_type)
|
|
105
188
|
except RequestException:
|
|
106
189
|
return HttpResponse(status=404)
|
|
107
190
|
|
|
@@ -125,7 +208,7 @@ def create_contact(first_name, last_name, email):
|
|
|
125
208
|
{
|
|
126
209
|
"key": "DATETIMECONSENTED",
|
|
127
210
|
"value": datetime.datetime.now().__str__(),
|
|
128
|
-
}
|
|
211
|
+
}
|
|
129
212
|
]
|
|
130
213
|
}
|
|
131
214
|
],
|
|
@@ -139,8 +222,15 @@ def create_contact(first_name, last_name, email):
|
|
|
139
222
|
)
|
|
140
223
|
|
|
141
224
|
|
|
142
|
-
def add_contact_to_address_book(
|
|
143
|
-
|
|
225
|
+
def add_contact_to_address_book(
|
|
226
|
+
first_name: str,
|
|
227
|
+
last_name: str,
|
|
228
|
+
email: str,
|
|
229
|
+
address_book_id: int,
|
|
230
|
+
user_type: DotmailerUserType = None,
|
|
231
|
+
):
|
|
232
|
+
main_address_book_url = f"https://r1-api.dotmailer.com/v2/address-books/{address_book_id}/contacts"
|
|
233
|
+
|
|
144
234
|
body = {
|
|
145
235
|
"email": email,
|
|
146
236
|
"optInType": "VerifiedDouble",
|
|
@@ -153,19 +243,47 @@ def add_contact_to_address_book(first_name, last_name, email):
|
|
|
153
243
|
}
|
|
154
244
|
|
|
155
245
|
post(
|
|
156
|
-
|
|
246
|
+
main_address_book_url,
|
|
157
247
|
json=body,
|
|
158
248
|
auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
|
|
159
249
|
)
|
|
160
250
|
|
|
251
|
+
if user_type is not None:
|
|
252
|
+
specific_address_book_url = app_settings.DOTMAILER_NO_ACCOUNT_ADDRESS_BOOK_URL
|
|
253
|
+
|
|
254
|
+
if user_type == DotmailerUserType.TEACHER:
|
|
255
|
+
specific_address_book_url = app_settings.DOTMAILER_TEACHER_ADDRESS_BOOK_URL
|
|
256
|
+
elif user_type == DotmailerUserType.STUDENT:
|
|
257
|
+
specific_address_book_url = app_settings.DOTMAILER_STUDENT_ADDRESS_BOOK_URL
|
|
258
|
+
|
|
259
|
+
post(
|
|
260
|
+
specific_address_book_url,
|
|
261
|
+
json=body,
|
|
262
|
+
auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def delete_contact(email: str):
|
|
267
|
+
try:
|
|
268
|
+
user = get_dotmailer_user_by_email(email)
|
|
269
|
+
user_id = user.get("id")
|
|
270
|
+
if user_id:
|
|
271
|
+
url = app_settings.DOTMAILER_DELETE_USER_BY_ID_URL.replace("ID", str(user_id))
|
|
272
|
+
delete(
|
|
273
|
+
url,
|
|
274
|
+
auth=(
|
|
275
|
+
app_settings.DOTMAILER_USER,
|
|
276
|
+
app_settings.DOTMAILER_PASSWORD,
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
except RequestException:
|
|
280
|
+
return HttpResponse(status=404)
|
|
281
|
+
|
|
161
282
|
|
|
162
283
|
def get_dotmailer_user_by_email(email):
|
|
163
284
|
url = app_settings.DOTMAILER_GET_USER_BY_EMAIL_URL.replace("EMAIL", email)
|
|
164
285
|
|
|
165
|
-
response = get(
|
|
166
|
-
url,
|
|
167
|
-
auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
|
|
168
|
-
)
|
|
286
|
+
response = get(url, auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD))
|
|
169
287
|
|
|
170
288
|
return json.loads(response.content)
|
|
171
289
|
|
|
@@ -173,9 +291,7 @@ def get_dotmailer_user_by_email(email):
|
|
|
173
291
|
def add_consent_record_to_dotmailer_user(user):
|
|
174
292
|
consent_date_time = datetime.datetime.now().__str__()
|
|
175
293
|
|
|
176
|
-
url = app_settings.DOTMAILER_PUT_CONSENT_DATA_URL.replace(
|
|
177
|
-
"USER_ID", str(user["id"])
|
|
178
|
-
)
|
|
294
|
+
url = app_settings.DOTMAILER_PUT_CONSENT_DATA_URL.replace("USER_ID", str(user["id"]))
|
|
179
295
|
body = {
|
|
180
296
|
"contact": {
|
|
181
297
|
"email": user["email"],
|
|
@@ -183,13 +299,7 @@ def add_consent_record_to_dotmailer_user(user):
|
|
|
183
299
|
"emailType": user["emailType"],
|
|
184
300
|
"dataFields": user["dataFields"],
|
|
185
301
|
},
|
|
186
|
-
"consentFields": [
|
|
187
|
-
{
|
|
188
|
-
"fields": [
|
|
189
|
-
{"key": "DATETIMECONSENTED", "value": consent_date_time},
|
|
190
|
-
]
|
|
191
|
-
}
|
|
192
|
-
],
|
|
302
|
+
"consentFields": [{"fields": [{"key": "DATETIMECONSENTED", "value": consent_date_time}]}],
|
|
193
303
|
}
|
|
194
304
|
|
|
195
305
|
put(
|
|
@@ -202,10 +312,7 @@ def add_consent_record_to_dotmailer_user(user):
|
|
|
202
312
|
def send_dotmailer_consent_confirmation_email_to_user(user):
|
|
203
313
|
url = app_settings.DOTMAILER_SEND_CAMPAIGN_URL
|
|
204
314
|
campaign_id = app_settings.DOTMAILER_THANKS_FOR_STAYING_CAMPAIGN_ID
|
|
205
|
-
body = {
|
|
206
|
-
"campaignID": campaign_id,
|
|
207
|
-
"contactIds": [str(user["id"])],
|
|
208
|
-
}
|
|
315
|
+
body = {"campaignID": campaign_id, "contactIds": [str(user["id"])]}
|
|
209
316
|
|
|
210
317
|
post(
|
|
211
318
|
url,
|
|
@@ -214,30 +321,43 @@ def send_dotmailer_consent_confirmation_email_to_user(user):
|
|
|
214
321
|
)
|
|
215
322
|
|
|
216
323
|
|
|
217
|
-
def
|
|
324
|
+
def update_indy_email(user, request, data):
|
|
218
325
|
changing_email = False
|
|
219
326
|
new_email = data["email"]
|
|
220
327
|
|
|
221
|
-
if new_email != "" and new_email != user.
|
|
328
|
+
if new_email != "" and new_email != user.email:
|
|
222
329
|
changing_email = True
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
330
|
+
users_with_email = User.objects.filter(email=new_email)
|
|
331
|
+
|
|
332
|
+
send_dotdigital_email(
|
|
333
|
+
campaign_ids["email_change_notification"],
|
|
334
|
+
[user.email],
|
|
335
|
+
personalization_values={"NEW_EMAIL_ADDRESS": new_email},
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# email is available
|
|
339
|
+
if not users_with_email.exists():
|
|
232
340
|
# new email to set and verify
|
|
233
|
-
send_verification_email(request, user
|
|
341
|
+
send_verification_email(request, user, data, new_email)
|
|
234
342
|
return changing_email, new_email
|
|
235
343
|
|
|
236
344
|
|
|
237
|
-
def
|
|
238
|
-
|
|
239
|
-
|
|
345
|
+
def update_email(user: Teacher or Student, request, data):
|
|
346
|
+
changing_email = False
|
|
347
|
+
new_email = data["email"]
|
|
240
348
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
349
|
+
if new_email != "" and new_email != user.new_user.email:
|
|
350
|
+
changing_email = True
|
|
351
|
+
users_with_email = User.objects.filter(email=new_email)
|
|
352
|
+
|
|
353
|
+
send_dotdigital_email(
|
|
354
|
+
campaign_ids["email_change_notification"],
|
|
355
|
+
[user.new_user.email],
|
|
356
|
+
personalization_values={"NEW_EMAIL_ADDRESS": new_email},
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# email is available
|
|
360
|
+
if not users_with_email.exists():
|
|
361
|
+
# new email to set and verify
|
|
362
|
+
send_verification_email(request, user.new_user, data, new_email)
|
|
363
|
+
return changing_email, new_email
|
common/helpers/generators.py
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# TODO: Move to Address model once we create it
|
|
2
|
+
def sanitise_uk_postcode(postcode):
|
|
3
|
+
if len(postcode) >= 5: # Valid UK postcodes are at least 5 chars long
|
|
4
|
+
outcode = postcode[:-3] # UK incodes are always 3 characters
|
|
5
|
+
|
|
6
|
+
# Insert a space between outcode and incode if there isn't already one
|
|
7
|
+
if not outcode.endswith(" "):
|
|
8
|
+
postcode = postcode[:-3] + " " + postcode[-3:]
|
|
9
|
+
|
|
10
|
+
return postcode
|
common/mail.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
from common import app_settings
|
|
6
|
+
from common.app_settings import MODULE_NAME, domain
|
|
7
|
+
from django.core.mail import EmailMultiAlternatives
|
|
8
|
+
from django.template import loader
|
|
9
|
+
|
|
10
|
+
campaign_ids = {
|
|
11
|
+
"admin_given": 1569057,
|
|
12
|
+
"admin_revoked": 1569071,
|
|
13
|
+
"delete_account": 1567477,
|
|
14
|
+
"email_change_notification": 1551600,
|
|
15
|
+
"email_change_verification": 1551594,
|
|
16
|
+
"invite_teacher_with_account": 1569599,
|
|
17
|
+
"invite_teacher_without_account": 1569607,
|
|
18
|
+
"level_creation": 1570259,
|
|
19
|
+
"reset_password": 1557153,
|
|
20
|
+
"student_join_request_notification": 1569486,
|
|
21
|
+
"student_join_request_rejected": 1569470,
|
|
22
|
+
"student_join_request_sent": 1569477,
|
|
23
|
+
"teacher_released": 1569537,
|
|
24
|
+
"user_already_registered": 1569539,
|
|
25
|
+
"verify_new_user": 1551577,
|
|
26
|
+
"verify_new_user_first_reminder": 1557170,
|
|
27
|
+
"verify_new_user_second_reminder": 1557173,
|
|
28
|
+
"verify_new_user_via_parent": 1551587,
|
|
29
|
+
"verify_released_student": 1580574,
|
|
30
|
+
"inactive_users_on_website_first_reminder": 1604381,
|
|
31
|
+
"inactive_users_on_website_second_reminder": 1606208,
|
|
32
|
+
"inactive_users_on_website_final_reminder": 1606215,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
address_book_ids = {
|
|
36
|
+
"newsletter": 9705772,
|
|
37
|
+
"donors": 37649245,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def add_contact(email: str):
|
|
42
|
+
"""Add a new contact to Dotdigital."""
|
|
43
|
+
# TODO: implement
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def remove_contact(email: str):
|
|
47
|
+
"""Remove an existing contact from Dotdigital."""
|
|
48
|
+
# TODO: implement
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class EmailAttachment:
|
|
53
|
+
"""An email attachment for a Dotdigital triggered campaign."""
|
|
54
|
+
|
|
55
|
+
file_name: str
|
|
56
|
+
mime_type: str
|
|
57
|
+
content: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def django_send_email(
|
|
61
|
+
sender,
|
|
62
|
+
recipients,
|
|
63
|
+
subject,
|
|
64
|
+
text_content,
|
|
65
|
+
title,
|
|
66
|
+
replace_url=None,
|
|
67
|
+
plaintext_template="email.txt",
|
|
68
|
+
html_template="email.html",
|
|
69
|
+
):
|
|
70
|
+
# add in template for templates to message
|
|
71
|
+
|
|
72
|
+
# setup templates
|
|
73
|
+
plaintext = loader.get_template(plaintext_template)
|
|
74
|
+
html = loader.get_template(html_template)
|
|
75
|
+
plaintext_email_context = {"content": text_content}
|
|
76
|
+
html_email_context = {
|
|
77
|
+
"content": text_content,
|
|
78
|
+
"title": title,
|
|
79
|
+
"url_prefix": domain(),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# render templates
|
|
83
|
+
plaintext_body = plaintext.render(plaintext_email_context)
|
|
84
|
+
original_html_body = html.render(html_email_context)
|
|
85
|
+
html_body = original_html_body
|
|
86
|
+
|
|
87
|
+
if replace_url:
|
|
88
|
+
verify_url = replace_url["verify_url"]
|
|
89
|
+
verify_replace_url = re.sub(
|
|
90
|
+
f"(.*/verify_email/)(.*)", f"\\1", verify_url
|
|
91
|
+
)
|
|
92
|
+
html_body = re.sub(
|
|
93
|
+
f"({verify_url})(.*){verify_url}",
|
|
94
|
+
f"\\1\\2{verify_replace_url}",
|
|
95
|
+
original_html_body,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# make message using templates
|
|
99
|
+
message = EmailMultiAlternatives(
|
|
100
|
+
subject, plaintext_body, sender, recipients
|
|
101
|
+
)
|
|
102
|
+
message.attach_alternative(html_body, "text/html")
|
|
103
|
+
|
|
104
|
+
message.send()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# pylint: disable-next=too-many-arguments
|
|
108
|
+
def send_dotdigital_email(
|
|
109
|
+
campaign_id: int,
|
|
110
|
+
to_addresses: t.List[str],
|
|
111
|
+
cc_addresses: t.Optional[t.List[str]] = None,
|
|
112
|
+
bcc_addresses: t.Optional[t.List[str]] = None,
|
|
113
|
+
from_address: t.Optional[str] = None,
|
|
114
|
+
personalization_values: t.Optional[t.Dict[str, str]] = None,
|
|
115
|
+
metadata: t.Optional[str] = None,
|
|
116
|
+
attachments: t.Optional[t.List[EmailAttachment]] = None,
|
|
117
|
+
region: str = "r1",
|
|
118
|
+
auth: t.Optional[str] = None,
|
|
119
|
+
timeout: int = 30,
|
|
120
|
+
):
|
|
121
|
+
# pylint: disable=line-too-long
|
|
122
|
+
"""Send a triggered email campaign using DotDigital's API.
|
|
123
|
+
|
|
124
|
+
https://developer.dotdigital.com/reference/send-transactional-email-using-a-triggered-campaign
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
campaign_id: The ID of the triggered campaign, which needs to be included within the request body.
|
|
128
|
+
to_addresses: The email address(es) to send to.
|
|
129
|
+
cc_addresses: The CC email address or address to to send to. separate email addresses with a comma. Maximum: 100.
|
|
130
|
+
bcc_addresses: The BCC email address or address to to send to. separate email addresses with a comma. Maximum: 100.
|
|
131
|
+
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.
|
|
132
|
+
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.
|
|
133
|
+
metadata: The metadata for your email. It can be either a single value or a series of values in a JSON object.
|
|
134
|
+
attachments: A Base64 encoded string. All attachment types are supported. Maximum file size: 15 MB.
|
|
135
|
+
region: The Dotdigital region id your account belongs to e.g. r1, r2 or r3.
|
|
136
|
+
auth: The authorization header used to enable API access. If None, the value will be retrieved from the DOTDIGITAL_AUTH environment variable.
|
|
137
|
+
timeout: Send timeout to avoid hanging.
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
AssertionError: If failed to send email.
|
|
141
|
+
"""
|
|
142
|
+
# pylint: enable=line-too-long
|
|
143
|
+
|
|
144
|
+
# Dotdigital emails don't work locally, so if testing emails locally use Django to send a dummy email instead
|
|
145
|
+
if MODULE_NAME == "local":
|
|
146
|
+
django_send_email(
|
|
147
|
+
from_address,
|
|
148
|
+
to_addresses,
|
|
149
|
+
"dummy_subject",
|
|
150
|
+
"dummy_text_content",
|
|
151
|
+
"dummy_title",
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
if auth is None:
|
|
155
|
+
auth = app_settings.DOTDIGITAL_AUTH
|
|
156
|
+
|
|
157
|
+
body = {
|
|
158
|
+
"campaignId": campaign_id,
|
|
159
|
+
"toAddresses": to_addresses,
|
|
160
|
+
}
|
|
161
|
+
if cc_addresses is not None:
|
|
162
|
+
body["ccAddresses"] = cc_addresses
|
|
163
|
+
if bcc_addresses is not None:
|
|
164
|
+
body["bccAddresses"] = bcc_addresses
|
|
165
|
+
if from_address is not None:
|
|
166
|
+
body["fromAddress"] = from_address
|
|
167
|
+
if personalization_values is not None:
|
|
168
|
+
body["personalizationValues"] = [
|
|
169
|
+
{
|
|
170
|
+
"name": key,
|
|
171
|
+
"value": value,
|
|
172
|
+
}
|
|
173
|
+
for key, value in personalization_values.items()
|
|
174
|
+
]
|
|
175
|
+
if metadata is not None:
|
|
176
|
+
body["metadata"] = metadata
|
|
177
|
+
if attachments is not None:
|
|
178
|
+
body["attachments"] = [
|
|
179
|
+
{
|
|
180
|
+
"fileName": attachment.file_name,
|
|
181
|
+
"mimeType": attachment.mime_type,
|
|
182
|
+
"content": attachment.content,
|
|
183
|
+
}
|
|
184
|
+
for attachment in attachments
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
response = requests.post(
|
|
188
|
+
url=f"https://{region}-api.dotdigital.com/v2/email/triggered-campaign",
|
|
189
|
+
json=body,
|
|
190
|
+
headers={
|
|
191
|
+
"accept": "text/plain",
|
|
192
|
+
"authorization": auth,
|
|
193
|
+
},
|
|
194
|
+
timeout=timeout,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
assert response.ok, (
|
|
198
|
+
"Failed to send email."
|
|
199
|
+
f" Reason: {response.reason}."
|
|
200
|
+
f" Text: {response.text}."
|
|
201
|
+
)
|
|
@@ -32,9 +32,7 @@ class Migration(migrations.Migration):
|
|
|
32
32
|
("token", models.CharField(max_length=30)),
|
|
33
33
|
(
|
|
34
34
|
"email",
|
|
35
|
-
models.CharField(
|
|
36
|
-
blank=True, default=None, max_length=200, null=True
|
|
37
|
-
),
|
|
35
|
+
models.CharField(blank=True, default=None, max_length=200, null=True),
|
|
38
36
|
),
|
|
39
37
|
("expiry", models.DateTimeField()),
|
|
40
38
|
("verified", models.BooleanField(default=False)),
|