codeforlife-portal 6.42.0__py2.py3-none-any.whl → 6.43.1__py2.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.

Potentially problematic release.


This version of codeforlife-portal might be problematic. Click here for more details.

@@ -29,30 +29,6 @@ def kickedEmail(request, schoolName):
29
29
  }
30
30
 
31
31
 
32
- def adminGivenEmail(request, schoolName):
33
-
34
- url = request.build_absolute_uri(reverse("dashboard"))
35
-
36
- return {
37
- "subject": f"You have been made a school or club administrator",
38
- "message": (
39
- f"Administrator control of the school or club '{schoolName}' has been "
40
- f"given to you. Go to {url} to start managing your school or club."
41
- ),
42
- }
43
-
44
-
45
- def adminRevokedEmail(request, schoolName):
46
- return {
47
- "subject": f"You are no longer a school or club administrator",
48
- "message": (
49
- f"Your administrator control of the school or club '{schoolName}' has been "
50
- f"revoked. If you think this is an error, please contact one of the other "
51
- f"administrators in your school or club."
52
- ),
53
- }
54
-
55
-
56
32
  def studentJoinRequestSentEmail(request, schoolName, accessCode):
57
33
  return {
58
34
  "subject": f"School or club join request sent",
@@ -6,14 +6,11 @@ from uuid import uuid4
6
6
 
7
7
  import jwt
8
8
  from common import app_settings
9
- from common.app_settings import domain
10
- from common.mail import campaign_ids, send_dotdigital_email
9
+ from common.mail import campaign_ids, django_send_email, send_dotdigital_email
11
10
  from common.models import Student, Teacher
12
11
  from django.conf import settings
13
12
  from django.contrib.auth.models import User
14
- from django.core.mail import EmailMultiAlternatives
15
13
  from django.http import HttpResponse
16
- from django.template import loader
17
14
  from django.urls import reverse
18
15
  from django.utils import timezone
19
16
  from requests import delete, get, post, put
@@ -31,41 +28,6 @@ class DotmailerUserType(Enum):
31
28
  NO_ACCOUNT = auto()
32
29
 
33
30
 
34
- def send_email(
35
- sender,
36
- recipients,
37
- subject,
38
- text_content,
39
- title,
40
- replace_url=None,
41
- plaintext_template="email.txt",
42
- html_template="email.html",
43
- ):
44
- # add in template for templates to message
45
-
46
- # setup templates
47
- plaintext = loader.get_template(plaintext_template)
48
- html = loader.get_template(html_template)
49
- plaintext_email_context = {"content": text_content}
50
- html_email_context = {"content": text_content, "title": title, "url_prefix": domain()}
51
-
52
- # render templates
53
- plaintext_body = plaintext.render(plaintext_email_context)
54
- original_html_body = html.render(html_email_context)
55
- html_body = original_html_body
56
-
57
- if replace_url:
58
- verify_url = replace_url["verify_url"]
59
- verify_replace_url = re.sub(f"(.*/verify_email/)(.*)", f"\\1", verify_url)
60
- html_body = re.sub(f"({verify_url})(.*){verify_url}", f"\\1\\2{verify_replace_url}", original_html_body)
61
-
62
- # make message using templates
63
- message = EmailMultiAlternatives(subject, plaintext_body, sender, recipients)
64
- message.attach_alternative(html_body, "text/html")
65
-
66
- message.send()
67
-
68
-
69
31
  def generate_token(user, new_email="", preverified=False):
70
32
  if preverified:
71
33
  user.userprofile.is_verified = preverified
@@ -91,6 +53,19 @@ def _newsletter_ticked(data):
91
53
  return "newsletter_ticked" in data and data["newsletter_ticked"]
92
54
 
93
55
 
56
+ def send_email(
57
+ sender,
58
+ recipients,
59
+ subject,
60
+ text_content,
61
+ title,
62
+ replace_url=None,
63
+ plaintext_template="email.txt",
64
+ html_template="email.html",
65
+ ):
66
+ django_send_email(sender, recipients, subject, text_content, title, replace_url, plaintext_template, html_template)
67
+
68
+
94
69
  def send_verification_email(request, user, data, new_email=None, age=None):
95
70
  """
96
71
  Sends emails relating to email address verification.
cfl_common/common/mail.py CHANGED
@@ -3,8 +3,13 @@ from dataclasses import dataclass
3
3
 
4
4
  import requests
5
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
6
9
 
7
10
  campaign_ids = {
11
+ "admin_given": 1569057,
12
+ "admin_revoked": 1569071,
8
13
  "email_change_notification": 1551600,
9
14
  "email_change_verification": 1551594,
10
15
  "reset_password": 1557153,
@@ -34,6 +39,41 @@ class EmailAttachment:
34
39
  content: str
35
40
 
36
41
 
42
+ def django_send_email(
43
+ sender,
44
+ recipients,
45
+ subject,
46
+ text_content,
47
+ title,
48
+ replace_url=None,
49
+ plaintext_template="email.txt",
50
+ html_template="email.html",
51
+ ):
52
+ # add in template for templates to message
53
+
54
+ # setup templates
55
+ plaintext = loader.get_template(plaintext_template)
56
+ html = loader.get_template(html_template)
57
+ plaintext_email_context = {"content": text_content}
58
+ html_email_context = {"content": text_content, "title": title, "url_prefix": domain()}
59
+
60
+ # render templates
61
+ plaintext_body = plaintext.render(plaintext_email_context)
62
+ original_html_body = html.render(html_email_context)
63
+ html_body = original_html_body
64
+
65
+ if replace_url:
66
+ verify_url = replace_url["verify_url"]
67
+ verify_replace_url = re.sub(f"(.*/verify_email/)(.*)", f"\\1", verify_url)
68
+ html_body = re.sub(f"({verify_url})(.*){verify_url}", f"\\1\\2{verify_replace_url}", original_html_body)
69
+
70
+ # make message using templates
71
+ message = EmailMultiAlternatives(subject, plaintext_body, sender, recipients)
72
+ message.attach_alternative(html_body, "text/html")
73
+
74
+ message.send()
75
+
76
+
37
77
  # pylint: disable-next=too-many-arguments
38
78
  def send_dotdigital_email(
39
79
  campaign_id: int,
@@ -71,47 +111,51 @@ def send_dotdigital_email(
71
111
  """
72
112
  # pylint: enable=line-too-long
73
113
 
74
- if auth is None:
75
- auth = app_settings.DOTDIGITAL_AUTH
76
-
77
- body = {
78
- "campaignId": campaign_id,
79
- "toAddresses": to_addresses,
80
- }
81
- if cc_addresses is not None:
82
- body["ccAddresses"] = cc_addresses
83
- if bcc_addresses is not None:
84
- body["bccAddresses"] = bcc_addresses
85
- if from_address is not None:
86
- body["fromAddress"] = from_address
87
- if personalization_values is not None:
88
- body["personalizationValues"] = [
89
- {
90
- "name": key,
91
- "value": value,
92
- }
93
- for key, value in personalization_values.items()
94
- ]
95
- if metadata is not None:
96
- body["metadata"] = metadata
97
- if attachments is not None:
98
- body["attachments"] = [
99
- {
100
- "fileName": attachment.file_name,
101
- "mimeType": attachment.mime_type,
102
- "content": attachment.content,
103
- }
104
- for attachment in attachments
105
- ]
106
-
107
- response = requests.post(
108
- url=f"https://{region}-api.dotdigital.com/v2/email/triggered-campaign",
109
- json=body,
110
- headers={
111
- "accept": "text/plain",
112
- "authorization": auth,
113
- },
114
- timeout=timeout,
115
- )
116
-
117
- assert response.ok, "Failed to send email." f" Reason: {response.reason}." f" Text: {response.text}."
114
+ # Dotdigital emails don't work locally, so if testing emails locally use Django to send a dummy email instead
115
+ if MODULE_NAME == "local":
116
+ django_send_email(from_address, to_addresses, "dummy_subject", "dummy_text_content", "dummy_title")
117
+ else:
118
+ if auth is None:
119
+ auth = app_settings.DOTDIGITAL_AUTH
120
+
121
+ body = {
122
+ "campaignId": campaign_id,
123
+ "toAddresses": to_addresses,
124
+ }
125
+ if cc_addresses is not None:
126
+ body["ccAddresses"] = cc_addresses
127
+ if bcc_addresses is not None:
128
+ body["bccAddresses"] = bcc_addresses
129
+ if from_address is not None:
130
+ body["fromAddress"] = from_address
131
+ if personalization_values is not None:
132
+ body["personalizationValues"] = [
133
+ {
134
+ "name": key,
135
+ "value": value,
136
+ }
137
+ for key, value in personalization_values.items()
138
+ ]
139
+ if metadata is not None:
140
+ body["metadata"] = metadata
141
+ if attachments is not None:
142
+ body["attachments"] = [
143
+ {
144
+ "fileName": attachment.file_name,
145
+ "mimeType": attachment.mime_type,
146
+ "content": attachment.content,
147
+ }
148
+ for attachment in attachments
149
+ ]
150
+
151
+ response = requests.post(
152
+ url=f"https://{region}-api.dotdigital.com/v2/email/triggered-campaign",
153
+ json=body,
154
+ headers={
155
+ "accept": "text/plain",
156
+ "authorization": auth,
157
+ },
158
+ timeout=timeout,
159
+ )
160
+
161
+ assert response.ok, "Failed to send email." f" Reason: {response.reason}." f" Text: {response.text}."
@@ -0,0 +1,29 @@
1
+ from django.apps.registry import Apps
2
+ from django.db import migrations
3
+
4
+ from portal.views.api import __anonymise_user
5
+
6
+
7
+ def anonymise_orphan_users(apps: Apps, *args):
8
+ """
9
+ Users should never exist without a user-type linked to them. Anonymise all
10
+ instances of User objects without a Teacher or Student instance.
11
+ """
12
+ User = apps.get_model("auth", "User")
13
+
14
+ active_orphan_users = User.objects.filter(
15
+ new_teacher__isnull=True, new_student__isnull=True, is_active=True
16
+ )
17
+
18
+ for active_orphan_user in active_orphan_users:
19
+ __anonymise_user(active_orphan_user)
20
+
21
+
22
+ class Migration(migrations.Migration):
23
+ dependencies = [("common", "0048_unique_school_names")]
24
+
25
+ operations = [
26
+ migrations.RunPython(
27
+ code=anonymise_orphan_users, reverse_code=migrations.RunPython.noop
28
+ ),
29
+ ]
@@ -0,0 +1,30 @@
1
+ from uuid import uuid4
2
+
3
+ from django.apps.registry import Apps
4
+ from django.db import migrations
5
+
6
+
7
+ def anonymise_orphan_schools(apps: Apps, *args):
8
+ """
9
+ Schools without any teachers or students should be anonymised (inactive).
10
+ Mark all active orphan schools as inactive.
11
+ """
12
+ School = apps.get_model("common", "School")
13
+
14
+ active_orphan_schools = School.objects.filter(teacher_school__isnull=True)
15
+
16
+ for active_orphan_school in active_orphan_schools:
17
+ active_orphan_school.name = uuid4().hex
18
+ active_orphan_school.is_active = False
19
+ active_orphan_school.save()
20
+
21
+
22
+ class Migration(migrations.Migration):
23
+ dependencies = [("common", "0049_anonymise_orphan_users")]
24
+
25
+ operations = [
26
+ migrations.RunPython(
27
+ code=anonymise_orphan_schools,
28
+ reverse_code=migrations.RunPython.noop,
29
+ ),
30
+ ]
@@ -0,0 +1,30 @@
1
+ from django.apps.registry import Apps
2
+ from django.db import migrations
3
+
4
+
5
+ def verify_returning_users(apps: Apps, *args):
6
+ """
7
+ Users cannot be unverified after having logged in at least once. Grab all
8
+ instances of unverified UserProfile where the User has logged in and mark it
9
+ as verified.
10
+ """
11
+ UserProfile = apps.get_model("common", "UserProfile")
12
+
13
+ unverified_returning_userprofiles = UserProfile.objects.filter(
14
+ user__last_login__isnull=False, is_verified=False
15
+ )
16
+
17
+ for unverified_returning_userprofile in unverified_returning_userprofiles:
18
+ unverified_returning_userprofile.is_verified = True
19
+ unverified_returning_userprofile.save()
20
+
21
+
22
+ class Migration(migrations.Migration):
23
+ dependencies = [("common", "0050_anonymise_orphan_schools")]
24
+
25
+ operations = [
26
+ migrations.RunPython(
27
+ code=verify_returning_users,
28
+ reverse_code=migrations.RunPython.noop,
29
+ ),
30
+ ]
@@ -2,7 +2,6 @@ import re
2
2
  from datetime import timedelta
3
3
  from uuid import uuid4
4
4
 
5
- import pgeocode
6
5
  from django.contrib.auth.models import User
7
6
  from django.db import models
8
7
  from django.utils import timezone
@@ -0,0 +1,30 @@
1
+ import pytest
2
+ from django_test_migrations.migrator import Migrator
3
+
4
+
5
+ @pytest.mark.django_db
6
+ def test_migration_anonymise_orphan_schools(migrator: Migrator):
7
+ state = migrator.apply_initial_migration(
8
+ ("common", "0049_anonymise_orphan_users")
9
+ )
10
+ User = state.apps.get_model("auth", "User")
11
+ UserProfile = state.apps.get_model("common", "UserProfile")
12
+ Teacher = state.apps.get_model("common", "Teacher")
13
+ School = state.apps.get_model("common", "School")
14
+
15
+ orphan_school = School.objects.create(name="OrphanSchool")
16
+ teacher_school = School.objects.create(name="TeacherSchool")
17
+
18
+ teacher_user = User.objects.create_user("TeacherUser", password="password")
19
+ teacher_userprofile = UserProfile.objects.create(user=teacher_user)
20
+ Teacher.objects.create(
21
+ user=teacher_userprofile, new_user=teacher_user, school=teacher_school
22
+ )
23
+
24
+ migrator.apply_tested_migration(("common", "0050_anonymise_orphan_schools"))
25
+
26
+ def assert_school_anonymised(pk: int, anonymised: bool):
27
+ assert School.objects.get(pk=pk).is_active != anonymised
28
+
29
+ assert_school_anonymised(orphan_school.pk, True)
30
+ assert_school_anonymised(teacher_school.pk, False)
@@ -0,0 +1,30 @@
1
+ import pytest
2
+ from django_test_migrations.migrator import Migrator
3
+
4
+
5
+ @pytest.mark.django_db
6
+ def test_migration_anonymise_orphan_users(migrator: Migrator):
7
+ state = migrator.apply_initial_migration(
8
+ ("common", "0048_unique_school_names")
9
+ )
10
+ User = state.apps.get_model("auth", "User")
11
+ UserProfile = state.apps.get_model("common", "UserProfile")
12
+ Teacher = state.apps.get_model("common", "Teacher")
13
+ Student = state.apps.get_model("common", "Student")
14
+
15
+ orphan_user = User.objects.create_user("OrphanUser", password="password")
16
+ teacher_user = User.objects.create_user("TeacherUser", password="password")
17
+ student_user = User.objects.create_user("StudentUser", password="password")
18
+ teacher_userprofile = UserProfile.objects.create(user=teacher_user)
19
+ student_userprofile = UserProfile.objects.create(user=student_user)
20
+ Teacher.objects.create(user=teacher_userprofile, new_user=teacher_user)
21
+ Student.objects.create(user=student_userprofile, new_user=student_user)
22
+
23
+ migrator.apply_tested_migration(("common", "0049_anonymise_orphan_users"))
24
+
25
+ def assert_user_anonymised(pk: int, anonymised: bool):
26
+ assert User.objects.get(pk=pk).is_active != anonymised
27
+
28
+ assert_user_anonymised(orphan_user.pk, True)
29
+ assert_user_anonymised(teacher_user.pk, False)
30
+ assert_user_anonymised(student_user.pk, False)
@@ -3,8 +3,10 @@ from django_test_migrations.migrator import Migrator
3
3
 
4
4
 
5
5
  @pytest.mark.django_db
6
- def test_0048_unique_school_names(migrator: Migrator):
7
- state = migrator.apply_initial_migration(("common", "0047_delete_school_postcode"))
6
+ def test_migration_unique_school_names(migrator: Migrator):
7
+ state = migrator.apply_initial_migration(
8
+ ("common", "0047_delete_school_postcode")
9
+ )
8
10
  School = state.apps.get_model("common", "School")
9
11
 
10
12
  school_name = "ExampleSchool"
@@ -15,7 +17,9 @@ def test_0048_unique_school_names(migrator: Migrator):
15
17
  School(name=f"{school_name} 1"),
16
18
  ]
