From bacf91b4ea458a279d55b9f7dd8e3f27a6a157fb Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 20 Feb 2024 09:10:41 +0000 Subject: [PATCH 01/13] reset students' password --- backend/Pipfile | 4 +- backend/Pipfile.lock | 19 ++++---- backend/api/serializers/user.py | 47 ++++++------------- backend/api/tests/views/test_user.py | 68 +++++++++++++++++++++++----- backend/api/views/user.py | 25 ++++++++-- 5 files changed, 105 insertions(+), 58 deletions(-) diff --git a/backend/Pipfile b/backend/Pipfile index 73e310ce..e1cecd9d 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -7,7 +7,7 @@ name = "pypi" # Before adding a new package, check it's not listed under [packages] at # https://github.com/ocadotechnology/codeforlife-package-python/blob/{ref}/Pipfile # Replace "{ref}" in the above URL with the ref set below. -codeforlife = {ref = "v0.13.7", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} +codeforlife = {ref = "reset_students_password", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} # TODO: check if we need the below packages whitenoise = "==6.5.0" django-pipeline = "==2.0.8" @@ -34,7 +34,7 @@ google-cloud-container = "==2.3.0" # Before adding a new package, check it's not listed under [dev-packages] at # https://github.com/ocadotechnology/codeforlife-package-python/blob/{ref}/Pipfile # Replace "{ref}" in the above URL with the ref set below. -codeforlife = {ref = "v0.13.7", git = "https://github.com/ocadotechnology/codeforlife-package-python.git", extras = ["dev"]} +codeforlife = {ref = "reset_students_password", git = "https://github.com/ocadotechnology/codeforlife-package-python.git", extras = ["dev"]} # TODO: check if we need the below packages django-selenium-clean = "==0.3.3" django-test-migrations = "==1.2.0" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index d8f3d366..095d1acb 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9e02bb3bd37675c13e20e4e4add09570c4ead5526ab3650ee2a3d47864023155" + "sha256": "894ad5f63407ce1f006a370c2c2487d8e3874f0d503bd5b61fa3c06eaab399fe" }, "pipfile-spec": 6, "requires": { @@ -168,7 +168,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "30953150f4d2762e421d8f798badb57ad0318fd6" + "ref": "78c5b17988d899b2677f4a7d3a12408077d83351" }, "codeforlife-portal": { "hashes": [ @@ -806,6 +806,7 @@ "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" ], + "markers": "python_version >= '3.6'", "version": "==3.1.2" }, "pandas": { @@ -1058,7 +1059,7 @@ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.8.2" }, "pytz": { @@ -1209,7 +1210,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sortedcontainers": { @@ -1304,6 +1305,7 @@ "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==2.0.1" }, "xlwt": { @@ -1512,7 +1514,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "30953150f4d2762e421d8f798badb57ad0318fd6" + "ref": "78c5b17988d899b2677f4a7d3a12408077d83351" }, "codeforlife-portal": { "hashes": [ @@ -2184,6 +2186,7 @@ "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" ], + "markers": "python_version >= '3.6'", "version": "==3.1.2" }, "packaging": { @@ -2492,7 +2495,6 @@ "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5", "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42" ], - "markers": "python_version >= '3.7'", "version": "==7.2.1" }, "pytest-cov": { @@ -2551,7 +2553,7 @@ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.8.2" }, "pytz": { @@ -2727,7 +2729,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "snapshottest": { @@ -2895,6 +2897,7 @@ "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==2.0.1" }, "xlwt": { diff --git a/backend/api/serializers/user.py b/backend/api/serializers/user.py index eaf43b32..ef726f6f 100644 --- a/backend/api/serializers/user.py +++ b/backend/api/serializers/user.py @@ -3,18 +3,22 @@ Created on 18/01/2024 at 15:14:32(+00:00). """ -import string import typing as t from itertools import groupby from codeforlife.serializers import ModelListSerializer -from codeforlife.user.models import Class, Student, Teacher, User, UserProfile +from codeforlife.user.models import ( + Class, + Student, + StudentUser, + Teacher, + User, + UserProfile, +) from codeforlife.user.serializers import UserSerializer as _UserSerializer -from django.contrib.auth.hashers import make_password from django.contrib.auth.password_validation import ( validate_password as _validate_password, ) -from django.utils.crypto import get_random_string from rest_framework import serializers from .student import StudentSerializer @@ -36,40 +40,15 @@ def create(self, validated_data): # TODO: replace this logic with bulk creates for each object when we # switch to PostgreSQL. - users: t.List[User] = [] - for user_fields in validated_data: - password = get_random_string( - length=6, - allowed_chars=string.ascii_lowercase, - ) - - user = User.objects.create_user( + return [ + StudentUser.objects.create_user( first_name=user_fields["first_name"], - username=get_random_string(length=30), - password=make_password(password), - ) - users.append(user) - - # pylint: disable-next=protected-access - user._password = password - - login_id = None - while ( - login_id is None - or Student.objects.filter(login_id=login_id).exists() - ): - login_id = get_random_string(length=64) - - Student.objects.create( - class_field=classes[ + klass=classes[ user_fields["new_student"]["class_field"]["access_code"] ], - user=UserProfile.objects.create(user=user), - new_user=user, - login_id=login_id, ) - - return users + for user_fields in validated_data + ] def validate(self, attrs): super().validate(attrs) diff --git a/backend/api/tests/views/test_user.py b/backend/api/tests/views/test_user.py index 41560f1c..1f798d77 100644 --- a/backend/api/tests/views/test_user.py +++ b/backend/api/tests/views/test_user.py @@ -7,10 +7,12 @@ from codeforlife.permissions import OR from codeforlife.tests import ModelViewSetTestCase +from codeforlife.types import JsonDict from codeforlife.user.models import ( AdminSchoolTeacherUser, Class, SchoolTeacherUser, + StudentUser, User, ) from codeforlife.user.permissions import IsTeacher @@ -56,7 +58,7 @@ def _get_pk_and_token_for_user(self, email: str): # test: get permissions def test_get_permissions__bulk(self): - """Only school-teachers can perform bulk actions.""" + """Only admin-teachers or class-teachers can perform bulk actions.""" self.assert_get_permissions( permissions=[ OR(IsTeacher(is_admin=True), IsTeacher(in_class=True)) @@ -64,8 +66,19 @@ def test_get_permissions__bulk(self): action="bulk", ) + def test_get_permissions__students__reset_password(self): + """ + Only admin-teachers or class-teachers can reset students' passwords. + """ + self.assert_get_permissions( + permissions=[ + OR(IsTeacher(is_admin=True), IsTeacher(in_class=True)) + ], + action="students__reset_password", + ) + def test_get_permissions__partial_update__teacher(self): - """Only admin-school-teachers can update a teacher.""" + """Only admin-teachers can update a teacher.""" self.assert_get_permissions( permissions=[IsTeacher(is_admin=True)], action="partial_update", @@ -73,7 +86,7 @@ def test_get_permissions__partial_update__teacher(self): ) def test_get_permissions__partial_update__student(self): - """Only school-teachers can update a student.""" + """Only admin-teachers or class-teachers can update a student.""" self.assert_get_permissions( permissions=[ OR(IsTeacher(is_admin=True), IsTeacher(in_class=True)) @@ -84,11 +97,7 @@ def test_get_permissions__partial_update__student(self): # test: get queryset - def _test_get_queryset__bulk(self, request_method: str): - assert User.objects.filter( - new_teacher__school=self.admin_school_teacher_user.teacher.school - ).exists() - + def _test_get_queryset(self, action: str, request_method: str): student_users = list( User.objects.filter( new_student__class_field__teacher__school=( @@ -102,15 +111,21 @@ def _test_get_queryset__bulk(self, request_method: str): request_method, user=self.admin_school_teacher_user ) - self.assert_get_queryset(student_users, action="bulk", request=request) + self.assert_get_queryset(student_users, action=action, request=request) def test_get_queryset__bulk__patch(self): """Bulk partial-update can only target student-users.""" - self._test_get_queryset__bulk("patch") + self._test_get_queryset(action="bulk", request_method="patch") def test_get_queryset__bulk__delete(self): """Bulk destroy can only target student-users.""" - self._test_get_queryset__bulk("delete") + self._test_get_queryset(action="bulk", request_method="delete") + + def test_get_queryset__students__reset_password(self): + """Resetting student passwords can only target student-users.""" + self._test_get_queryset( + action="students__reset_password", request_method="patch" + ) # test: bulk actions @@ -275,6 +290,37 @@ def test_reset_password__patch__indy(self): self.client.patch(viewname, data={"password": "N3wPassword"}) self.client.login(email=self.indy_email, password="N3wPassword") + # test: students actions + + def test_students__reset_password(self): + """Teacher can bulk reset students' password.""" + self.client.login_as(self.admin_school_teacher_user) + + student_user_ids = list( + StudentUser.objects.filter( + new_student__class_field__teacher__school=( + self.admin_school_teacher_user.teacher.school + ) + ).values_list("id", flat=True) + ) + assert student_user_ids + + response = self.client.patch( + self.reverse_action("students--reset-password"), + student_user_ids, + content_type="application/json", + ) + + passwords: JsonDict = response.json() + assert all(isinstance(password, str) for password in passwords.values()) + + updated_student_user_ids = [ + int(student_user_id) for student_user_id in passwords.keys() + ] + student_user_ids.sort() + updated_student_user_ids.sort() + self.assertListEqual(student_user_ids, updated_student_user_ids) + # test: generic actions def test_partial_update__teacher(self): diff --git a/backend/api/views/user.py b/backend/api/views/user.py index cd0b80a4..45ad3f1d 100644 --- a/backend/api/views/user.py +++ b/backend/api/views/user.py @@ -7,7 +7,7 @@ from codeforlife.permissions import OR from codeforlife.request import Request -from codeforlife.user.models import User +from codeforlife.user.models import StudentUser, User from codeforlife.user.permissions import IsTeacher from codeforlife.user.views import UserViewSet as _UserViewSet from django.contrib.auth.tokens import ( @@ -31,7 +31,7 @@ class UserViewSet(_UserViewSet): serializer_class = UserSerializer def get_permissions(self): - if self.action == "bulk": + if self.action in ["bulk", "students__reset_password"]: return [OR(IsTeacher(is_admin=True), IsTeacher(in_class=True))] if self.action == "partial_update": if "teacher" in self.request.data: @@ -43,7 +43,9 @@ def get_permissions(self): def get_queryset(self): queryset = super().get_queryset() - if self.action == "bulk" and self.request.method in ["PATCH", "DELETE"]: + if ( + self.action == "bulk" and self.request.method in ["PATCH", "DELETE"] + ) or self.action == "students__reset_password": queryset = queryset.filter( new_student__isnull=False, new_student__class_field__isnull=False, @@ -142,3 +144,20 @@ def request_password_reset(self, request: Request): "token": token, } ) + + @action(detail=False, methods=["patch"], url_path="students/reset-password") + def students__reset_password(self, request: Request): + """Bulk reset students' password.""" + queryset = self._get_bulk_queryset(request.data) + + passwords: t.Dict[int, str] = {} + for pk in queryset.values_list("pk", flat=True): + student_user = StudentUser(pk=pk) + student_user.set_password() + + # pylint: disable-next=protected-access + passwords[pk] = t.cast(str, student_user._password) + + student_user.save() # TODO: replace with bulk update + + return Response(passwords) From 19b43804a579502d7fd8ae88fbfb47fbd56b76e4 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 20 Feb 2024 10:04:54 +0000 Subject: [PATCH 02/13] move fixtures to py package --- backend/Pipfile.lock | 9 +- backend/api/fixtures/independent.json | 29 ---- backend/api/fixtures/non_school_teacher.json | 29 ---- backend/api/fixtures/school_1.json | 154 ------------------- backend/api/fixtures/school_2.json | 104 ------------- 5 files changed, 3 insertions(+), 322 deletions(-) delete mode 100644 backend/api/fixtures/independent.json delete mode 100644 backend/api/fixtures/non_school_teacher.json delete mode 100644 backend/api/fixtures/school_1.json delete mode 100644 backend/api/fixtures/school_2.json diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 095d1acb..eeee8435 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -168,7 +168,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "78c5b17988d899b2677f4a7d3a12408077d83351" + "ref": "143249578faf25e4163dde1e2f0be222b092b67f" }, "codeforlife-portal": { "hashes": [ @@ -806,7 +806,6 @@ "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" ], - "markers": "python_version >= '3.6'", "version": "==3.1.2" }, "pandas": { @@ -1305,7 +1304,6 @@ "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==2.0.1" }, "xlwt": { @@ -1514,7 +1512,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "78c5b17988d899b2677f4a7d3a12408077d83351" + "ref": "143249578faf25e4163dde1e2f0be222b092b67f" }, "codeforlife-portal": { "hashes": [ @@ -2186,7 +2184,6 @@ "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" ], - "markers": "python_version >= '3.6'", "version": "==3.1.2" }, "packaging": { @@ -2495,6 +2492,7 @@ "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5", "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42" ], + "markers": "python_version >= '3.7'", "version": "==7.2.1" }, "pytest-cov": { @@ -2897,7 +2895,6 @@ "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==2.0.1" }, "xlwt": { diff --git a/backend/api/fixtures/independent.json b/backend/api/fixtures/independent.json deleted file mode 100644 index 2bd37483..00000000 --- a/backend/api/fixtures/independent.json +++ /dev/null @@ -1,29 +0,0 @@ -[ - { - "model": "auth.user", - "pk": 28, - "fields": { - "first_name": "Indy", - "last_name": "Man", - "username": "indy@man.com", - "email": "indy@man.com", - "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" - } - }, - { - "model": "common.userprofile", - "pk": 28, - "fields": { - "user": 28, - "is_verified": true - } - }, - { - "model": "common.student", - "pk": 18, - "fields": { - "user": 28, - "new_user": 28 - } - } -] diff --git a/backend/api/fixtures/non_school_teacher.json b/backend/api/fixtures/non_school_teacher.json deleted file mode 100644 index bfaacaf5..00000000 --- a/backend/api/fixtures/non_school_teacher.json +++ /dev/null @@ -1,29 +0,0 @@ -[ - { - "model": "auth.user", - "pk": 22, - "fields": { - "first_name": "John", - "last_name": "Doe", - "username": "teacher@noschool.com", - "email": "teacher@noschool.com", - "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" - } - }, - { - "model": "common.userprofile", - "pk": 22, - "fields": { - "user": 22, - "is_verified": true - } - }, - { - "model": "common.teacher", - "pk": 5, - "fields": { - "user": 22, - "new_user": 22 - } - } -] \ No newline at end of file diff --git a/backend/api/fixtures/school_1.json b/backend/api/fixtures/school_1.json deleted file mode 100644 index c72454e6..00000000 --- a/backend/api/fixtures/school_1.json +++ /dev/null @@ -1,154 +0,0 @@ -[ - { - "model": "common.school", - "pk": 2, - "fields": { - "name": "School 1", - "country": "GB", - "county": "Hertfordshire" - } - }, - { - "model": "auth.user", - "pk": 23, - "fields": { - "first_name": "John", - "last_name": "Doe", - "username": "teacher@school1.com", - "email": "teacher@school1.com", - "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" - } - }, - { - "model": "common.userprofile", - "pk": 23, - "fields": { - "user": 23, - "is_verified": true - } - }, - { - "model": "common.teacher", - "pk": 6, - "fields": { - "user": 23, - "new_user": 23, - "school": 2 - } - }, - { - "model": "common.class", - "pk": 6, - "fields": { - "name": "Class 1 @ School 1", - "access_code": "ZZ111", - "teacher": 6 - } - }, - { - "model": "auth.user", - "pk": 27, - "fields": { - "first_name": "Student1", - "username": "111111111111111111111111111111", - "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" - } - }, - { - "model": "common.userprofile", - "pk": 27, - "fields": { - "user": 27, - "is_verified": true - } - }, - { - "model": "common.student", - "pk": 17, - "fields": { - "user": 27, - "new_user": 27, - "class_field": 6 - } - }, - { - "model": "auth.user", - "pk": 24, - "fields": { - "first_name": "Jane", - "last_name": "Doe", - "username": "admin.teacher@school1.com", - "email": "admin.teacher@school1.com", - "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" - } - }, - { - "model": "common.userprofile", - "pk": 24, - "fields": { - "user": 24, - "is_verified": true - } - }, - { - "model": "common.teacher", - "pk": 7, - "fields": { - "user": 24, - "new_user": 24, - "school": 2, - "is_admin": true - } - }, - { - "model": "common.class", - "pk": 7, - "fields": { - "name": "Class 2 @ School 1", - "access_code": "ZZ222", - "teacher": 7 - } - }, - { - "model": "common.schoolteacherinvitation", - "pk": 1, - "fields": { - "token": "pbkdf2_sha256$260000$hbsAadmrRo744BTM6NofUb$ePs/7vi6sSzOPpiWxNhXMZnNnE7aXOpzIhxrAa/rdiU=", - "school": 2, - "from_teacher": 7, - "invited_teacher_first_name": "Invited", - "invited_teacher_last_name": "Teacher", - "invited_teacher_email": "invited@teacher.com", - "invited_teacher_is_admin": false, - "expiry": "2024-02-09 20:26:08.298402+00:00" - } - }, - { - "model": "common.schoolteacherinvitation", - "pk": 2, - "fields": { - "token": "pbkdf2_sha256$260000$hbsAadmrRo744BTM6NofUb$ePs/7vi6sSzOPpiWxNhXMZnNnE7aXOpzIhxrAa/rdiU=", - "school": 2, - "from_teacher": 7, - "invited_teacher_first_name": "Invited", - "invited_teacher_last_name": "Teacher", - "invited_teacher_email": "invited@teacher.com", - "invited_teacher_is_admin": false, - "expiry": "9999-02-09 20:26:08.298402+00:00" - } - }, - { - "model": "common.schoolteacherinvitation", - "pk": 3, - "fields": { - "token": "pbkdf2_sha256$260000$hbsAadmrRo744BTM6NofUb$ePs/7vi6sSzOPpiWxNhXMZnNnE7aXOpzIhxrAa/rdiU=", - "school": 2, - "from_teacher": 7, - "invited_teacher_first_name": "Invited", - "invited_teacher_last_name": "Teacher", - "invited_teacher_email": "teacher@school1.com", - "invited_teacher_is_admin": false, - "expiry": "9999-02-09 20:26:08.298402+00:00" - } - } -] diff --git a/backend/api/fixtures/school_2.json b/backend/api/fixtures/school_2.json deleted file mode 100644 index 7e6e5dae..00000000 --- a/backend/api/fixtures/school_2.json +++ /dev/null @@ -1,104 +0,0 @@ -[ - { - "model": "common.school", - "pk": 3, - "fields": { - "name": "School 2", - "country": "GB", - "county": "Hertfordshire" - } - }, - { - "model": "auth.user", - "pk": 25, - "fields": { - "first_name": "John", - "last_name": "Doe", - "username": "teacher@school2.com", - "email": "teacher@school2.com", - "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" - } - }, - { - "model": "common.userprofile", - "pk": 25, - "fields": { - "user": 25, - "is_verified": true, - "otp_secret": "KI6FA34LPRQU265KQBFYS2MTDYHE2EIG" - } - }, - { - "model": "user.authfactor", - "pk": 1, - "fields": { - "user": 25, - "type": "otp" - } - }, - { - "model": "common.teacher", - "pk": 8, - "fields": { - "user": 25, - "new_user": 25, - "school": 3 - } - }, - { - "model": "common.class", - "pk": 8, - "fields": { - "name": "Class 1 @ School 2", - "access_code": "XX111", - "teacher": 8 - } - }, - { - "model": "auth.user", - "pk": 26, - "fields": { - "first_name": "Jane", - "last_name": "Doe", - "username": "admin.teacher@school2.com", - "email": "admin.teacher@school2.com", - "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" - } - }, - { - "model": "common.userprofile", - "pk": 26, - "fields": { - "user": 26, - "is_verified": true, - "otp_secret": "KI6FA34LPRQU265KQBFYS2MTDYHE2EIG" - } - }, - { - "model": "user.authfactor", - "pk": 2, - "fields": { - "user": 26, - "type": "otp" - } - }, - { - "model": "common.teacher", - "pk": 9, - "fields": { - "user": 26, - "new_user": 26, - "school": 3, - "is_admin": true - } - }, - { - "model": "common.class", - "pk": 9, - "fields": { - "name": "Class 2 @ School 2", - "access_code": "XX222", - "teacher": 9 - } - } -] \ No newline at end of file From e022dc3b0a58fc7e7bb47dd748baf6b63c2cd621 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 20 Feb 2024 12:17:27 +0000 Subject: [PATCH 03/13] add school teacher invitation fixtures --- .../school_1_teacher_invitations.json | 44 +++++++++++++++++++ .../test_school_teacher_invitation.py | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 backend/api/fixtures/school_1_teacher_invitations.json diff --git a/backend/api/fixtures/school_1_teacher_invitations.json b/backend/api/fixtures/school_1_teacher_invitations.json new file mode 100644 index 00000000..c1a0ab07 --- /dev/null +++ b/backend/api/fixtures/school_1_teacher_invitations.json @@ -0,0 +1,44 @@ +[ + { + "model": "common.schoolteacherinvitation", + "pk": 1, + "fields": { + "token": "pbkdf2_sha256$260000$hbsAadmrRo744BTM6NofUb$ePs/7vi6sSzOPpiWxNhXMZnNnE7aXOpzIhxrAa/rdiU=", + "school": 2, + "from_teacher": 7, + "invited_teacher_first_name": "Invited", + "invited_teacher_last_name": "Teacher", + "invited_teacher_email": "invited@teacher.com", + "invited_teacher_is_admin": false, + "expiry": "2024-02-09 20:26:08.298402+00:00" + } + }, + { + "model": "common.schoolteacherinvitation", + "pk": 2, + "fields": { + "token": "pbkdf2_sha256$260000$hbsAadmrRo744BTM6NofUb$ePs/7vi6sSzOPpiWxNhXMZnNnE7aXOpzIhxrAa/rdiU=", + "school": 2, + "from_teacher": 7, + "invited_teacher_first_name": "Invited", + "invited_teacher_last_name": "Teacher", + "invited_teacher_email": "invited@teacher.com", + "invited_teacher_is_admin": false, + "expiry": "9999-02-09 20:26:08.298402+00:00" + } + }, + { + "model": "common.schoolteacherinvitation", + "pk": 3, + "fields": { + "token": "pbkdf2_sha256$260000$hbsAadmrRo744BTM6NofUb$ePs/7vi6sSzOPpiWxNhXMZnNnE7aXOpzIhxrAa/rdiU=", + "school": 2, + "from_teacher": 7, + "invited_teacher_first_name": "Invited", + "invited_teacher_last_name": "Teacher", + "invited_teacher_email": "teacher@school1.com", + "invited_teacher_is_admin": false, + "expiry": "9999-02-09 20:26:08.298402+00:00" + } + } +] \ No newline at end of file diff --git a/backend/api/tests/serializers/test_school_teacher_invitation.py b/backend/api/tests/serializers/test_school_teacher_invitation.py index ca35323a..3790e1c9 100644 --- a/backend/api/tests/serializers/test_school_teacher_invitation.py +++ b/backend/api/tests/serializers/test_school_teacher_invitation.py @@ -18,7 +18,7 @@ class TestSchoolTeacherInvitationSerializer( ModelSerializerTestCase[SchoolTeacherInvitation] ): model_serializer_class = SchoolTeacherInvitationSerializer - fixtures = ["school_1"] + fixtures = ["school_1", "school_1_teacher_invitations"] def setUp(self): self.admin_school_teacher_user = AdminSchoolTeacherUser.objects.get( From 817698afb53eb9523a09bc9c59dccbf834954387 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 20 Feb 2024 12:33:38 +0000 Subject: [PATCH 04/13] use school teacher invitation fixtures --- backend/api/tests/views/test_school_teacher_invitation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/api/tests/views/test_school_teacher_invitation.py b/backend/api/tests/views/test_school_teacher_invitation.py index 97d0ec1d..df73b228 100644 --- a/backend/api/tests/views/test_school_teacher_invitation.py +++ b/backend/api/tests/views/test_school_teacher_invitation.py @@ -19,7 +19,11 @@ class TestSchoolTeacherInvitationViewSet( ): basename = "school-teacher-invitation" model_view_set_class = SchoolTeacherInvitationViewSet - fixtures = ["non_school_teacher", "school_1"] + fixtures = [ + "non_school_teacher", + "school_1", + "school_1_teacher_invitations", + ] school_admin_teacher_email = "admin.teacher@school1.com" non_school_teacher_email = "teacher@noschool.com" From 891e2da6be2a7284fa8eb8b1577697a3911c5633 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 20 Feb 2024 13:02:30 +0000 Subject: [PATCH 05/13] reset login id also --- backend/Pipfile.lock | 6 ++--- backend/api/tests/views/test_user.py | 36 ++++++++++++++++++---------- backend/api/views/user.py | 13 ++++++---- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index eeee8435..87992402 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -168,7 +168,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "143249578faf25e4163dde1e2f0be222b092b67f" + "ref": "68735e475981e985fcbdad7fe056d5f58c71912a" }, "codeforlife-portal": { "hashes": [ @@ -1512,7 +1512,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "143249578faf25e4163dde1e2f0be222b092b67f" + "ref": "68735e475981e985fcbdad7fe056d5f58c71912a" }, "codeforlife-portal": { "hashes": [ @@ -2492,7 +2492,7 @@ "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5", "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.10'", "version": "==7.2.1" }, "pytest-cov": { diff --git a/backend/api/tests/views/test_user.py b/backend/api/tests/views/test_user.py index 1f798d77..57ed3d63 100644 --- a/backend/api/tests/views/test_user.py +++ b/backend/api/tests/views/test_user.py @@ -296,30 +296,42 @@ def test_students__reset_password(self): """Teacher can bulk reset students' password.""" self.client.login_as(self.admin_school_teacher_user) - student_user_ids = list( + student_users = list( StudentUser.objects.filter( new_student__class_field__teacher__school=( self.admin_school_teacher_user.teacher.school ) - ).values_list("id", flat=True) + ) ) - assert student_user_ids + assert student_users response = self.client.patch( self.reverse_action("students--reset-password"), - student_user_ids, + [student_user.id for student_user in student_users], content_type="application/json", ) - passwords: JsonDict = response.json() - assert all(isinstance(password, str) for password in passwords.values()) + fields: JsonDict = response.json() + for student_user in student_users: + student_user_fields = t.cast(JsonDict, fields[str(student_user.id)]) - updated_student_user_ids = [ - int(student_user_id) for student_user_id in passwords.keys() - ] - student_user_ids.sort() - updated_student_user_ids.sort() - self.assertListEqual(student_user_ids, updated_student_user_ids) + password = t.cast(str, student_user_fields["password"]) + assert isinstance(password, str) + assert not student_user.check_password(password) + + student_login_id = t.cast( + str, + t.cast( + JsonDict, + student_user_fields["student"], + )["login_id"], + ) + assert isinstance(student_login_id, str) + assert student_user.student.login_id != student_login_id + + student_user.refresh_from_db() + assert student_user.check_password(password) + assert student_user.student.login_id == student_login_id # test: generic actions diff --git a/backend/api/views/user.py b/backend/api/views/user.py index 45ad3f1d..8248d91c 100644 --- a/backend/api/views/user.py +++ b/backend/api/views/user.py @@ -7,6 +7,7 @@ from codeforlife.permissions import OR from codeforlife.request import Request +from codeforlife.types import DataDict from codeforlife.user.models import StudentUser, User from codeforlife.user.permissions import IsTeacher from codeforlife.user.views import UserViewSet as _UserViewSet @@ -150,14 +151,18 @@ def students__reset_password(self, request: Request): """Bulk reset students' password.""" queryset = self._get_bulk_queryset(request.data) - passwords: t.Dict[int, str] = {} + fields: t.Dict[int, DataDict] = {} for pk in queryset.values_list("pk", flat=True): student_user = StudentUser(pk=pk) student_user.set_password() - # pylint: disable-next=protected-access - passwords[pk] = t.cast(str, student_user._password) + fields[pk] = { + # pylint: disable-next=protected-access + "password": student_user._password, + "student": {"login_id": student_user.student.login_id}, + } student_user.save() # TODO: replace with bulk update + student_user.student.save() # TODO: replace with bulk update - return Response(passwords) + return Response(fields) From 8bbc796ba97750216080efd214f8853267108b20 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 20 Feb 2024 14:58:21 +0000 Subject: [PATCH 06/13] new py package --- backend/Pipfile | 4 ++-- backend/Pipfile.lock | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/Pipfile b/backend/Pipfile index e1cecd9d..69bdcab2 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -7,7 +7,7 @@ name = "pypi" # Before adding a new package, check it's not listed under [packages] at # https://github.com/ocadotechnology/codeforlife-package-python/blob/{ref}/Pipfile # Replace "{ref}" in the above URL with the ref set below. -codeforlife = {ref = "reset_students_password", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} +codeforlife = {ref = "v0.13.8", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} # TODO: check if we need the below packages whitenoise = "==6.5.0" django-pipeline = "==2.0.8" @@ -34,7 +34,7 @@ google-cloud-container = "==2.3.0" # Before adding a new package, check it's not listed under [dev-packages] at # https://github.com/ocadotechnology/codeforlife-package-python/blob/{ref}/Pipfile # Replace "{ref}" in the above URL with the ref set below. -codeforlife = {ref = "reset_students_password", git = "https://github.com/ocadotechnology/codeforlife-package-python.git", extras = ["dev"]} +codeforlife = {ref = "v0.13.8", git = "https://github.com/ocadotechnology/codeforlife-package-python.git", extras = ["dev"]} # TODO: check if we need the below packages django-selenium-clean = "==0.3.3" django-test-migrations = "==1.2.0" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 87992402..57cbbaca 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "894ad5f63407ce1f006a370c2c2487d8e3874f0d503bd5b61fa3c06eaab399fe" + "sha256": "8fa0272b15e11ed5fcb092921c2977dba1e7b4d79df68811bc71540b430033e9" }, "pipfile-spec": 6, "requires": { @@ -168,7 +168,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "68735e475981e985fcbdad7fe056d5f58c71912a" + "ref": "4b2797f9b8ac681df7ae3e9406f255123cf55412" }, "codeforlife-portal": { "hashes": [ @@ -1058,7 +1058,7 @@ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.2" }, "pytz": { @@ -1209,7 +1209,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "sortedcontainers": { @@ -1512,7 +1512,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "68735e475981e985fcbdad7fe056d5f58c71912a" + "ref": "4b2797f9b8ac681df7ae3e9406f255123cf55412" }, "codeforlife-portal": { "hashes": [ @@ -2492,7 +2492,7 @@ "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5", "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42" ], - "markers": "python_version < '3.10'", + "markers": "python_version >= '3.7'", "version": "==7.2.1" }, "pytest-cov": { @@ -2551,7 +2551,7 @@ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.2" }, "pytz": { @@ -2727,7 +2727,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "snapshottest": { From 840d7ddcd2c77a27498b9e7b1f90661574966556 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 20 Feb 2024 16:31:23 +0000 Subject: [PATCH 07/13] feedback --- backend/api/tests/views/test_user.py | 1 + backend/api/views/user.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/api/tests/views/test_user.py b/backend/api/tests/views/test_user.py index 57ed3d63..23caf27a 100644 --- a/backend/api/tests/views/test_user.py +++ b/backend/api/tests/views/test_user.py @@ -331,6 +331,7 @@ def test_students__reset_password(self): student_user.refresh_from_db() assert student_user.check_password(password) + self.client.login_as(student_user, password) assert student_user.student.login_id == student_login_id # test: generic actions diff --git a/backend/api/views/user.py b/backend/api/views/user.py index 8248d91c..907c6212 100644 --- a/backend/api/views/user.py +++ b/backend/api/views/user.py @@ -162,7 +162,8 @@ def students__reset_password(self, request: Request): "student": {"login_id": student_user.student.login_id}, } - student_user.save() # TODO: replace with bulk update - student_user.student.save() # TODO: replace with bulk update + # TODO: replace with bulk update + student_user.save(update_fields=["password"]) + student_user.student.save(update_fields=["login_id"]) return Response(fields) From 6fdcdd59428b4327e825a225de1a5a4f0977bbde Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 20 Feb 2024 16:50:37 +0000 Subject: [PATCH 08/13] use new py package --- backend/Pipfile | 4 ++-- backend/Pipfile.lock | 10 +++++++--- backend/api/views/user.py | 3 +++ backend/sso/permissions.py | 2 +- backend/sso/views.py | 5 ++--- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/backend/Pipfile b/backend/Pipfile index 69bdcab2..261640be 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -7,7 +7,7 @@ name = "pypi" # Before adding a new package, check it's not listed under [packages] at # https://github.com/ocadotechnology/codeforlife-package-python/blob/{ref}/Pipfile # Replace "{ref}" in the above URL with the ref set below. -codeforlife = {ref = "v0.13.8", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} +codeforlife = {ref = "v0.13.9", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} # TODO: check if we need the below packages whitenoise = "==6.5.0" django-pipeline = "==2.0.8" @@ -34,7 +34,7 @@ google-cloud-container = "==2.3.0" # Before adding a new package, check it's not listed under [dev-packages] at # https://github.com/ocadotechnology/codeforlife-package-python/blob/{ref}/Pipfile # Replace "{ref}" in the above URL with the ref set below. -codeforlife = {ref = "v0.13.8", git = "https://github.com/ocadotechnology/codeforlife-package-python.git", extras = ["dev"]} +codeforlife = {ref = "v0.13.9", git = "https://github.com/ocadotechnology/codeforlife-package-python.git", extras = ["dev"]} # TODO: check if we need the below packages django-selenium-clean = "==0.3.3" django-test-migrations = "==1.2.0" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 57cbbaca..fbdefb16 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8fa0272b15e11ed5fcb092921c2977dba1e7b4d79df68811bc71540b430033e9" + "sha256": "133387961394ddba0202782e2bb4265b005cd74c06be1456cf8cae8c3df2a832" }, "pipfile-spec": 6, "requires": { @@ -168,7 +168,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "4b2797f9b8ac681df7ae3e9406f255123cf55412" + "ref": "cd70f4fc980eee04a029402029a1dfb52a0f1e3e" }, "codeforlife-portal": { "hashes": [ @@ -806,6 +806,7 @@ "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" ], + "markers": "python_version >= '3.6'", "version": "==3.1.2" }, "pandas": { @@ -1304,6 +1305,7 @@ "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==2.0.1" }, "xlwt": { @@ -1512,7 +1514,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "4b2797f9b8ac681df7ae3e9406f255123cf55412" + "ref": "cd70f4fc980eee04a029402029a1dfb52a0f1e3e" }, "codeforlife-portal": { "hashes": [ @@ -2184,6 +2186,7 @@ "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" ], + "markers": "python_version >= '3.6'", "version": "==3.1.2" }, "packaging": { @@ -2895,6 +2898,7 @@ "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==2.0.1" }, "xlwt": { diff --git a/backend/api/views/user.py b/backend/api/views/user.py index 907c6212..ba514898 100644 --- a/backend/api/views/user.py +++ b/backend/api/views/user.py @@ -56,6 +56,9 @@ def get_queryset(self): def perform_bulk_destroy(self, queryset): queryset.update(first_name="", is_active=False) + # def destroy(self, request: Request): + # return super().des + @action( detail=True, methods=["get", "patch"], diff --git a/backend/sso/permissions.py b/backend/sso/permissions.py index b273b355..2491da88 100644 --- a/backend/sso/permissions.py +++ b/backend/sso/permissions.py @@ -13,5 +13,5 @@ class UserHasSessionAuthFactors(BasePermission): def has_permission(self, request: Request, view: View): return ( isinstance(request.user, User) - and request.user.session.session_auth_factors.exists() + and request.user.session.auth_factors.exists() ) diff --git a/backend/sso/views.py b/backend/sso/views.py index dec499e3..cb10e5d1 100644 --- a/backend/sso/views.py +++ b/backend/sso/views.py @@ -94,7 +94,7 @@ def form_valid(self, form: BaseAuthForm): # type: ignore { "user_id": user.id, "auth_factors": list( - user.session.session_auth_factors.values_list( + user.session.auth_factors.values_list( "auth_factor__type", flat=True ) ), @@ -125,10 +125,9 @@ class LoginOptionsView(APIView): def get(self, request: Request): user = t.cast(User, request.user) - session_auth_factors = user.session.session_auth_factors response_data = {"id": user.id} - if session_auth_factors.filter( + if user.session.auth_factors.filter( auth_factor__type=AuthFactor.Type.OTP ).exists(): response_data[ From e5784c1a2ad102c4cd4ccd26cf58e0c482a1412d Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 20 Feb 2024 16:53:46 +0000 Subject: [PATCH 09/13] remove comment --- backend/api/views/user.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/api/views/user.py b/backend/api/views/user.py index ba514898..907c6212 100644 --- a/backend/api/views/user.py +++ b/backend/api/views/user.py @@ -56,9 +56,6 @@ def get_queryset(self): def perform_bulk_destroy(self, queryset): queryset.update(first_name="", is_active=False) - # def destroy(self, request: Request): - # return super().des - @action( detail=True, methods=["get", "patch"], From 5b97a1d791b81969b6bfd5573fbc653732693eaf Mon Sep 17 00:00:00 2001 From: SKairinos Date: Tue, 20 Feb 2024 19:05:23 +0000 Subject: [PATCH 10/13] new python package --- backend/Pipfile | 4 ++-- backend/Pipfile.lock | 14 +++++++------- backend/api/serializers/auth_factor.py | 6 +++--- backend/api/serializers/klass.py | 6 +++--- .../api/serializers/school_teacher_invitation.py | 2 +- backend/api/serializers/student.py | 2 +- backend/api/serializers/teacher.py | 2 +- backend/api/serializers/user.py | 2 +- backend/api/views/auth_factor.py | 2 +- backend/api/views/otp_bypass_token.py | 9 +++------ backend/api/views/school_teacher_invitation.py | 2 +- 11 files changed, 24 insertions(+), 27 deletions(-) diff --git a/backend/Pipfile b/backend/Pipfile index 261640be..30b620b8 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -7,7 +7,7 @@ name = "pypi" # Before adding a new package, check it's not listed under [packages] at # https://github.com/ocadotechnology/codeforlife-package-python/blob/{ref}/Pipfile # Replace "{ref}" in the above URL with the ref set below. -codeforlife = {ref = "v0.13.9", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} +codeforlife = {ref = "anonymize_user", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} # TODO: check if we need the below packages whitenoise = "==6.5.0" django-pipeline = "==2.0.8" @@ -34,7 +34,7 @@ google-cloud-container = "==2.3.0" # Before adding a new package, check it's not listed under [dev-packages] at # https://github.com/ocadotechnology/codeforlife-package-python/blob/{ref}/Pipfile # Replace "{ref}" in the above URL with the ref set below. -codeforlife = {ref = "v0.13.9", git = "https://github.com/ocadotechnology/codeforlife-package-python.git", extras = ["dev"]} +codeforlife = {ref = "anonymize_user", git = "https://github.com/ocadotechnology/codeforlife-package-python.git", extras = ["dev"]} # TODO: check if we need the below packages django-selenium-clean = "==0.3.3" django-test-migrations = "==1.2.0" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index fbdefb16..9190e051 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "133387961394ddba0202782e2bb4265b005cd74c06be1456cf8cae8c3df2a832" + "sha256": "956e010184988684d4473d057a88bdac4a0edaa2c2b6f1f61971d8e6a7dd8b91" }, "pipfile-spec": 6, "requires": { @@ -168,7 +168,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "cd70f4fc980eee04a029402029a1dfb52a0f1e3e" + "ref": "87de72a17508dad87d081dab51038321ae6b247b" }, "codeforlife-portal": { "hashes": [ @@ -1059,7 +1059,7 @@ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.8.2" }, "pytz": { @@ -1210,7 +1210,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sortedcontainers": { @@ -1514,7 +1514,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "cd70f4fc980eee04a029402029a1dfb52a0f1e3e" + "ref": "87de72a17508dad87d081dab51038321ae6b247b" }, "codeforlife-portal": { "hashes": [ @@ -2554,7 +2554,7 @@ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.8.2" }, "pytz": { @@ -2730,7 +2730,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "snapshottest": { diff --git a/backend/api/serializers/auth_factor.py b/backend/api/serializers/auth_factor.py index 09b5ad20..63def67b 100644 --- a/backend/api/serializers/auth_factor.py +++ b/backend/api/serializers/auth_factor.py @@ -24,7 +24,7 @@ class Meta: # pylint: disable-next=missing-function-docstring def validate_type(self, value: str): if AuthFactor.objects.filter( - user=self.request_user, type=value + user=self.request.auth_user, type=value ).exists(): raise serializers.ValidationError( "You already have this authentication factor enabled.", @@ -34,7 +34,7 @@ def validate_type(self, value: str): return value def create(self, validated_data): - user = self.request_user + user = self.request.auth_user if not user.userprofile.otp_secret: user.userprofile.otp_secret = pyotp.random_base32() user.userprofile.save() @@ -51,6 +51,6 @@ def to_representation(self, instance): ): representation[ "totp_provisioning_uri" - ] = self.request_user.totp_provisioning_uri + ] = self.request.auth_user.totp_provisioning_uri return representation diff --git a/backend/api/serializers/klass.py b/backend/api/serializers/klass.py index 8b758535..da97b09c 100644 --- a/backend/api/serializers/klass.py +++ b/backend/api/serializers/klass.py @@ -42,7 +42,7 @@ def validate_teacher(self, value: int): code="does_not_exist", ) - user = self.request_school_teacher_user + user = self.request.school_teacher_user if not queryset.filter(school=user.teacher.school_id).exists(): raise serializers.ValidationError( "This teacher is not in your school.", @@ -60,7 +60,7 @@ def validate_teacher(self, value: int): # pylint: disable-next=missing-function-docstring def validate_name(self, value: str): if Class.objects.filter( - teacher__school=self.request_school_teacher_user.teacher.school, + teacher__school=self.request.school_teacher_user.teacher.school, name=value, ).exists(): raise serializers.ValidationError( @@ -90,7 +90,7 @@ def create(self, validated_data): "teacher_id": ( validated_data["teacher"]["id"] if "teacher" in validated_data - else self.request_school_teacher_user.teacher.id + else self.request.school_teacher_user.teacher.id ), "classmates_data_viewable": validated_data[ "classmates_data_viewable" diff --git a/backend/api/serializers/school_teacher_invitation.py b/backend/api/serializers/school_teacher_invitation.py index 3f94a961..c2b20394 100644 --- a/backend/api/serializers/school_teacher_invitation.py +++ b/backend/api/serializers/school_teacher_invitation.py @@ -39,7 +39,7 @@ class Meta: } def create(self, validated_data): - user = self.request_admin_school_teacher_user + user = self.request.admin_school_teacher_user token = get_random_string(length=32) validated_data["token"] = make_password(token) diff --git a/backend/api/serializers/student.py b/backend/api/serializers/student.py index 4aaaeaaa..e8852ddf 100644 --- a/backend/api/serializers/student.py +++ b/backend/api/serializers/student.py @@ -20,7 +20,7 @@ class Meta(_StudentSerializer.Meta): # pylint: disable-next=missing-function-docstring def validate_klass(self, value: str): # Only teachers can manage students. - teacher = self.request_school_teacher_user.teacher + teacher = self.request.school_teacher_user.teacher try: klass = Class.objects.get(access_code=value) diff --git a/backend/api/serializers/teacher.py b/backend/api/serializers/teacher.py index ef868e69..c3d5e719 100644 --- a/backend/api/serializers/teacher.py +++ b/backend/api/serializers/teacher.py @@ -22,7 +22,7 @@ class Meta(_TeacherSerializer.Meta): def validate_is_admin(self, value: bool): instance = t.cast(t.Optional[User], self.parent.instance) if instance: - user = self.request_school_teacher_user + user = self.request.school_teacher_user if user.pk == instance.pk: raise serializers.ValidationError( "Cannot update own permissions.", diff --git a/backend/api/serializers/user.py b/backend/api/serializers/user.py index ef726f6f..5903d202 100644 --- a/backend/api/serializers/user.py +++ b/backend/api/serializers/user.py @@ -189,7 +189,7 @@ def to_representation(self, instance: User): # Return student's auto-generated password. if ( representation["student"] is not None - and self.request_user.teacher is not None + and self.request.auth_user.teacher is not None ): # pylint: disable-next=protected-access password = instance._password diff --git a/backend/api/views/auth_factor.py b/backend/api/views/auth_factor.py index f417efb4..1c813cf3 100644 --- a/backend/api/views/auth_factor.py +++ b/backend/api/views/auth_factor.py @@ -17,7 +17,7 @@ class AuthFactorViewSet(ModelViewSet[AuthFactor]): serializer_class = AuthFactorSerializer def get_queryset(self): - return AuthFactor.objects.filter(user=self.request_user) + return AuthFactor.objects.filter(user=self.request.auth_user) def get_permissions(self): if self.action in ["retrieve", "bulk"]: diff --git a/backend/api/views/otp_bypass_token.py b/backend/api/views/otp_bypass_token.py index c5d50d7a..ae5c9559 100644 --- a/backend/api/views/otp_bypass_token.py +++ b/backend/api/views/otp_bypass_token.py @@ -3,11 +3,9 @@ Created on 23/01/2024 at 17:54:08(+00:00). """ -import typing as t - from codeforlife.permissions import AllowNone from codeforlife.request import Request -from codeforlife.user.models import OtpBypassToken, User +from codeforlife.user.models import OtpBypassToken from codeforlife.user.permissions import IsTeacher from codeforlife.views import ModelViewSet from rest_framework import status @@ -20,8 +18,7 @@ class OtpBypassTokenViewSet(ModelViewSet[OtpBypassToken]): http_method_names = ["post"] def get_queryset(self): - user: User = self.request.user # type: ignore[assignment] - return OtpBypassToken.objects.filter(user=user) + return OtpBypassToken.objects.filter(user=self.request.teacher_user) def get_permissions(self): if self.action == "create": @@ -33,7 +30,7 @@ def get_permissions(self): def generate(self, request: Request): """Generates some OTP bypass tokens for a user.""" - user = t.cast(User, request.user) + user = request.auth_user OtpBypassToken.objects.filter(user=user).delete() diff --git a/backend/api/views/school_teacher_invitation.py b/backend/api/views/school_teacher_invitation.py index bff91cb3..1d9c2ed2 100644 --- a/backend/api/views/school_teacher_invitation.py +++ b/backend/api/views/school_teacher_invitation.py @@ -43,7 +43,7 @@ def get_queryset(self): return queryset return queryset.filter( - school=self.request_admin_school_teacher_user.teacher.school + school=self.request.admin_school_teacher_user.teacher.school ) @action( From d928948792dcfe250aaac0d8ad103b560e7add98 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Wed, 21 Feb 2024 15:51:17 +0000 Subject: [PATCH 11/13] anonymize independent- or teacher-user --- backend/api/tests/views/test_user.py | 199 ++++++++++++++++++++++----- backend/api/views/user.py | 47 ++++++- 2 files changed, 207 insertions(+), 39 deletions(-) diff --git a/backend/api/tests/views/test_user.py b/backend/api/tests/views/test_user.py index 23caf27a..a09c88c8 100644 --- a/backend/api/tests/views/test_user.py +++ b/backend/api/tests/views/test_user.py @@ -11,15 +11,21 @@ from codeforlife.user.models import ( AdminSchoolTeacherUser, Class, + IndependentUser, + NonAdminSchoolTeacherUser, + NonSchoolTeacherUser, SchoolTeacherUser, + Student, StudentUser, + TypedUser, User, ) -from codeforlife.user.permissions import IsTeacher +from codeforlife.user.permissions import IsIndependent, IsTeacher from django.contrib.auth.tokens import ( PasswordResetTokenGenerator, default_token_generator, ) +from django.db.models.query import QuerySet from rest_framework import status from ...views import UserViewSet @@ -28,26 +34,23 @@ default_token_generator: PasswordResetTokenGenerator = default_token_generator -# pylint: disable-next=missing-class-docstring +# pylint: disable-next=missing-class-docstring,too-many-public-methods class TestUserViewSet(ModelViewSetTestCase[User]): basename = "user" model_view_set_class = UserViewSet fixtures = ["independent", "non_school_teacher", "school_1"] - non_school_teacher_email = "teacher@noschool.com" - school_teacher_email = "teacher@school1.com" - indy_email = "indy@man.com" - def setUp(self): + self.non_school_teacher_user = NonSchoolTeacherUser.objects.get( + email="teacher@noschool.com" + ) self.admin_school_teacher_user = AdminSchoolTeacherUser.objects.get( email="admin.teacher@school1.com" ) - - def _login_non_admin_school_teacher(self): - return self.client.login_non_admin_school_teacher( - email=self.school_teacher_email, - password="password", + self.non_admin_school_teacher_user = ( + NonAdminSchoolTeacherUser.objects.get(email="teacher@school1.com") ) + self.indy_user = IndependentUser.objects.get(email="indy@man.com") def _get_pk_and_token_for_user(self, email: str): user = User.objects.get(email__iexact=email) @@ -60,9 +63,7 @@ def _get_pk_and_token_for_user(self, email: str): def test_get_permissions__bulk(self): """Only admin-teachers or class-teachers can perform bulk actions.""" self.assert_get_permissions( - permissions=[ - OR(IsTeacher(is_admin=True), IsTeacher(in_class=True)) - ], + [OR(IsTeacher(is_admin=True), IsTeacher(in_class=True))], action="bulk", ) @@ -71,16 +72,14 @@ def test_get_permissions__students__reset_password(self): Only admin-teachers or class-teachers can reset students' passwords. """ self.assert_get_permissions( - permissions=[ - OR(IsTeacher(is_admin=True), IsTeacher(in_class=True)) - ], + [OR(IsTeacher(is_admin=True), IsTeacher(in_class=True))], action="students__reset_password", ) def test_get_permissions__partial_update__teacher(self): """Only admin-teachers can update a teacher.""" self.assert_get_permissions( - permissions=[IsTeacher(is_admin=True)], + [IsTeacher(is_admin=True)], action="partial_update", request=self.client.request_factory.patch(data={"teacher": {}}), ) @@ -88,16 +87,23 @@ def test_get_permissions__partial_update__teacher(self): def test_get_permissions__partial_update__student(self): """Only admin-teachers or class-teachers can update a student.""" self.assert_get_permissions( - permissions=[ - OR(IsTeacher(is_admin=True), IsTeacher(in_class=True)) - ], + [OR(IsTeacher(is_admin=True), IsTeacher(in_class=True))], action="partial_update", request=self.client.request_factory.patch(data={"student": {}}), ) + def test_get_permissions__destroy(self): + """Only independents or teachers can destroy a user.""" + self.assert_get_permissions( + [OR(IsTeacher(), IsIndependent())], + action="destroy", + ) + # test: get queryset - def _test_get_queryset(self, action: str, request_method: str): + def _test_get_queryset__student_users( + self, action: str, request_method: str + ): student_users = list( User.objects.filter( new_student__class_field__teacher__school=( @@ -115,25 +121,41 @@ def _test_get_queryset(self, action: str, request_method: str): def test_get_queryset__bulk__patch(self): """Bulk partial-update can only target student-users.""" - self._test_get_queryset(action="bulk", request_method="patch") + self._test_get_queryset__student_users( + action="bulk", request_method="patch" + ) def test_get_queryset__bulk__delete(self): """Bulk destroy can only target student-users.""" - self._test_get_queryset(action="bulk", request_method="delete") + self._test_get_queryset__student_users( + action="bulk", request_method="delete" + ) def test_get_queryset__students__reset_password(self): """Resetting student passwords can only target student-users.""" - self._test_get_queryset( + self._test_get_queryset__student_users( action="students__reset_password", request_method="patch" ) + def test_get_queryset__destroy(self): + """Destroying a user can only target the user making the request.""" + return self.assert_get_queryset( + [self.admin_school_teacher_user], + action="destroy", + request=self.client.request_factory.delete( + user=self.admin_school_teacher_user + ), + ) + # test: bulk actions def test_bulk_create__students(self): """Teacher can bulk create students.""" - user = self._login_non_admin_school_teacher() + self.client.login_as(self.non_admin_school_teacher_user) - klass: t.Optional[Class] = user.teacher.class_teacher.first() + klass: t.Optional[ + Class + ] = self.non_admin_school_teacher_user.teacher.class_teacher.first() assert klass is not None response = self.client.bulk_create( @@ -209,7 +231,7 @@ def test_request_password_reset__valid_email(self): viewname = self.reverse_action("request-password-reset") response = self.client.post( - viewname, data={"email": self.non_school_teacher_email} + viewname, data={"email": self.non_school_teacher_user.email} ) assert response.data["reset_password_url"] is not None @@ -219,7 +241,7 @@ def test_request_password_reset__valid_email(self): def test_reset_password__invalid_pk(self): """Reset password raises 400 on GET with invalid pk""" _, token = self._get_pk_and_token_for_user( - self.non_school_teacher_email + self.non_school_teacher_user.email ) viewname = self.reverse_action( @@ -236,7 +258,9 @@ def test_reset_password__invalid_pk(self): def test_reset_password__invalid_token(self): """Reset password raises 400 on GET with invalid token""" - pk, _ = self._get_pk_and_token_for_user(self.non_school_teacher_email) + pk, _ = self._get_pk_and_token_for_user( + self.non_school_teacher_user.email + ) viewname = self.reverse_action( "reset-password", kwargs={"pk": pk, "token": "whatever"} @@ -253,7 +277,7 @@ def test_reset_password__invalid_token(self): def test_reset_password__get(self): """Reset password GET succeeds.""" pk, token = self._get_pk_and_token_for_user( - self.non_school_teacher_email + self.non_school_teacher_user.email ) viewname = self.reverse_action( @@ -265,7 +289,7 @@ def test_reset_password__get(self): def test_reset_password__patch__teacher(self): """Teacher can successfully update password.""" pk, token = self._get_pk_and_token_for_user( - self.non_school_teacher_email + self.non_school_teacher_user.email ) viewname = self.reverse_action( @@ -273,14 +297,14 @@ def test_reset_password__patch__teacher(self): ) self.client.patch(viewname, data={"password": "N3wPassword!"}) - self.client.login( - email=self.non_school_teacher_email, + self.client.login_as( + self.non_school_teacher_user, password="N3wPassword!", ) def test_reset_password__patch__indy(self): """Indy can successfully update password.""" - pk, token = self._get_pk_and_token_for_user(self.indy_email) + pk, token = self._get_pk_and_token_for_user(self.indy_user.email) viewname = self.reverse_action( "reset-password", @@ -288,7 +312,7 @@ def test_reset_password__patch__indy(self): ) self.client.patch(viewname, data={"password": "N3wPassword"}) - self.client.login(email=self.indy_email, password="N3wPassword") + self.client.login_as(self.indy_user, password="N3wPassword") # test: students actions @@ -361,3 +385,106 @@ def test_partial_update__teacher(self): }, }, ) + + def assert_user_is_anonymized(self, user: User): + """Assert user has been anonymized. + + Args: + user: The user to assert. + """ + assert user.first_name == "" + assert user.last_name == "" + assert user.email == "" + assert not user.is_active + + def assert_classes_are_anonymized( + self, + school_teacher_user: SchoolTeacherUser, + class_names: t.Iterable[str], + ): + """Assert the classes and their students have been anonymized. + + Args: + school_teacher_user: The user the classes belong to. + class_names: The original class names. + """ + # TODO: remove when using new data strategy + queryset = QuerySet( + model=Class.objects.model, + using=Class.objects._db, + hints=Class.objects._hints, + ).filter(teacher=school_teacher_user.teacher) + + for klass, name in zip(queryset, class_names): + assert klass.name != name + assert klass.access_code == "" + assert not klass.is_active + + student: Student # TODO: delete in new data schema + for student in klass.students.all(): + self.assert_user_is_anonymized(student.new_user) + + def _test_destroy( + self, + user: TypedUser, + status_code_assertion: int = status.HTTP_204_NO_CONTENT, + ): + self.client.login_as(user) + self.client.destroy( + user, + status_code_assertion=status_code_assertion, + make_assertions=False, + ) + + def test_destroy__class_teacher(self): + """Class-teacher-users can anonymize themself and their classes.""" + user = self.non_admin_school_teacher_user + assert user.teacher.class_teacher.exists() + class_names = list( + user.teacher.class_teacher.values_list("name", flat=True) + ) + + self._test_destroy(user) + user.refresh_from_db() + self.assert_user_is_anonymized(user) + self.assert_classes_are_anonymized(user, class_names) + + def test_destroy__school_teacher__last_teacher(self): + """ + School-teacher-users can anonymize themself and their school if they are + the last teacher. + """ + user = self.admin_school_teacher_user + assert user.teacher.class_teacher.exists() + class_names = list( + user.teacher.class_teacher.values_list("name", flat=True) + ) + school_name = user.teacher.school.name + + SchoolTeacherUser.objects.filter( + new_teacher__school=user.teacher.school + ).exclude(pk=user.pk).delete() + + self._test_destroy(user) + user.refresh_from_db() + self.assert_user_is_anonymized(user) + self.assert_classes_are_anonymized(user, class_names) + assert user.teacher.school.name != school_name + assert not user.teacher.school.is_active + + def test_destroy__school_teacher__last_admin_teacher(self): + """ + School-teacher-users cannot anonymize themself if they are the last + admin teachers. + """ + self._test_destroy( + self.admin_school_teacher_user, + status_code_assertion=status.HTTP_409_CONFLICT, + ) + + def test_destroy__independent(self): + """Independent-users can anonymize themself.""" + user = self.indy_user + self._test_destroy(user) + user.refresh_from_db() + self.assert_user_is_anonymized(user) diff --git a/backend/api/views/user.py b/backend/api/views/user.py index 907c6212..6b4f9883 100644 --- a/backend/api/views/user.py +++ b/backend/api/views/user.py @@ -8,8 +8,8 @@ from codeforlife.permissions import OR from codeforlife.request import Request from codeforlife.types import DataDict -from codeforlife.user.models import StudentUser, User -from codeforlife.user.permissions import IsTeacher +from codeforlife.user.models import Class, SchoolTeacher, StudentUser, User +from codeforlife.user.permissions import IsIndependent, IsTeacher from codeforlife.user.views import UserViewSet as _UserViewSet from django.contrib.auth.tokens import ( PasswordResetTokenGenerator, @@ -32,6 +32,8 @@ class UserViewSet(_UserViewSet): serializer_class = UserSerializer def get_permissions(self): + if self.action == "destroy": + return [OR(IsTeacher(), IsIndependent())] if self.action in ["bulk", "students__reset_password"]: return [OR(IsTeacher(is_admin=True), IsTeacher(in_class=True))] if self.action == "partial_update": @@ -44,7 +46,9 @@ def get_permissions(self): def get_queryset(self): queryset = super().get_queryset() - if ( + if self.action == "destroy": + queryset = queryset.filter(pk=self.request.auth_user.pk) + elif ( self.action == "bulk" and self.request.method in ["PATCH", "DELETE"] ) or self.action == "students__reset_password": queryset = queryset.filter( @@ -56,6 +60,43 @@ def get_queryset(self): def perform_bulk_destroy(self, queryset): queryset.update(first_name="", is_active=False) + def destroy(self, request, *args, **kwargs): + user = self.get_object() + + def anonymize_user(user: User): + user.first_name = "" + user.last_name = "" + user.email = "" + user.is_active = False + user.save() + + if user.teacher: + if user.teacher.school: + other_school_teachers = SchoolTeacher.objects.filter( + school=user.teacher.school + ).exclude(pk=user.teacher.pk) + + if not other_school_teachers.exists(): + user.teacher.school.anonymise() + elif ( + user.teacher.is_admin + and not other_school_teachers.filter(is_admin=True).exists() + ): + return Response(status=status.HTTP_409_CONFLICT) + + klass: Class # TODO: delete in new data schema + for klass in user.teacher.class_teacher.all(): + for student_user in StudentUser.objects.filter( + new_student__class_field=klass + ): + anonymize_user(student_user) + + klass.anonymise() + + anonymize_user(user) + + return Response(status=status.HTTP_204_NO_CONTENT) + @action( detail=True, methods=["get", "patch"], From c451bce47ec91ff02360910f10be52ff886bdb20 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Wed, 21 Feb 2024 16:39:57 +0000 Subject: [PATCH 12/13] remove duplicate test --- backend/api/tests/views/test_user.py | 44 ---------------------------- 1 file changed, 44 deletions(-) diff --git a/backend/api/tests/views/test_user.py b/backend/api/tests/views/test_user.py index ac2600d1..a09c88c8 100644 --- a/backend/api/tests/views/test_user.py +++ b/backend/api/tests/views/test_user.py @@ -316,50 +316,6 @@ def test_reset_password__patch__indy(self): # test: students actions - def test_students__reset_password(self): - """Teacher can bulk reset students' password.""" - self.client.login_as(self.admin_school_teacher_user) - - student_users = list( - StudentUser.objects.filter( - new_student__class_field__teacher__school=( - self.admin_school_teacher_user.teacher.school - ) - ) - ) - assert student_users - - response = self.client.patch( - self.reverse_action("students--reset-password"), - [student_user.id for student_user in student_users], - content_type="application/json", - ) - - fields: JsonDict = response.json() - for student_user in student_users: - student_user_fields = t.cast(JsonDict, fields[str(student_user.id)]) - - password = t.cast(str, student_user_fields["password"]) - assert isinstance(password, str) - assert not student_user.check_password(password) - - student_login_id = t.cast( - str, - t.cast( - JsonDict, - student_user_fields["student"], - )["login_id"], - ) - assert isinstance(student_login_id, str) - assert student_user.student.login_id != student_login_id - - student_user.refresh_from_db() - assert student_user.check_password(password) - self.client.login_as(student_user, password) - assert student_user.student.login_id == student_login_id - - # test: students actions - def test_students__reset_password(self): """Teacher can bulk reset students' password.""" self.client.login_as(self.admin_school_teacher_user) From 681823c4cf200ff50c5e4cc3283af95d8a4a3a74 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 22 Feb 2024 09:05:53 +0000 Subject: [PATCH 13/13] new py package --- backend/Pipfile | 4 +- backend/Pipfile.lock | 236 +++++++++++++++++++++---------------------- 2 files changed, 120 insertions(+), 120 deletions(-) diff --git a/backend/Pipfile b/backend/Pipfile index 30b620b8..372cfc58 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -7,7 +7,7 @@ name = "pypi" # Before adding a new package, check it's not listed under [packages] at # https://github.com/ocadotechnology/codeforlife-package-python/blob/{ref}/Pipfile # Replace "{ref}" in the above URL with the ref set below. -codeforlife = {ref = "anonymize_user", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} +codeforlife = {ref = "v0.13.10", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} # TODO: check if we need the below packages whitenoise = "==6.5.0" django-pipeline = "==2.0.8" @@ -34,7 +34,7 @@ google-cloud-container = "==2.3.0" # Before adding a new package, check it's not listed under [dev-packages] at # https://github.com/ocadotechnology/codeforlife-package-python/blob/{ref}/Pipfile # Replace "{ref}" in the above URL with the ref set below. -codeforlife = {ref = "anonymize_user", git = "https://github.com/ocadotechnology/codeforlife-package-python.git", extras = ["dev"]} +codeforlife = {ref = "v0.13.10", git = "https://github.com/ocadotechnology/codeforlife-package-python.git", extras = ["dev"]} # TODO: check if we need the below packages django-selenium-clean = "==0.3.3" django-test-migrations = "==1.2.0" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index fa456bdb..5bba802a 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "956e010184988684d4473d057a88bdac4a0edaa2c2b6f1f61971d8e6a7dd8b91" + "sha256": "c3b11b9d8699621ec2071fed98072adb143c11ae3dd29b642c565fb491add82d" }, "pipfile-spec": 6, "requires": { @@ -168,7 +168,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "87de72a17508dad87d081dab51038321ae6b247b" + "ref": "9f3c8292be7400cef9255cdf6283f9bb7edea955" }, "codeforlife-portal": { "hashes": [ @@ -501,62 +501,62 @@ }, "grpcio": { "hashes": [ - "sha256:0250a7a70b14000fa311de04b169cc7480be6c1a769b190769d347939d3232a8", - "sha256:069fe2aeee02dfd2135d562d0663fe70fbb69d5eed6eb3389042a7e963b54de8", - "sha256:082081e6a36b6eb5cf0fd9a897fe777dbb3802176ffd08e3ec6567edd85bc104", - "sha256:0c5807e9152eff15f1d48f6b9ad3749196f79a4a050469d99eecb679be592acc", - "sha256:14e8f2c84c0832773fb3958240c69def72357bc11392571f87b2d7b91e0bb092", - "sha256:2a6087f234cb570008a6041c8ffd1b7d657b397fdd6d26e83d72283dae3527b1", - "sha256:2bb2a2911b028f01c8c64d126f6b632fcd8a9ac975aa1b3855766c94e4107180", - "sha256:2f44c32aef186bbba254129cea1df08a20be414144ac3bdf0e84b24e3f3b2e05", - "sha256:30e980cd6db1088c144b92fe376747328d5554bc7960ce583ec7b7d81cd47287", - "sha256:33aed0a431f5befeffd9d346b0fa44b2c01aa4aeae5ea5b2c03d3e25e0071216", - "sha256:33bdea30dcfd4f87b045d404388469eb48a48c33a6195a043d116ed1b9a0196c", - "sha256:39aa848794b887120b1d35b1b994e445cc028ff602ef267f87c38122c1add50d", - "sha256:4216e67ad9a4769117433814956031cb300f85edc855252a645a9a724b3b6594", - "sha256:49c9b6a510e3ed8df5f6f4f3c34d7fbf2d2cae048ee90a45cd7415abab72912c", - "sha256:4eec8b8c1c2c9b7125508ff7c89d5701bf933c99d3910e446ed531cd16ad5d87", - "sha256:50d56280b482875d1f9128ce596e59031a226a8b84bec88cb2bf76c289f5d0de", - "sha256:53b69e79d00f78c81eecfb38f4516080dc7f36a198b6b37b928f1c13b3c063e9", - "sha256:55ccb7db5a665079d68b5c7c86359ebd5ebf31a19bc1a91c982fd622f1e31ff2", - "sha256:5a1ebbae7e2214f51b1f23b57bf98eeed2cf1ba84e4d523c48c36d5b2f8829ff", - "sha256:61b7199cd2a55e62e45bfb629a35b71fc2c0cb88f686a047f25b1112d3810904", - "sha256:660fc6b9c2a9ea3bb2a7e64ba878c98339abaf1811edca904ac85e9e662f1d73", - "sha256:6d140bdeb26cad8b93c1455fa00573c05592793c32053d6e0016ce05ba267549", - "sha256:6e490fa5f7f5326222cb9f0b78f207a2b218a14edf39602e083d5f617354306f", - "sha256:6ecf21d20d02d1733e9c820fb5c114c749d888704a7ec824b545c12e78734d1c", - "sha256:70c83bb530572917be20c21f3b6be92cd86b9aecb44b0c18b1d3b2cc3ae47df0", - "sha256:72153a0d2e425f45b884540a61c6639436ddafa1829a42056aa5764b84108b8e", - "sha256:73e14acd3d4247169955fae8fb103a2b900cfad21d0c35f0dcd0fdd54cd60367", - "sha256:76eaaba891083fcbe167aa0f03363311a9f12da975b025d30e94b93ac7a765fc", - "sha256:79ae0dc785504cb1e1788758c588c711f4e4a0195d70dff53db203c95a0bd303", - "sha256:7d142bcd604166417929b071cd396aa13c565749a4c840d6c702727a59d835eb", - "sha256:8c9554ca8e26241dabe7951aa1fa03a1ba0856688ecd7e7bdbdd286ebc272e4c", - "sha256:8d488fbdbf04283f0d20742b64968d44825617aa6717b07c006168ed16488804", - "sha256:91422ba785a8e7a18725b1dc40fbd88f08a5bb4c7f1b3e8739cab24b04fa8a03", - "sha256:9a66f4d2a005bc78e61d805ed95dedfcb35efa84b7bba0403c6d60d13a3de2d6", - "sha256:9b106bc52e7f28170e624ba61cc7dc6829566e535a6ec68528f8e1afbed1c41f", - "sha256:9b54577032d4f235452f77a83169b6527bf4b77d73aeada97d45b2aaf1bf5ce0", - "sha256:a09506eb48fa5493c58f946c46754ef22f3ec0df64f2b5149373ff31fb67f3dd", - "sha256:a212e5dea1a4182e40cd3e4067ee46be9d10418092ce3627475e995cca95de21", - "sha256:a731ac5cffc34dac62053e0da90f0c0b8560396a19f69d9703e88240c8f05858", - "sha256:af5ef6cfaf0d023c00002ba25d0751e5995fa0e4c9eec6cd263c30352662cbce", - "sha256:b58b855d0071575ea9c7bc0d84a06d2edfbfccec52e9657864386381a7ce1ae9", - "sha256:bc808924470643b82b14fe121923c30ec211d8c693e747eba8a7414bc4351a23", - "sha256:c557e94e91a983e5b1e9c60076a8fd79fea1e7e06848eb2e48d0ccfb30f6e073", - "sha256:c71be3f86d67d8d1311c6076a4ba3b75ba5703c0b856b4e691c9097f9b1e8bd2", - "sha256:c8754c75f55781515a3005063d9a05878b2cfb3cb7e41d5401ad0cf19de14872", - "sha256:cb0af13433dbbd1c806e671d81ec75bd324af6ef75171fd7815ca3074fe32bfe", - "sha256:cba6209c96828711cb7c8fcb45ecef8c8859238baf15119daa1bef0f6c84bfe7", - "sha256:cf77f8cf2a651fbd869fbdcb4a1931464189cd210abc4cfad357f1cacc8642a6", - "sha256:d7404cebcdb11bb5bd40bf94131faf7e9a7c10a6c60358580fe83913f360f929", - "sha256:dd1d3a8d1d2e50ad9b59e10aa7f07c7d1be2b367f3f2d33c5fade96ed5460962", - "sha256:e5d97c65ea7e097056f3d1ead77040ebc236feaf7f71489383d20f3b4c28412a", - "sha256:f1c3dc536b3ee124e8b24feb7533e5c70b9f2ef833e3b2e5513b2897fd46763a", - "sha256:f2212796593ad1d0235068c79836861f2201fc7137a99aa2fea7beeb3b101177", - "sha256:fead980fbc68512dfd4e0c7b1f5754c2a8e5015a04dea454b9cada54a8423525" - ], - "version": "==1.60.1" + "sha256:0b9179478b09ee22f4a36b40ca87ad43376acdccc816ce7c2193a9061bf35701", + "sha256:0d3dee701e48ee76b7d6fbbba18ba8bc142e5b231ef7d3d97065204702224e0e", + "sha256:0d7ae7fc7dbbf2d78d6323641ded767d9ec6d121aaf931ec4a5c50797b886532", + "sha256:0e97f37a3b7c89f9125b92d22e9c8323f4e76e7993ba7049b9f4ccbe8bae958a", + "sha256:136ffd79791b1eddda8d827b607a6285474ff8a1a5735c4947b58c481e5e4271", + "sha256:1bc8449084fe395575ed24809752e1dc4592bb70900a03ca42bf236ed5bf008f", + "sha256:1eda79574aec8ec4d00768dcb07daba60ed08ef32583b62b90bbf274b3c279f7", + "sha256:29cb592c4ce64a023712875368bcae13938c7f03e99f080407e20ffe0a9aa33b", + "sha256:2c1488b31a521fbba50ae86423f5306668d6f3a46d124f7819c603979fc538c4", + "sha256:2e84bfb2a734e4a234b116be208d6f0214e68dcf7804306f97962f93c22a1839", + "sha256:2f3d9a4d0abb57e5f49ed5039d3ed375826c2635751ab89dcc25932ff683bbb6", + "sha256:36df33080cd7897623feff57831eb83c98b84640b016ce443305977fac7566fb", + "sha256:38f69de9c28c1e7a8fd24e4af4264726637b72f27c2099eaea6e513e7142b47e", + "sha256:39cd45bd82a2e510e591ca2ddbe22352e8413378852ae814549c162cf3992a93", + "sha256:3fa15850a6aba230eed06b236287c50d65a98f05054a0f01ccedf8e1cc89d57f", + "sha256:4cd356211579043fce9f52acc861e519316fff93980a212c8109cca8f47366b6", + "sha256:56ca7ba0b51ed0de1646f1735154143dcbdf9ec2dbe8cc6645def299bb527ca1", + "sha256:5e709f7c8028ce0443bddc290fb9c967c1e0e9159ef7a030e8c21cac1feabd35", + "sha256:614c3ed234208e76991992342bab725f379cc81c7dd5035ee1de2f7e3f7a9842", + "sha256:62aa1659d8b6aad7329ede5d5b077e3d71bf488d85795db517118c390358d5f6", + "sha256:62ccb92f594d3d9fcd00064b149a0187c246b11e46ff1b7935191f169227f04c", + "sha256:662d3df5314ecde3184cf87ddd2c3a66095b3acbb2d57a8cada571747af03873", + "sha256:748496af9238ac78dcd98cce65421f1adce28c3979393e3609683fcd7f3880d7", + "sha256:77d48e5b1f8f4204889f1acf30bb57c30378e17c8d20df5acbe8029e985f735c", + "sha256:7a195531828b46ea9c4623c47e1dc45650fc7206f8a71825898dd4c9004b0928", + "sha256:7e1f51e2a460b7394670fdb615e26d31d3260015154ea4f1501a45047abe06c9", + "sha256:7eea57444a354ee217fda23f4b479a4cdfea35fb918ca0d8a0e73c271e52c09c", + "sha256:7f9d6c3223914abb51ac564dc9c3782d23ca445d2864321b9059d62d47144021", + "sha256:81531632f93fece32b2762247c4c169021177e58e725494f9a746ca62c83acaa", + "sha256:81d444e5e182be4c7856cd33a610154fe9ea1726bd071d07e7ba13fafd202e38", + "sha256:821a44bd63d0f04e33cf4ddf33c14cae176346486b0df08b41a6132b976de5fc", + "sha256:88f41f33da3840b4a9bbec68079096d4caf629e2c6ed3a72112159d570d98ebe", + "sha256:8aab8f90b2a41208c0a071ec39a6e5dbba16fd827455aaa070fec241624ccef8", + "sha256:921148f57c2e4b076af59a815467d399b7447f6e0ee10ef6d2601eb1e9c7f402", + "sha256:92cdb616be44c8ac23a57cce0243af0137a10aa82234f23cd46e69e115071388", + "sha256:95370c71b8c9062f9ea033a0867c4c73d6f0ff35113ebd2618171ec1f1e903e0", + "sha256:98d8f4eb91f1ce0735bf0b67c3b2a4fea68b52b2fd13dc4318583181f9219b4b", + "sha256:a33f2bfd8a58a02aab93f94f6c61279be0f48f99fcca20ebaee67576cd57307b", + "sha256:ab140a3542bbcea37162bdfc12ce0d47a3cda3f2d91b752a124cc9fe6776a9e2", + "sha256:b3d3d755cfa331d6090e13aac276d4a3fb828bf935449dc16c3d554bf366136b", + "sha256:b71c65427bf0ec6a8b48c68c17356cb9fbfc96b1130d20a07cb462f4e4dcdcd5", + "sha256:b7a6be562dd18e5d5bec146ae9537f20ae1253beb971c0164f1e8a2f5a27e829", + "sha256:bcff647e7fe25495e7719f779cc219bbb90b9e79fbd1ce5bda6aae2567f469f2", + "sha256:c912688acc05e4ff012c8891803659d6a8a8b5106f0f66e0aed3fb7e77898fa6", + "sha256:ce1aafdf8d3f58cb67664f42a617af0e34555fe955450d42c19e4a6ad41c84bd", + "sha256:d6a56ba703be6b6267bf19423d888600c3f574ac7c2cc5e6220af90662a4d6b0", + "sha256:e803e9b58d8f9b4ff0ea991611a8d51b31c68d2e24572cd1fe85e99e8cc1b4f8", + "sha256:eef1d16ac26c5325e7d39f5452ea98d6988c700c427c52cbc7ce3201e6d93334", + "sha256:f359d635ee9428f0294bea062bb60c478a8ddc44b0b6f8e1f42997e5dc12e2ee", + "sha256:f4c04fe33039b35b97c02d2901a164bbbb2f21fb9c4e2a45a959f0b044c3512c", + "sha256:f897b16190b46bc4d4aaf0a32a4b819d559a37a756d7c6b571e9562c360eed72", + "sha256:fbe0c20ce9a1cff75cfb828b21f08d0a1ca527b67f2443174af6626798a754a4", + "sha256:fc2836cb829895ee190813446dce63df67e6ed7b9bf76060262c55fcd097d270", + "sha256:fcc98cff4084467839d0a20d16abc2a76005f3d1b38062464d088c07f500d170" + ], + "version": "==1.62.0" }, "grpcio-status": { "hashes": [ @@ -1059,7 +1059,7 @@ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.2" }, "pytz": { @@ -1210,7 +1210,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "sortedcontainers": { @@ -1514,7 +1514,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "87de72a17508dad87d081dab51038321ae6b247b" + "ref": "9f3c8292be7400cef9255cdf6283f9bb7edea955" }, "codeforlife-portal": { "hashes": [ @@ -1528,61 +1528,61 @@ "toml" ], "hashes": [ - "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61", - "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1", - "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7", - "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7", - "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75", - "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd", - "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35", - "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04", - "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6", - "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042", - "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166", - "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1", - "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d", - "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c", - "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66", - "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70", - "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1", - "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676", - "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630", - "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a", - "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74", - "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad", - "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19", - "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6", - "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448", - "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018", - "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218", - "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756", - "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54", - "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45", - "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628", - "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968", - "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d", - "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25", - "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60", - "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950", - "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06", - "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295", - "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b", - "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c", - "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc", - "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74", - "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1", - "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee", - "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011", - "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156", - "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766", - "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5", - "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581", - "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016", - "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c", - "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3" + "sha256:006d220ba2e1a45f1de083d5022d4955abb0aedd78904cd5a779b955b019ec73", + "sha256:06fe398145a2e91edaf1ab4eee66149c6776c6b25b136f4a86fcbbb09512fd10", + "sha256:175f56572f25e1e1201d2b3e07b71ca4d201bf0b9cb8fad3f1dfae6a4188de86", + "sha256:18cac867950943fe93d6cd56a67eb7dcd2d4a781a40f4c1e25d6f1ed98721a55", + "sha256:1a5ee18e3a8d766075ce9314ed1cb695414bae67df6a4b0805f5137d93d6f1cb", + "sha256:20a875bfd8c282985c4720c32aa05056f77a68e6d8bbc5fe8632c5860ee0b49b", + "sha256:2412e98e70f16243be41d20836abd5f3f32edef07cbf8f407f1b6e1ceae783ac", + "sha256:2599972b21911111114100d362aea9e70a88b258400672626efa2b9e2179609c", + "sha256:2ed37e16cf35c8d6e0b430254574b8edd242a367a1b1531bd1adc99c6a5e00fe", + "sha256:32b4ab7e6c924f945cbae5392832e93e4ceb81483fd6dc4aa8fb1a97b9d3e0e1", + "sha256:34423abbaad70fea9d0164add189eabaea679068ebdf693baa5c02d03e7db244", + "sha256:3507427d83fa961cbd73f11140f4a5ce84208d31756f7238d6257b2d3d868405", + "sha256:3733545eb294e5ad274abe131d1e7e7de4ba17a144505c12feca48803fea5f64", + "sha256:3ff5bdb08d8938d336ce4088ca1a1e4b6c8cd3bef8bb3a4c0eb2f37406e49643", + "sha256:3ff7f92ae5a456101ca8f48387fd3c56eb96353588e686286f50633a611afc95", + "sha256:42a9e754aa250fe61f0f99986399cec086d7e7a01dd82fd863a20af34cbce962", + "sha256:51593a1f05c39332f623d64d910445fdec3d2ac2d96b37ce7f331882d5678ddf", + "sha256:5b11f9c6587668e495cc7365f85c93bed34c3a81f9f08b0920b87a89acc13469", + "sha256:69f1665165ba2fe7614e2f0c1aed71e14d83510bf67e2ee13df467d1c08bf1e8", + "sha256:78cdcbf7b9cb83fe047ee09298e25b1cd1636824067166dc97ad0543b079d22f", + "sha256:7df95fdd1432a5d2675ce630fef5f239939e2b3610fe2f2b5bf21fa505256fa3", + "sha256:81a5fb41b0d24447a47543b749adc34d45a2cf77b48ca74e5bf3de60a7bd9edc", + "sha256:840456cb1067dc350af9080298c7c2cfdddcedc1cb1e0b30dceecdaf7be1a2d3", + "sha256:8562ca91e8c40864942615b1d0b12289d3e745e6b2da901d133f52f2d510a1e3", + "sha256:861d75402269ffda0b33af94694b8e0703563116b04c681b1832903fac8fd647", + "sha256:8b98c89db1b150d851a7840142d60d01d07677a18f0f46836e691c38134ed18b", + "sha256:a178b7b1ac0f1530bb28d2e51f88c0bab3e5949835851a60dda80bff6052510c", + "sha256:a8ddbd158e069dded57738ea69b9744525181e99974c899b39f75b2b29a624e2", + "sha256:ac4bab32f396b03ebecfcf2971668da9275b3bb5f81b3b6ba96622f4ef3f6e17", + "sha256:ac9e95cefcf044c98d4e2c829cd0669918585755dd9a92e28a1a7012322d0a95", + "sha256:adbdfcda2469d188d79771d5696dc54fab98a16d2ef7e0875013b5f56a251047", + "sha256:b3c8bbb95a699c80a167478478efe5e09ad31680931ec280bf2087905e3b95ec", + "sha256:b3f2b1eb229f23c82898eedfc3296137cf1f16bb145ceab3edfd17cbde273fb7", + "sha256:b4ae777bebaed89e3a7e80c4a03fac434a98a8abb5251b2a957d38fe3fd30088", + "sha256:b953275d4edfab6cc0ed7139fa773dfb89e81fee1569a932f6020ce7c6da0e8f", + "sha256:bf54c3e089179d9d23900e3efc86d46e4431188d9a657f345410eecdd0151f50", + "sha256:bf711d517e21fb5bc429f5c4308fbc430a8585ff2a43e88540264ae87871e36a", + "sha256:c00e54f0bd258ab25e7f731ca1d5144b0bf7bec0051abccd2bdcff65fa3262c9", + "sha256:c11ca2df2206a4e3e4c4567f52594637392ed05d7c7fb73b4ea1c658ba560265", + "sha256:c5f9683be6a5b19cd776ee4e2f2ffb411424819c69afab6b2db3a0a364ec6642", + "sha256:cf89ab85027427d351f1de918aff4b43f4eb5f33aff6835ed30322a86ac29c9e", + "sha256:d1b750a8409bec61caa7824bfd64a8074b6d2d420433f64c161a8335796c7c6b", + "sha256:d779a48fac416387dd5673fc5b2d6bd903ed903faaa3247dc1865c65eaa5a93e", + "sha256:d9a1ef0f173e1a19738f154fb3644f90d0ada56fe6c9b422f992b04266c55d5a", + "sha256:ddb79414c15c6f03f56cc68fa06994f047cf20207c31b5dad3f6bab54a0f66ef", + "sha256:ef00d31b7569ed3cb2036f26565f1984b9fc08541731ce01012b02a4c238bf03", + "sha256:f40ac873045db4fd98a6f40387d242bde2708a3f8167bd967ccd43ad46394ba2", + "sha256:f593a4a90118d99014517c2679e04a4ef5aee2d81aa05c26c734d271065efcb6", + "sha256:f5df76c58977bc35a49515b2fbba84a1d952ff0ec784a4070334dfbec28a2def", + "sha256:f72cdd2586f9a769570d4b5714a3837b3a59a53b096bb954f1811f6a0afad305", + "sha256:f8e845d894e39fb53834da826078f6dc1a933b32b1478cf437007367efaf6f6a", + "sha256:fe6e43c8b510719b48af7db9631b5fbac910ade4bd90e6378c85ac5ac706382c" ], "markers": "python_version >= '3.8'", - "version": "==7.4.1" + "version": "==7.4.2" }, "defusedxml": { "hashes": [ @@ -2495,7 +2495,7 @@ "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5", "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.10'", "version": "==7.2.1" }, "pytest-cov": { @@ -2554,7 +2554,7 @@ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.2" }, "pytz": { @@ -2730,7 +2730,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "snapshottest": { @@ -2917,4 +2917,4 @@ "version": "==3.17.0" } } -} \ No newline at end of file +}