codeforlife-portal 6.42.0__py2.py3-none-any.whl → 6.43.0__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.
- 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.0.dist-info}/METADATA +2 -2
- {codeforlife_portal-6.42.0.dist-info → codeforlife_portal-6.43.0.dist-info}/RECORD +16 -10
- portal/__init__.py +1 -1
- portal/tests/test_api.py +93 -26
- portal/views/login/student.py +32 -11
- {codeforlife_portal-6.42.0.dist-info → codeforlife_portal-6.43.0.dist-info}/LICENSE.md +0 -0
- {codeforlife_portal-6.42.0.dist-info → codeforlife_portal-6.43.0.dist-info}/WHEEL +0 -0
- {codeforlife_portal-6.42.0.dist-info → codeforlife_portal-6.43.0.dist-info}/top_level.txt +0 -0
|
@@ -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.0
|
|
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.0
|
|
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
|
|
@@ -7,7 +7,7 @@ cfl_common/common/context_processors.py,sha256=X0iuX5qu9kMWa7q8osE9CJ2LgM7pPOYQF
|
|
|
7
7
|
cfl_common/common/csp_config.py,sha256=sZT6s9zMT5FFIqNODsURT0ifxbDgXpDlki8UxaBq2iE,2940
|
|
8
8
|
cfl_common/common/email_messages.py,sha256=DRiz6MCKUGdFsC-pN9EwFqzPhpzMWXaT9HPcji1BkvE,4437
|
|
9
9
|
cfl_common/common/mail.py,sha256=5iwvedYfaJUv7v8vVpV1kyBtnw04EJhHPy3FRGI9WHM,4223
|
|
10
|
-
cfl_common/common/models.py,sha256=
|
|
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
|
|
@@ -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=QcuQSlJPY-R1FVgen2wz3xT3Zw5oZBeORJW5UieLYIU,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
|
|
@@ -624,7 +630,7 @@ 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
|
|
@@ -636,8 +642,8 @@ portal/views/two_factor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
|
|
|
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.0.dist-info/LICENSE.md,sha256=9AbRlCDqD2D1tPibimysFv3zg3AIc49-eyv9aEsyq9w,115
|
|
646
|
+
codeforlife_portal-6.43.0.dist-info/METADATA,sha256=YnRGpJH354nDAr6qgrjVk7parxVg530N5gvYp6EtDNk,1137
|
|
647
|
+
codeforlife_portal-6.43.0.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
|
|
648
|
+
codeforlife_portal-6.43.0.dist-info/top_level.txt,sha256=8e5pdsuIoTqEAMqpelHBjGjLbffcBtgOoggmd2q7nMw,41
|
|
649
|
+
codeforlife_portal-6.43.0.dist-info/RECORD,,
|
portal/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "6.
|
|
1
|
+
__version__ = "6.43.0"
|
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
|
+
)
|
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"))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|