17
19
  )
18
- school_ids = list(School.objects.order_by("-id")[:3].values_list("id", flat=True))
20
+ school_ids = list(
21
+ School.objects.order_by("-id")[:3].values_list("id", flat=True)
22
+ )
19
23
  school_ids.reverse()
20
24
 
21
25
  migrator.apply_tested_migration(("common", "0048_unique_school_names"))
@@ -0,0 +1,59 @@
1
+ from datetime import datetime, timezone
2
+
3
+ import pytest
4
+ from django_test_migrations.migrator import Migrator
5
+
6
+ from portal.views.api import __anonymise_user
7
+
8
+
9
+ @pytest.mark.django_db
10
+ def test_migration_verify_returning_users(migrator: Migrator):
11
+ state = migrator.apply_initial_migration(
12
+ ("common", "0050_anonymise_orphan_schools")
13
+ )
14
+ User = state.apps.get_model("auth", "User")
15
+ UserProfile = state.apps.get_model("common", "UserProfile")
16
+
17
+ returning_user = User.objects.create_user(
18
+ "ReturningUser",
19
+ password="password",
20
+ last_login=datetime.now(tz=timezone.utc),
21
+ )
22
+ returning_userprofile = UserProfile.objects.create(user=returning_user)
23
+
24
+ non_returning_user = User.objects.create_user(
25
+ "NonReturningUser", password="password"
26
+ )
27
+ non_returning_userprofile = UserProfile.objects.create(
28
+ user=non_returning_user
29
+ )
30
+
31
+ anonymised_returning_user = User.objects.create_user(
32
+ "AnonReturningUser",
33
+ password="password",
34
+ last_login=datetime.now(tz=timezone.utc),
35
+ )
36
+ anonymised_returning_userprofile = UserProfile.objects.create(
37
+ user=anonymised_returning_user
38
+ )
39
+ __anonymise_user(anonymised_returning_user)
40
+
41
+ anonymised_non_returning_user = User.objects.create_user(
42
+ "AnonNonReturningUser", password="password"
43
+ )
44
+ anonymised_non_returning_userprofile = UserProfile.objects.create(
45
+ user=anonymised_non_returning_user
46
+ )
47
+ __anonymise_user(anonymised_non_returning_user)
48
+
49
+ migrator.apply_tested_migration(("common", "0051_verify_returning_users"))
50
+
51
+ def assert_userprofile_is_verified(pk: int, verified: bool):
52
+ assert UserProfile.objects.get(pk=pk).is_verified == verified
53
+
54
+ assert_userprofile_is_verified(returning_userprofile.pk, True)
55
+ assert_userprofile_is_verified(non_returning_userprofile.pk, False)
56
+ assert_userprofile_is_verified(anonymised_returning_userprofile.pk, True)
57
+ assert_userprofile_is_verified(
58
+ anonymised_non_returning_userprofile.pk, False
59
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codeforlife-portal
3
- Version: 6.42.0
3
+ Version: 6.43.1
4
4
  Classifier: Programming Language :: Python
5
5
  Classifier: Programming Language :: Python :: 3.8
6
6
  Classifier: Framework :: Django
@@ -24,7 +24,7 @@ Requires-Dist: django-classy-tags ==2.0.0
24
24
  Requires-Dist: libsass ==0.23.0
25
25
  Requires-Dist: phonenumbers ==8.12.12
26
26
  Requires-Dist: more-itertools ==8.7.0
27
- Requires-Dist: cfl-common ==6.42.0
27
+ Requires-Dist: cfl-common ==6.43.1
28
28
  Requires-Dist: django-ratelimit ==3.0.1
29
29
  Requires-Dist: django-preventconcurrentlogins ==0.8.2
30
30
  Requires-Dist: django-csp ==3.7
@@ -5,14 +5,14 @@ cfl_common/common/app_settings.py,sha256=x2ROLY5Xl5LgqjxyTiChZvQorZYUXpFzEkaLsjh
5
5
  cfl_common/common/apps.py,sha256=49UXZ3bSkFKvIEOL4zM7y1sAhccQJyRtsoOg5XVd_8Y,129
6
6
  cfl_common/common/context_processors.py,sha256=X0iuX5qu9kMWa7q8osE9CJ2LgM7pPOYQFGdjm8X3rk0,236
7
7
  cfl_common/common/csp_config.py,sha256=sZT6s9zMT5FFIqNODsURT0ifxbDgXpDlki8UxaBq2iE,2940
8
- cfl_common/common/email_messages.py,sha256=DRiz6MCKUGdFsC-pN9EwFqzPhpzMWXaT9HPcji1BkvE,4437
9
- cfl_common/common/mail.py,sha256=5iwvedYfaJUv7v8vVpV1kyBtnw04EJhHPy3FRGI9WHM,4223
10
- cfl_common/common/models.py,sha256=vnvy8U-sHopyaxgJK9wTxelbKsCnYMjuEu3HIuAEkrs,14974
8
+ cfl_common/common/email_messages.py,sha256=oK8cb5wrDbxq6xoEoILHJggs3P35aEyk9eFJHIR6y74,3644
9
+ cfl_common/common/mail.py,sha256=qgsgBc0drFIG8ik4ZBjaqjSvk0LVDIJV2fNYWFrbzHU,5990
10
+ cfl_common/common/models.py,sha256=EunFsc7sOWfWiFf4IQwuy56gu8pu3YpPoOgVtsMhbRM,14958
11
11
  cfl_common/common/permissions.py,sha256=gC6RQGZI2QDBbglx-xr_V4Hl2C2nf1V2_uPmEuoEcJo,2416
12
12
  cfl_common/common/utils.py,sha256=Nn2Npao9Uqad5Js_IdHwF-ow6wrPNpBLW4AO1LxoEBc,1727
13
13
  cfl_common/common/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  cfl_common/common/helpers/data_migration_loader.py,sha256=_BhS5lPmhcuVUbryBmJytlWdHyT02KYyxPkHar32mOE,1748
15
- cfl_common/common/helpers/emails.py,sha256=u2X2brjHIlUDNIgQ6-Ld23Y4zyouJGTFH-G7HNQDBYs,11041
15
+ cfl_common/common/helpers/emails.py,sha256=6xY8YxYY3ywcFWguys2G_36HTaBBah4ma1p20s4mpKU,10099
16
16
  cfl_common/common/helpers/generators.py,sha256=kTL5e91I8wgmjJ-mu4jr9vIacjccUZ5pZSAz5cUNhdM,1505
17
17
  cfl_common/common/helpers/organisation.py,sha256=e-JKumKoXrkMTzZPv0H4ViWL8vtCt7oXJjn_zZ1ec00,427
18
18
  cfl_common/common/migrations/0001_initial.py,sha256=Y2kt2xmdCbrmDXCgqmhXeacicNg26Zj7L7SANSsgAAI,9664
@@ -63,12 +63,18 @@ cfl_common/common/migrations/0045_otp.py,sha256=_GmCOFOINqFMBqPBvdBaR1nwAI_FkzIl
63
63
  cfl_common/common/migrations/0046_alter_school_country.py,sha256=dg_lexw7ALB-jlOm_EBQauk9mI4VbqUGv0qQsHo0b5s,437
64
64
  cfl_common/common/migrations/0047_delete_school_postcode.py,sha256=GPV0hLfXmbPpx4-G5OaaLy6aalKvSnZLH0aGggYx9u0,331
65
65
  cfl_common/common/migrations/0048_unique_school_names.py,sha256=pu5xiuesvFNGngD-hl0OQ6Gi2r6pEY9fPCayKyb9n04,1433
66
+ cfl_common/common/migrations/0049_anonymise_orphan_users.py,sha256=tw9xMrDMRPDCO8HWjBVlnQF8r1YVCKZnVr2wZ3He6og,847
67
+ cfl_common/common/migrations/0050_anonymise_orphan_schools.py,sha256=_KCkSkoObTpLplX6gXvlV3JXpddn7neyJEa8YKFWeW0,869
68
+ cfl_common/common/migrations/0051_verify_returning_users.py,sha256=GVdMtOFIJQveYvKlK5EW-1tA_T-mEP_L_HfHarsaPvY,957
66
69
  cfl_common/common/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
70
  cfl_common/common/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
- cfl_common/common/tests/test_0048_unique_school_names.py,sha256=4WAGJoqCK1VYQrh8v4jVh7JTm8Gs_iZzmcDu1vYQcRc,999
69
71
  cfl_common/common/tests/test_migration_aimmo_characters.py,sha256=pdCCsns90Qz05QqmaBUYK18jKe9aP-symtZjkKG4rag,1079
72
+ cfl_common/common/tests/test_migration_anonymise_orphan_schools.py,sha256=wJRyPgRvBsXLSCFdbBi2GXjgSgDbKUTRiM31CXIvpqs,1194
73
+ cfl_common/common/tests/test_migration_anonymise_orphan_users.py,sha256=MGuI8YVvUReXxjK36i2n-vkC677I8HqVHph778zL34Q,1368
70
74
  cfl_common/common/tests/test_migration_blocked_time.py,sha256=z9WxMTrZTKFieLfbQwkoOZozziPHmWVk6T4FysLeHGk,590
71
75
  cfl_common/common/tests/test_migration_remove_teacher_title.py,sha256=wwm6tayb75QmDXwXBfxu6SIMf7Ant4rEHHEBLIFjHcI,522
76
+ cfl_common/common/tests/test_migration_unique_school_names.py,sha256=D5SQ1UmD8yHLiEDsA7mWl1O4HzzxsBN_RXErB3ikg5I,1032
77
+ cfl_common/common/tests/test_migration_verify_returning_users.py,sha256=n8JGW-TmE1Hv_4AHl3kn9b6Hwp7DkpZWiYkbPeR8PXw,2054
72
78
  cfl_common/common/tests/test_models.py,sha256=xMdzonW5CADMjas_zfg8V1YPQpUetleyn6TE95hbO9k,3723
73
79
  cfl_common/common/tests/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
80
  cfl_common/common/tests/utils/classes.py,sha256=ZA2pp9Pyx3rwi0VFwtuUA2Pys9xQJ-L_zE0u2tpwEH4,1094
@@ -100,7 +106,7 @@ example_project/portal_test_settings.py,sha256=frp_XMpd-z1g3VFCRxB2w7AaFW2ivRVKn
100
106
  example_project/settings.py,sha256=XRZZvASoIl5a9xe3masTq_CUBleuJq9ByHx8f_e2UFc,5613
101
107
  example_project/urls.py,sha256=OVeRQ-TCpzHISBRuzqD0yd3ewF7H5U3c-f2p2alfUD0,430
102
108
  example_project/wsgi.py,sha256=U1W6WzZxZaIdYZ5tks7w9fqp5WS5qvn2iThsVcskrWw,829
103
- portal/__init__.py,sha256=ica5ssOzZS9jT8YpXm7HhZkzfZI3xz6GxgU2O-zXjQc,23
109
+ portal/__init__.py,sha256=Ngar-CylGsb6FZcYQn3kMXH5BOJ1Ub5AefCQXyRW8-E,23
104
110
  portal/admin.py,sha256=k5Hsiln43DlVPoufnrx5AXWu_RijX8xi_n7wwBuuCJo,5132
105
111
  portal/app_settings.py,sha256=DhWLQOwM0zVOXE3O5TNKbMM9K6agfLuCsHOdr1J7xEI,651
106
112
  portal/backends.py,sha256=2Dss6_WoQwPuDzJUF1yEaTQTNG4eUrD12ujJQ5cp5Tc,812
@@ -532,7 +538,7 @@ portal/tests/selenium_test_case.py,sha256=eWUF_5SqkI178bkay5SUDa06r0QTIKUUT8jTAh
532
538
  portal/tests/test_2FA.py,sha256=0N4C9Ab3TvO9W__oQLCo-fLDH1Ho3CiGGsSg-2TiZUE,3597
533
539
  portal/tests/test_admin.py,sha256=AM2dgv8j9m4L-SDO-sMA9tQvQH9GwRBrlwRG9OgqtfI,1451
534
540
  portal/tests/test_aimmo_dashboards.py,sha256=24yGHieAxDHHP-S6qhCWphuszBzVUK3JUF4CmaarbbM,8615
535
- portal/tests/test_api.py,sha256=cKWLmKOGID5gKW9Ru-7gI0EXpETBM5Gju2odOB8DkP8,12818
541
+ portal/tests/test_api.py,sha256=Yo5s_nEGOoG35jA39yZ6nuDOUZvuCZ8o8o8XhZos61w,13819
536
542
  portal/tests/test_captcha_forms.py,sha256=lirhIli-sHovun8VdrF0he7KRFTAd8DMCpkJ8cQNotg,1015
537
543
  portal/tests/test_class.py,sha256=V6Fkc6PqdisefKD3xs9PbfE2pKp-9e0gwQVkPUiu6bk,14150
538
544
  portal/tests/test_daily_activities.py,sha256=-siDCMGBD1ijjccHVk7eEmrk4bgTsvbh0B6hDoj2fo0,1803
@@ -542,7 +548,7 @@ portal/tests/test_independent_student.py,sha256=ysWpkYiwjPdB7gO3ow-5JuxqLi0IywRS
542
548
  portal/tests/test_invite_teacher.py,sha256=oeOaoJV1IqJSYPlaPFjnhVXdB2mq8otCTLp_lfjuCfk,12224
543
549
  portal/tests/test_middleware.py,sha256=b6jfNmiRZ2snqLKsyJUG-RivoX5fmrqLlQkG9MeVnqM,8034
544
550
  portal/tests/test_newsletter_footer.py,sha256=MdVUX53mEoDTa4Krq-jg9LFNo-QyghqvTvhHeNXBGnE,838
545
- portal/tests/test_organisation.py,sha256=fOtck-0MkPM2F0V4RFH-QUeWEk6yUIXDv_GI5cl8sdg,7649
551
+ portal/tests/test_organisation.py,sha256=kCMUNzLN6EzaMUBcFkqXwnqLGgOuQxQWIHHt63nhqBs,7574
546
552
  portal/tests/test_partials.py,sha256=ydh1nef6BqvMfah2BSBS9QDiKY0xopY74k_W1YVobAE,3687
547
553
  portal/tests/test_ratelimit.py,sha256=XWq1A9XgRrlcMHibGoJ0kc4gLc5U_u5UhKHjthxCfYA,19376
548
554
  portal/tests/test_school_student.py,sha256=bFZwY4twaFHQLp0cltMq8cLNDZGgCHTZBCZHK0JcV8s,8604
@@ -624,20 +630,20 @@ portal/views/cron/__init__.py,sha256=5rxXyhJmLOExRdrYZ1VJttTsyRIPRybzdftbUDwFByI
624
630
  portal/views/cron/user.py,sha256=N4slzEXqzp557LLPlwA6sD3HVzDu74NBf128uvtwKnM,6044
625
631
  portal/views/login/__init__.py,sha256=xSCtyFPSI87BRUybBgqa86ekFEolX5gUDbBSfBUMTyI,399
626
632
  portal/views/login/independent_student.py,sha256=3dFULhwMAlX4VDrJl-Znril6a9M5xKBSHO1eWvujfS0,2662
627
- portal/views/login/student.py,sha256=4bQLLhB-KHvvjim07rYWXaLCZzHjUVofl0wrRkb1s0w,5224
633
+ portal/views/login/student.py,sha256=dt6cMfWepBJsVCRcADltfYSHVpyeP1WGLKSogMJ22E0,5539
628
634
  portal/views/login/teacher.py,sha256=kRugP7TPbZIb_BmYMYxFeugxZy8UbCry_q0_jJDJ_Mw,1975
629
635
  portal/views/student/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
630
636
  portal/views/student/edit_account_details.py,sha256=keMakqgqy5xB76QbpwsnkadxbMg_dGsAxLuP2CoWbvc,8551
631
637
  portal/views/student/play.py,sha256=-v9lBjHF3_PAKRgWcCGGt_bEOpIkmJDJnzgR5JvqrMo,8908
632
638
  portal/views/teacher/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
633
- portal/views/teacher/dashboard.py,sha256=_zKAEIaMwCI5LdF5QN2kLy59UTkNhNnx5mYhe-mfdVA,24991
639
+ portal/views/teacher/dashboard.py,sha256=nNA7XxylunLjyCpKmq7h_CYITi7wM3YnmjqzUL0A-bI,25236
634
640
  portal/views/teacher/teach.py,sha256=B71jReMJ4BYFmo7NtJVK3-4DeXEwxfu_WA3Ij1RYzdI,34725
635
641
  portal/views/two_factor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
636
642
  portal/views/two_factor/core.py,sha256=O_wcBeFqdPYSGNGv-pT_vbs5-Dj1Z-Jfkd6f9-E5yZI,760
637
643
  portal/views/two_factor/form.py,sha256=lnHNKI-BMlpncTuW3zUzjPaJJNuEra2I_nOam0eOKFY,257
638
644
  portal/views/two_factor/profile.py,sha256=tkl_ludo8arMtd5LKNmohM66vpC_YQiP-0nspTSJiJ4,383
639
- codeforlife_portal-6.42.0.dist-info/LICENSE.md,sha256=9AbRlCDqD2D1tPibimysFv3zg3AIc49-eyv9aEsyq9w,115
640
- codeforlife_portal-6.42.0.dist-info/METADATA,sha256=CmKt5JHc-erJVPaxVVCZ5OSA1btKf0HjBOC_gQ-eAYo,1137
641
- codeforlife_portal-6.42.0.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
642
- codeforlife_portal-6.42.0.dist-info/top_level.txt,sha256=8e5pdsuIoTqEAMqpelHBjGjLbffcBtgOoggmd2q7nMw,41
643
- codeforlife_portal-6.42.0.dist-info/RECORD,,
645
+ codeforlife_portal-6.43.1.dist-info/LICENSE.md,sha256=9AbRlCDqD2D1tPibimysFv3zg3AIc49-eyv9aEsyq9w,115
646
+ codeforlife_portal-6.43.1.dist-info/METADATA,sha256=tFzxDaekn6AVyuunmE_TYU1KveLBu1erHq8KPawpbzs,1137
647
+ codeforlife_portal-6.43.1.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
648
+ codeforlife_portal-6.43.1.dist-info/top_level.txt,sha256=8e5pdsuIoTqEAMqpelHBjGjLbffcBtgOoggmd2q7nMw,41
649
+ codeforlife_portal-6.43.1.dist-info/RECORD,,
portal/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "6.42.0"
1
+ __version__ = "6.43.1"
portal/tests/test_api.py CHANGED
@@ -5,7 +5,10 @@ from unittest.mock import patch
5
5
  import pytest
6
6
  from common.models import Class, School, Student, Teacher
7
7
  from common.tests.utils.classes import create_class_directly
8
- from common.tests.utils.organisation import create_organisation_directly, join_teacher_to_organisation
8
+ from common.tests.utils.organisation import (
9
+ create_organisation_directly,
10
+ join_teacher_to_organisation,
11
+ )
9
12
  from common.tests.utils.student import create_school_student_directly
10
13
  from common.tests.utils.teacher import signup_teacher_directly
11
14
  from common.tests.utils.user import create_user_directly, get_superuser
@@ -19,7 +22,10 @@ from rest_framework.test import APIClient, APITestCase
19
22
 
20
23
  class APITests(APITestCase):
21
24
  def test_valid_date_registered(self):
22
- url = reverse("registered-users", kwargs={"year": "2016", "month": "04", "day": "01"})
25
+ url = reverse(
26
+ "registered-users",
27
+ kwargs={"year": "2016", "month": "04", "day": "01"},
28
+ )
23
29
  superuser = get_superuser()
24
30
  self.client.force_authenticate(user=superuser)
25
31
  response = self.client.get(url)
@@ -27,14 +33,20 @@ class APITests(APITestCase):
27
33
  assert_that(isinstance(response.data, int))
28
34
 
29
35
  def test_invalid_date_registered(self):
30
- url = reverse("registered-users", kwargs={"year": "2016", "month": "05", "day": "35"})
36
+ url = reverse(
37
+ "registered-users",
38
+ kwargs={"year": "2016", "month": "05", "day": "35"},
39
+ )
31
40
  superuser = get_superuser()
32
41
  self.client.force_authenticate(user=superuser)
33
42
  response = self.client.get(url)
34
43
  assert_that(response, has_status_code(status.HTTP_404_NOT_FOUND))
35
44
 
36
45
  def test_valid_date_lastconnectedsince(self):
37
- url = reverse("last-connected-since", kwargs={"year": "2016", "month": "04", "day": "01"})
46
+ url = reverse(
47
+ "last-connected-since",
48
+ kwargs={"year": "2016", "month": "04", "day": "01"},
49
+ )
38
50
  superuser = get_superuser()
39
51
  self.client.force_authenticate(user=superuser)
40
52
  response = self.client.get(url)
@@ -42,7 +54,10 @@ class APITests(APITestCase):
42
54
  assert_that(isinstance(response.data, int))
43
55
 
44
56
  def test_invalid_date_lastconnectedsince(self):
45
- url = reverse("last-connected-since", kwargs={"year": "2016", "month": "05", "day": "35"})
57
+ url = reverse(
58
+ "last-connected-since",
59
+ kwargs={"year": "2016", "month": "05", "day": "35"},
60
+ )
46
61
  superuser = get_superuser()
47
62
  self.client.force_authenticate(user=superuser)
48
63
  response = self.client.get(url)
@@ -67,7 +82,9 @@ class APITests(APITestCase):
67
82
  assert len(response.data) == 1
68
83
 
69
84
  @patch("portal.views.api.IS_CLOUD_SCHEDULER_FUNCTION", return_value=True)
70
- def test_get_inactive_users_if_appengine(self, mock_is_cloud_scheduler_function):
85
+ def test_get_inactive_users_if_appengine(
86
+ self, mock_is_cloud_scheduler_function
87
+ ):
71
88
  client = APIClient()
72
89
  create_user_directly(active=False)
73
90
  create_user_directly(active=True)
@@ -86,7 +103,9 @@ class APITests(APITestCase):
86
103
  assert response.status_code == status.HTTP_403_FORBIDDEN
87
104
 
88
105
  @patch("portal.views.api.IS_CLOUD_SCHEDULER_FUNCTION", return_value=True)
89
- def test_delete_inactive_users_if_appengine(self, mock_is_cloud_scheduler_function):
106
+ def test_delete_inactive_users_if_appengine(
107
+ self, mock_is_cloud_scheduler_function
108
+ ):
90
109
  client = APIClient()
91
110
  create_user_directly(active=False)
92
111
  create_user_directly(active=False)
@@ -94,14 +113,25 @@ class APITests(APITestCase):
94
113
  response = client.get(url)
95
114
  users = response.data
96
115
  assert len(users) == 2
116
+
117
+ # NOTE: Migration 0049 causes user 34 (created via migration 0001) to
118
+ # be marked as inactive. Slightly tweaked this test so it still
119
+ # passes but takes into account this new anonymisation.
120
+ old_deleted_users = list(User.objects.filter(is_active=False))
121
+ assert len(old_deleted_users) == 1
122
+
97
123
  response = client.delete(url)
98
124
  assert mock_is_cloud_scheduler_function.called
99
125
  assert response.status_code == status.HTTP_204_NO_CONTENT
126
+
100
127
  for user in users:
101
128
  with pytest.raises(User.DoesNotExist):
102
129
  User.objects.get(username=user["username"])
130
+
103
131
  deleted_users = list(User.objects.filter(is_active=False))
104
- assert len(deleted_users) == 2
132
+ new_deleted_users_count = len(deleted_users) - len(old_deleted_users)
133
+ assert new_deleted_users_count == 2
134
+
105
135
  for user in deleted_users:
106
136
  assert user.first_name == "Deleted"
107
137
  assert user.last_name == "User"
@@ -112,27 +142,41 @@ class APITests(APITestCase):
112
142
  assert len(response.data) == 0
113
143
 
114
144
  @patch("portal.views.api.IS_CLOUD_SCHEDULER_FUNCTION", return_value=True)
115
- def test_orphan_schools_and_classes_are_anonymised(self, mock_is_cloud_scheduler_function):
145
+ def test_orphan_schools_and_classes_are_anonymised(
146
+ self, mock_is_cloud_scheduler_function
147
+ ):
116
148
  client = APIClient()
117
149
  # Create a school with an active teacher
118
150
  school1_teacher1_email, _ = signup_teacher_directly()
119
151
  school1 = create_organisation_directly(school1_teacher1_email)
120
- klass11, _, access_code11 = create_class_directly(school1_teacher1_email)
152
+ klass11, _, access_code11 = create_class_directly(
153
+ school1_teacher1_email
154
+ )
121
155
  _, _, student11 = create_school_student_directly(access_code11)
122
156
 
123
157
  # Create a school with one active non-admin teacher and one inactive admin teacher
124
158
  school2_teacher1_email, _ = signup_teacher_directly()
125
159
  school2_teacher2_email, _ = signup_teacher_directly()
126
160
  school2 = create_organisation_directly(school2_teacher1_email)
127
- join_teacher_to_organisation(school2_teacher2_email, school2.name, is_admin=True)
128
- klass21, _, access_code21 = create_class_directly(school2_teacher1_email)
161
+ join_teacher_to_organisation(
162
+ school2_teacher2_email, school2.name, is_admin=True
163
+ )
164
+ klass21, _, access_code21 = create_class_directly(
165
+ school2_teacher1_email
166
+ )
129
167
  _, _, student21 = create_school_student_directly(access_code21)
130
- klass22, _, access_code22 = create_class_directly(school2_teacher2_email)
168
+ klass22, _, access_code22 = create_class_directly(
169
+ school2_teacher2_email
170
+ )
131
171
  _, _, student22 = create_school_student_directly(access_code22)
132
- school2_teacher1 = Teacher.objects.get(new_user__email=school2_teacher1_email)
172
+ school2_teacher1 = Teacher.objects.get(
173
+ new_user__email=school2_teacher1_email
174
+ )
133
175
  school2_teacher1.is_admin = False
134
176
  school2_teacher1.save()
135
- school2_teacher2 = Teacher.objects.get(new_user__email=school2_teacher2_email)
177
+ school2_teacher2 = Teacher.objects.get(
178
+ new_user__email=school2_teacher2_email
179
+ )
136
180
  school2_teacher2.new_user.is_active = False
137
181
  school2_teacher2.new_user.save()
138
182
 
@@ -141,28 +185,40 @@ class APITests(APITestCase):
141
185
  school3_teacher2_email, _ = signup_teacher_directly()
142
186
  school3 = create_organisation_directly(school3_teacher1_email)
143
187
  join_teacher_to_organisation(school3_teacher2_email, school3.name)
144
- klass31, _, access_code31 = create_class_directly(school3_teacher1_email)
188
+ klass31, _, access_code31 = create_class_directly(
189
+ school3_teacher1_email
190
+ )
145
191
  _, _, student31 = create_school_student_directly(access_code31)
146
- klass32, _, access_code32 = create_class_directly(school3_teacher2_email)
192
+ klass32, _, access_code32 = create_class_directly(
193
+ school3_teacher2_email
194
+ )
147
195
  _, _, student32 = create_school_student_directly(access_code32)
148
- school3_teacher1 = Teacher.objects.get(new_user__email=school3_teacher1_email)
196
+ school3_teacher1 = Teacher.objects.get(
197
+ new_user__email=school3_teacher1_email
198
+ )
149
199
  school3_teacher1.new_user.is_active = False
150
200
  school3_teacher1.new_user.save()
151
- school3_teacher2 = Teacher.objects.get(new_user__email=school3_teacher2_email)
201
+ school3_teacher2 = Teacher.objects.get(
202
+ new_user__email=school3_teacher2_email
203
+ )
152
204
  school3_teacher2.new_user.is_active = False
153
205
  school3_teacher2.new_user.save()
154
206
 
155
207
  # Create a school with no active teachers
156
208
  school4_teacher1_email, _ = signup_teacher_directly()
157
209
  school4 = create_organisation_directly(school4_teacher1_email)
158
- school4_teacher1 = Teacher.objects.get(new_user__email=school4_teacher1_email)
210
+ school4_teacher1 = Teacher.objects.get(
211
+ new_user__email=school4_teacher1_email
212
+ )
159
213
  school4_teacher1.new_user.is_active = False
160
214
  school4_teacher1.new_user.save()
161
215
 
162
216
  # Create a school with no teachers
163
217
  school5_teacher1_email, _ = signup_teacher_directly()
164
218
  school5 = create_organisation_directly(school5_teacher1_email)
165
- school5_teacher1 = Teacher.objects.get(new_user__email=school5_teacher1_email)
219
+ school5_teacher1 = Teacher.objects.get(
220
+ new_user__email=school5_teacher1_email
221
+ )
166
222
  school5_teacher1.delete()
167
223
 
168
224
  # Call the API
@@ -182,7 +238,9 @@ class APITests(APITestCase):
182
238
  assert Student.objects.filter(pk=student21.pk).exists()
183
239
  assert not Student.objects.get(pk=student22.pk).new_user.is_active
184
240
  # Also check the first teacher is now an admin
185
- assert Teacher.objects.get(new_user__email=school2_teacher1_email).is_admin
241
+ assert Teacher.objects.get(
242
+ new_user__email=school2_teacher1_email
243
+ ).is_admin
186
244
 
187
245
  # Check the third school is anonymised together with its classes and students
188
246
  assert not School.objects.filter(name=school3.name).exists()
@@ -250,14 +308,19 @@ class APITests(APITestCase):
250
308
  last_name=random_account["last_name"],
251
309
  )
