codeforlife-portal 6.41.12__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.
@@ -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.41.12
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.41.12
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=vnvy8U-sHopyaxgJK9wTxelbKsCnYMjuEu3HIuAEkrs,14974
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
@@ -96,11 +102,11 @@ deploy/static/robots.txt,sha256=5cS4RITuQhbpNzvpk4AyDCXdlIBfmfCoBYRvCHY2VT8,24
96
102
  deploy/templates/deploy/csrf_failure.html,sha256=-pBRPn4Y7nUdYHGpTHCokT9Boi-isuwuivF8V2K1SgM,412
97
103
  example_project/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
104
  example_project/manage.py,sha256=EUgybZlZ7xk2Zf2KCwBbK_z7gf7Ifqs0_bl4Kijhdgo,242
99
- example_project/portal_test_settings.py,sha256=fkbeU36YhWvNbR_hm28Rq4pV6ln-dGHK1VdML_1CToY,7227
100
- example_project/settings.py,sha256=GLelTfsnhAuf_rqjXUoIWoLtzKvr-9l8UQDQ23rxPQc,5580
105
+ example_project/portal_test_settings.py,sha256=frp_XMpd-z1g3VFCRxB2w7AaFW2ivRVKn0bAR4LM628,7260
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=5n_lQKHpwgRA1iQyQNFbe5k_9ibqOMSHLwuRsyJ4kqs,24
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,11 +538,11 @@ 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
539
- portal/tests/test_emails.py,sha256=-rI3FlJO7n9qfZ8Vz_Fe3DmjOngr4r23PCpjIoRxNY0,9133
545
+ portal/tests/test_emails.py,sha256=Y26VjhPOzc2aptHBSmOW9R-do3k1QaRqUc5MiwgsAQk,9156
540
546
  portal/tests/test_helper_methods.py,sha256=-SQCDZm2XUtyXGEp0CHIb_SSC9CPD-XOSnpnY8QclHk,890
541
547
  portal/tests/test_independent_student.py,sha256=ysWpkYiwjPdB7gO3ow-5JuxqLi0IywRSxG1s3rKzTK0,26282
542
548
  portal/tests/test_invite_teacher.py,sha256=oeOaoJV1IqJSYPlaPFjnhVXdB2mq8otCTLp_lfjuCfk,12224
@@ -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=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
@@ -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.41.12.dist-info/LICENSE.md,sha256=9AbRlCDqD2D1tPibimysFv3zg3AIc49-eyv9aEsyq9w,115
640
- codeforlife_portal-6.41.12.dist-info/METADATA,sha256=Z0s-OpwbdEecCMxzUNXi05RV9WuhbVksXXi-ogkVp1o,1139
641
- codeforlife_portal-6.41.12.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
642
- codeforlife_portal-6.41.12.dist-info/top_level.txt,sha256=8e5pdsuIoTqEAMqpelHBjGjLbffcBtgOoggmd2q7nMw,41
643
- codeforlife_portal-6.41.12.dist-info/RECORD,,
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,,
@@ -60,6 +60,7 @@ DATABASES = {
60
60
  "NAME": os.path.join(
61
61
  os.path.abspath(os.path.dirname(__file__)), "db.sqlite3"
62
62
  ), # Or path to database file if using sqlite3.
63
+ "ATOMIC_REQUESTS": True,
63
64
  }
64
65
  }
65
66
 
@@ -11,6 +11,7 @@ DATABASES = {
11
11
  "NAME": os.path.join(
12
12
  os.path.abspath(os.path.dirname(__file__)), "db.sqlite3"
13
13
  ), # Or path to database file if using sqlite3.
14
+ "ATOMIC_REQUESTS": True,
14
15
  }
15
16
  }
16
17
 
portal/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "6.41.12"
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 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
+ )
@@ -59,6 +59,7 @@ def test_newsletter_calls_correct_requests(mocker, monkeypatch):
59
59
  mocked_add_to_address_book.assert_called_once()
60
60
 
61
61
 
62
+ @pytest.mark.django_db
62
63
  def test_newsletter_get_not_allowed():
63
64
  c = Client()
64
65
 
@@ -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"))