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.
- cfl_common/common/email_messages.py +0 -24
- cfl_common/common/helpers/emails.py +14 -39
- cfl_common/common/mail.py +88 -44
- cfl_common/common/migrations/0049_anonymise_orphan_users.py +29 -0
- cfl_common/common/migrations/0050_anonymise_orphan_schools.py +30 -0
- cfl_common/common/migrations/0051_verify_returning_users.py +30 -0
- cfl_common/common/models.py +0 -1
- cfl_common/common/tests/test_migration_anonymise_orphan_schools.py +30 -0
- cfl_common/common/tests/test_migration_anonymise_orphan_users.py +30 -0
- cfl_common/common/tests/{test_0048_unique_school_names.py → test_migration_unique_school_names.py} +7 -3
- cfl_common/common/tests/test_migration_verify_returning_users.py +59 -0
- {codeforlife_portal-6.42.0.dist-info → codeforlife_portal-6.43.1.dist-info}/METADATA +2 -2
- {codeforlife_portal-6.42.0.dist-info → codeforlife_portal-6.43.1.dist-info}/RECORD +21 -15
- portal/__init__.py +1 -1
- portal/tests/test_api.py +93 -26
- portal/tests/test_organisation.py +5 -3
- portal/views/login/student.py +32 -11
- portal/views/teacher/dashboard.py +56 -32
- {codeforlife_portal-6.42.0.dist-info → codeforlife_portal-6.43.1.dist-info}/LICENSE.md +0 -0
- {codeforlife_portal-6.42.0.dist-info → codeforlife_portal-6.43.1.dist-info}/WHEEL +0 -0
- {codeforlife_portal-6.42.0.dist-info → codeforlife_portal-6.43.1.dist-info}/top_level.txt +0 -0
|
@@ -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.
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
]
|
cfl_common/common/models.py
CHANGED
|
@@ -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)
|
cfl_common/common/tests/{test_0048_unique_school_names.py → test_migration_unique_school_names.py}
RENAMED
|
@@ -3,8 +3,10 @@ from django_test_migrations.migrator import Migrator
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
@pytest.mark.django_db
|
|
6
|
-
def
|
|
7
|
-
state = migrator.apply_initial_migration(
|
|
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(
|
|
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.
|
|
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.
|
|
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=
|
|
9
|
-
cfl_common/common/mail.py,sha256=
|
|
10
|
-
cfl_common/common/models.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
640
|
-
codeforlife_portal-6.
|
|
641
|
-
codeforlife_portal-6.
|
|
642
|
-
codeforlife_portal-6.
|
|
643
|
-
codeforlife_portal-6.
|
|
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.
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
128
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 (
|
|
8
|
-
|
|
9
|
-
|
|
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
|
portal/views/login/student.py
CHANGED
|
@@ -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: "
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
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(
|
|
109
|
-
|
|
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(
|
|
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 (
|
|
6
|
-
|
|
7
|
-
|
|
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.
|
|
10
|
-
|
|
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 (
|
|
29
|
-
|
|
30
|
-
|
|
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 (
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|