252
310
 
253
- assert len(User.objects.all()) == len(random_accounts) + initial_users_length
311
+ assert (
312
+ len(User.objects.all())
313
+ == len(random_accounts) + initial_users_length
314
+ )
254
315
 
255
316
  client.login(username=admin_username, password=admin_password)
256
317
  response = client.get(reverse("remove_fake_accounts"))
257
318
  assert response.status_code == 204
258
319
 
259
320
  # check if after deletion all the users are still there
260
- assert len(User.objects.all()) == initial_users_length + 2 # mentioned in the fake_accounts description
321
+ assert (
322
+ len(User.objects.all()) == initial_users_length + 2
323
+ ) # mentioned in the fake_accounts description
261
324
 
262
325
 
263
326
  def has_status_code(status_code):
@@ -272,7 +335,11 @@ class HasStatusCode(BaseMatcher):
272
335
  return response.status_code == self.status_code
273
336
 
274
337
  def describe_to(self, description):
275
- description.append_text("has status code ").append_text(self.status_code)
338
+ description.append_text("has status code ").append_text(
339
+ self.status_code
340
+ )
276
341
 
277
342
  def describe_mismatch(self, response, mismatch_description):
278
- mismatch_description.append_text("had status code ").append_text(response.status_code)
343
+ mismatch_description.append_text("had status code ").append_text(
344
+ response.status_code
345
+ )
@@ -4,9 +4,11 @@ import time
4
4
 
5
5
  from common.models import Teacher
6
6
  from common.tests.utils.classes import create_class_directly
7
- from common.tests.utils.organisation import (create_organisation,
8
- create_organisation_directly,
9
- join_teacher_to_organisation)
7
+ from common.tests.utils.organisation import (
8
+ create_organisation,
9
+ create_organisation_directly,
10
+ join_teacher_to_organisation,
11
+ )
10
12
  from common.tests.utils.student import create_school_student_directly
11
13
  from common.tests.utils.teacher import signup_teacher_directly
12
14
  from selenium.webdriver.common.by import By
@@ -51,7 +51,8 @@ class StudentLoginView(LoginView):
51
51
  class_name = self.kwargs["access_code"].upper()
52
52
  messages.info(
53
53
  request,
54
- f"<strong>You are logged in to class: " f"{escape(class_name)}</strong>",
54
+ f"<strong>You are logged in to class: "
55
+ f"{escape(class_name)}</strong>",
55
56
  extra_tags="safe message--student",
56
57
  )
57
58
 
@@ -72,7 +73,9 @@ class StudentLoginView(LoginView):
72
73
  klass = classes[0]
73
74
 
74
75
  name = form.cleaned_data.get("username")
75
- students = Student.objects.filter(new_user__first_name__iexact=name, class_field=klass)
76
+ students = Student.objects.filter(
77
+ new_user__first_name__iexact=name, class_field=klass
78
+ )
76
79
  try:
77
80
  student = students[0]
78
81
  except IndexError:
@@ -81,32 +84,44 @@ class StudentLoginView(LoginView):
81
84
  raise Exception(msg)
82
85
 
83
86
  # Log the login time, class, and login type
84
- session = UserSession(user=student.new_user, class_field=klass, login_type=login_type)
87
+ session = UserSession(
88
+ user=student.new_user, class_field=klass, login_type=login_type
89
+ )
85
90
  session.save()
86
91
 
92
+ student.user.is_verified = True
93
+ student.user.save()
94
+
87
95
  def form_valid(self, form):
88
96
  """Security check complete. Log the user in."""
89
-
90
97
  # Reset ratelimit cache upon successful login
91
98
  clear_ratelimit_cache_for_user(form.cleaned_data["username"])
92
99
 
93
- login_type = self.kwargs.get("login_type", "classlink") # default to "classlink" if not specified
100
+ login_type = self.kwargs.get(
101
+ "login_type", "classlink"
102
+ ) # default to "classlink" if not specified
94
103
 
95
104
  self._add_login_data(form, login_type)
96
105
  return super(StudentLoginView, self).form_valid(form)
97
106
 
98
107
  def post(self, request, *args, **kwargs):
99
108
  """
100
- If the first name and access code found under the url inputted in the form corresponds to that of a blocked
101
- account, this redirects the user to the locked out page. However, if the lockout
102
- time is more than 24 hours before this is executed, the account is unlocked.
109
+ If the first name and access code found under the url inputted in the
110
+ form corresponds to that of a blocked account, this redirects the user
111
+ to the locked out page. However, if the lockout time is more than 24
112
+ hours before this is executed, the account is unlocked.
103
113
  """
104
114
  username = request.POST.get("username")
105
115
 
106
116
  # get access code from the current url
107
117
  access_code = get_access_code_from_request(request)
108
- if Student.objects.filter(new_user__first_name=username, class_field__access_code=access_code).exists():
109
- student = Student.objects.get(new_user__first_name=username, class_field__access_code=access_code)
118
+ if Student.objects.filter(
119
+ new_user__first_name=username, class_field__access_code=access_code
120
+ ).exists():
121
+ student = Student.objects.get(
122
+ new_user__first_name=username,
123
+ class_field__access_code=access_code,
124
+ )
110
125
 
111
126
  if student.blocked_time is not None:
112
127
  if has_user_lockout_expired(student):
@@ -129,9 +144,15 @@ def student_direct_login(request, user_id, login_id):
129
144
  if user:
130
145
  # Log the login time and class
131
146
  student = Student.objects.get(new_user=user)
132
- session = UserSession(user=user, class_field=student.class_field, login_type="direct")
147
+ session = UserSession(
148
+ user=user, class_field=student.class_field, login_type="direct"
149
+ )
133
150
  session.save()
134
151
 
135
152
  login(request, user)
153
+
154
+ student.user.is_verified = True
155
+ student.user.save()
156
+
136
157
  return HttpResponseRedirect(reverse_lazy("student_details"))
137
158
  return HttpResponseRedirect(reverse_lazy("home"))
@@ -2,12 +2,24 @@ from datetime import timedelta
2
2
  from uuid import uuid4
3
3
 
4
4
  from common import email_messages
5
- from common.helpers.emails import (INVITE_FROM, NOTIFICATION_EMAIL,
6
- DotmailerUserType, add_to_dotmailer,
7
- generate_token, send_email, update_email)
5
+ from common.helpers.emails import (
6
+ INVITE_FROM,
7
+ NOTIFICATION_EMAIL,
8
+ DotmailerUserType,
9
+ add_to_dotmailer,
10
+ generate_token,
11
+ send_email,
12
+ update_email,
13
+ )
8
14
  from common.helpers.generators import get_random_username
9
- from common.models import (Class, JoinReleaseStudent, SchoolTeacherInvitation,
10
- Student, Teacher)
15
+ from common.mail import campaign_ids, send_dotdigital_email
16
+ from common.models import (
17
+ Class,
18
+ JoinReleaseStudent,
19
+ SchoolTeacherInvitation,
20
+ Student,
21
+ Teacher,
22
+ )
11
23
  from common.permissions import check_teacher_authorised, logged_in_as_teacher
12
24
  from common.utils import using_two_factor
13
25
  from django.contrib import messages as messages
@@ -16,7 +28,7 @@ from django.contrib.auth.decorators import login_required, user_passes_test
16
28
  from django.contrib.auth.models import User
17
29
  from django.http import Http404, HttpResponseRedirect
18
30
  from django.shortcuts import get_object_or_404, render
19
- from django.urls import reverse_lazy
31
+ from django.urls import reverse, reverse_lazy
20
32
  from django.utils import timezone
21
33
  from django.views.decorators.http import require_POST
22
34
  from game.level_management import levels_shared_with, unshare_level
@@ -25,14 +37,20 @@ from two_factor.utils import devices_for_user
25
37
  from portal.forms.invite_teacher import InviteTeacherForm
26
38
  from portal.forms.organisation import OrganisationForm
27
39
  from portal.forms.registration import DeleteAccountForm
28
- from portal.forms.teach import (ClassCreationForm, InvitedTeacherForm,
29
- TeacherAddExternalStudentForm,
30
- TeacherEditAccountForm)
40
+ from portal.forms.teach import (
41
+ ClassCreationForm,
42
+ InvitedTeacherForm,
43
+ TeacherAddExternalStudentForm,
44
+ TeacherEditAccountForm,
45
+ )
31
46
  from portal.helpers.decorators import ratelimit
32
47
  from portal.helpers.password import check_update_password
33
- from portal.helpers.ratelimit import (RATELIMIT_LOGIN_GROUP,
34
- RATELIMIT_LOGIN_RATE, RATELIMIT_METHOD,
35
- clear_ratelimit_cache_for_user)
48
+ from portal.helpers.ratelimit import (
49
+ RATELIMIT_LOGIN_GROUP,
50
+ RATELIMIT_LOGIN_RATE,
51
+ RATELIMIT_METHOD,
52
+ clear_ratelimit_cache_for_user,
53
+ )
36
54
 
37
55
  from .teach import create_class
38
56
 
@@ -380,19 +398,22 @@ def invite_toggle_admin(request, invite_id):
380
398
 
381
399
  if invite.invited_teacher_is_admin:
382
400
  messages.success(request, "Administrator invite status has been given successfully")
383
- emailMessage = email_messages.adminGivenEmail(request, invite.school)
401
+ send_dotdigital_email(
402
+ campaign_ids["admin_given"],
403
+ [invite.invited_teacher_email],
404
+ personalization_values={
405
+ "SCHOOL_CLUB_NAME": invite.school,
406
+ "MANAGEMENT_LINK": request.build_absolute_uri(reverse("dashboard")),
407
+ },
408
+ )
384
409
 
385
410
  else:
386
411
  messages.success(request, "Administrator invite status has been revoked successfully")
387
- emailMessage = email_messages.adminRevokedEmail(request, invite.school)
388
-
389
- send_email(
390
- NOTIFICATION_EMAIL,
391
- [invite.invited_teacher_email],
392
- emailMessage["subject"],
393
- emailMessage["message"],
394
- emailMessage["subject"],
395
- )
412
+ send_dotdigital_email(
413
+ campaign_ids["admin_revoked"],
414
+ [invite.invited_teacher_email],
415
+ personalization_values={"SCHOOL_CLUB_NAME": invite.school},
416
+ )
396
417
 
397
418
  return HttpResponseRedirect(reverse_lazy("dashboard"))
398
419
 
@@ -411,7 +432,14 @@ def organisation_toggle_admin(request, pk):
411
432
 
412
433
  if teacher.is_admin:
413
434
  messages.success(request, "Administrator status has been given successfully.")
414
- email_message = email_messages.adminGivenEmail(request, teacher.school.name)
435
+ send_dotdigital_email(
436
+ campaign_ids["admin_given"],
437
+ [teacher.new_user.email],
438
+ personalization_values={
439
+ "SCHOOL_CLUB_NAME": teacher.school.name,
440
+ "MANAGEMENT_LINK": request.build_absolute_uri(reverse("dashboard")),
441
+ },
442
+ )
415
443
  else:
416
444
  # Remove access to all levels that are from other teachers' students
417
445
  [
@@ -420,15 +448,11 @@ def organisation_toggle_admin(request, pk):
420
448
  if hasattr(level.owner, "student") and not teacher.teaches(level.owner)
421
449
  ]
422
450
  messages.success(request, "Administrator status has been revoked successfully.")
423
- email_message = email_messages.adminRevokedEmail(request, teacher.school.name)
424
-
425
- send_email(
426
- NOTIFICATION_EMAIL,
427
- [teacher.new_user.email],
428
- email_message["subject"],
429
- email_message["message"],
430
- email_message["subject"],
431
- )
451
+ send_dotdigital_email(
452
+ campaign_ids["admin_revoked"],
453
+ [teacher.new_user.email],
454
+ personalization_values={"SCHOOL_CLUB_NAME": teacher.school.name},
455
+ )
432
456
 
433
457
  return HttpResponseRedirect(reverse_lazy("dashboard"))
434
458