From 83ec7b87cf7e1af9c74bd51d27fbe143807668f2 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Mon, 29 Jan 2024 21:36:27 +0000 Subject: [PATCH] feat: bulk create students --- backend/Pipfile | 2 +- backend/Pipfile.lock | 126 +++++++++++++-------------- backend/api/serializers/__init__.py | 2 + backend/api/serializers/student.py | 36 ++++++++ backend/api/serializers/teacher.py | 12 +++ backend/api/serializers/user.py | 97 ++++++++++++++++++++- backend/api/signals/user.py | 4 +- backend/api/tests/views/test_user.py | 35 +++++++- backend/api/views/user.py | 7 ++ 9 files changed, 252 insertions(+), 69 deletions(-) create mode 100644 backend/api/serializers/student.py create mode 100644 backend/api/serializers/teacher.py diff --git a/backend/Pipfile b/backend/Pipfile index dbec7652..ef07cdb4 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -28,7 +28,7 @@ google-cloud-logging = "==1.*" google-auth = "==2.*" google-cloud-container = "==2.3.0" # "django-anymail[amazon_ses]" = "==7.0.*" -codeforlife = {ref = "v0.9.6", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} +codeforlife = {ref = "bulk_create_students_2", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} django = "==3.2.20" djangorestframework = "==3.13.1" django-cors-headers = "==4.1.0" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index f1995e4d..a7f0f036 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b5d163672ae6fa1c185023cee8a31e22991c62a2b782442b60f4cc3036cc93e6" + "sha256": "12c5f59ecfb5535ad19732c5f29b8014c05caa54216f0c613702ab2a597513e9" }, "pipfile-spec": 6, "requires": { @@ -170,7 +170,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "db3cfe8fd8d889a705d3c66a4183745389fca7a3" + "ref": "dfdd3338232a3b4c7ea37272d7de064b0e085bdb" }, "codeforlife-portal": { "hashes": [ @@ -816,7 +816,6 @@ "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" ], - "markers": "python_version >= '3.6'", "version": "==3.1.2" }, "pandas": { @@ -1070,7 +1069,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": { @@ -1222,7 +1221,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": { @@ -1317,7 +1316,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": { @@ -1499,61 +1497,61 @@ "toml" ], "hashes": [ - "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca", - "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471", - "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a", - "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058", - "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85", - "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143", - "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446", - "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590", - "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a", - "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105", - "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9", - "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a", - "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac", - "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25", - "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2", - "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450", - "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932", - "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba", - "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137", - "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae", - "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614", - "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70", - "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e", - "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505", - "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870", - "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc", - "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451", - "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7", - "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e", - "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566", - "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5", - "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26", - "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2", - "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42", - "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555", - "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43", - "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed", - "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa", - "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516", - "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952", - "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd", - "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09", - "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c", - "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f", - "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6", - "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1", - "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0", - "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e", - "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9", - "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9", - "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e", - "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06" + "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" ], "markers": "python_version >= '3.8'", - "version": "==7.4.0" + "version": "==7.4.1" }, "defusedxml": { "hashes": [ @@ -1573,11 +1571,11 @@ }, "dill": { "hashes": [ - "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", - "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" + "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca", + "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7" ], "markers": "python_version < '3.11'", - "version": "==0.3.7" + "version": "==0.3.8" }, "django": { "hashes": [ @@ -1768,7 +1766,6 @@ "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" ], - "markers": "python_version >= '3.6'", "version": "==3.1.2" }, "packaging": { @@ -1982,7 +1979,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": { @@ -2122,7 +2119,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/serializers/__init__.py b/backend/api/serializers/__init__.py index 94753e37..2d829ea6 100644 --- a/backend/api/serializers/__init__.py +++ b/backend/api/serializers/__init__.py @@ -6,4 +6,6 @@ from .auth_factor import AuthFactorSerializer from .klass import ClassSerializer from .school import SchoolSerializer +from .student import StudentSerializer +from .teacher import TeacherSerializer from .user import UserSerializer diff --git a/backend/api/serializers/student.py b/backend/api/serializers/student.py new file mode 100644 index 00000000..f14f4e49 --- /dev/null +++ b/backend/api/serializers/student.py @@ -0,0 +1,36 @@ +""" +© Ocado Group +Created on 29/01/2024 at 10:14:59(+00:00). +""" + +from codeforlife.user.models import Class +from codeforlife.user.serializers import StudentSerializer as _StudentSerializer +from rest_framework import serializers + + +# pylint: disable-next=missing-class-docstring,too-many-ancestors +class StudentSerializer(_StudentSerializer): + klass = serializers.CharField(source="class_field.access_code") + + class Meta(_StudentSerializer.Meta): + pass + + # pylint: disable-next=missing-function-docstring + def validate_klass(self, value: str): + if self.request_user.teacher is None: + raise serializers.ValidationError( + "Only a teacher can assign a student to class." + ) + if self.request_user.teacher.school is None: + raise serializers.ValidationError( + "The requesting teacher must be in a school." + ) + if not Class.objects.filter( + access_code=value, + teacher__school=self.request_user.teacher.school_id, + ).exists(): + raise serializers.ValidationError( + "Class must belong to the same school as requesting teacher." + ) + + return value diff --git a/backend/api/serializers/teacher.py b/backend/api/serializers/teacher.py new file mode 100644 index 00000000..3cf51d79 --- /dev/null +++ b/backend/api/serializers/teacher.py @@ -0,0 +1,12 @@ +""" +© Ocado Group +Created on 29/01/2024 at 10:13:58(+00:00). +""" + +from codeforlife.user.serializers import TeacherSerializer as _TeacherSerializer + + +# pylint: disable-next=missing-class-docstring +class TeacherSerializer(_TeacherSerializer): + class Meta(_TeacherSerializer.Meta): + pass diff --git a/backend/api/serializers/user.py b/backend/api/serializers/user.py index 5cde120c..9895993d 100644 --- a/backend/api/serializers/user.py +++ b/backend/api/serializers/user.py @@ -3,16 +3,111 @@ Created on 18/01/2024 at 15:14:32(+00:00). """ +import string +import typing as t + +from codeforlife.serializers import ModelListSerializer +from codeforlife.user.models import Class, Student, User, UserProfile from codeforlife.user.serializers import UserSerializer as _UserSerializer +from django.contrib.auth.hashers import make_password +from django.utils.crypto import get_random_string from rest_framework import serializers +from .student import StudentSerializer +from .teacher import TeacherSerializer + # pylint: disable-next=missing-class-docstring +class UserListSerializer(ModelListSerializer[User]): + def create(self, validated_data): + classes = { + klass.access_code: klass + for klass in Class.objects.filter( + access_code__in={ + user_fields["new_student"]["class_field"]["access_code"] + for user_fields in 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( + first_name=user_fields["first_name"], + # last_name="", + # email="", + username=get_random_string(length=30), + password=make_password(password), + ) + users.append(user) + + # pylint: disable-next=protected-access + user._password = password + + # TODO: Is this needed? + 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[ + user_fields["new_student"]["class_field"]["access_code"] + ], + user=UserProfile.objects.create(user=user), + new_user=user, + login_id=login_id, + ) + + return users + + +# pylint: disable-next=missing-class-docstring,too-many-ancestors class UserSerializer(_UserSerializer): - current_password = serializers.CharField(write_only=True) + student = StudentSerializer(source="new_student", required=False) + + teacher = TeacherSerializer(source="new_teacher", required=False) + + current_password = serializers.CharField(write_only=True, required=False) class Meta(_UserSerializer.Meta): fields = [ *_UserSerializer.Meta.fields, + "password", "current_password", ] + extra_kwargs = { + **_UserSerializer.Meta.extra_kwargs, + "first_name": {"read_only": False}, + "password": {"write_only": True, "required": False}, + } + list_serializer_class = UserListSerializer + + def validate(self, attrs): + # TODO: make current password required when changing self-profile. + + return attrs + + def to_representation(self, instance: User): + representation = super().to_representation(instance) + + # Return student's auto-generated password. + if ( + representation["student"] is not None + and self.request_user.teacher is not None + ): + # pylint: disable-next=protected-access + password = instance._password + if password is not None: + representation["password"] = password + + return representation diff --git a/backend/api/signals/user.py b/backend/api/signals/user.py index 26578dee..7d3ba107 100644 --- a/backend/api/signals/user.py +++ b/backend/api/signals/user.py @@ -30,7 +30,9 @@ def user__pre_save__otp_secret(sender, instance: UserProfile, *args, **kwargs): def user__pre_save__email(sender, instance: User, *args, **kwargs): """Before a user's email field is updated.""" - if previous_values_are_unequal(instance, {"email"}): + if instance.teacher is not None and previous_values_are_unequal( + instance, {"email"} + ): instance.username = instance.email diff --git a/backend/api/tests/views/test_user.py b/backend/api/tests/views/test_user.py index f69a4608..f0bf8385 100644 --- a/backend/api/tests/views/test_user.py +++ b/backend/api/tests/views/test_user.py @@ -3,8 +3,10 @@ Created on 20/01/2024 at 10:58:52(+00:00). """ +import typing as t + from codeforlife.tests import ModelViewSetTestCase -from codeforlife.user.models import User +from codeforlife.user.models import Class, User from ...views import UserViewSet @@ -21,6 +23,13 @@ class TestUserViewSet(ModelViewSetTestCase[User]): basename = "user" model_view_set_class = UserViewSet + def _login_teacher(self): + return self.client.login_teacher( + email="maxplanck@codeforlife.com", + password="Password1", + is_admin=False, + ) + def test_is_unique_email(self): """ Check email is unique. @@ -41,3 +50,27 @@ def test_is_unique_email(self): ) self.assertTrue(response.json()) + + def test_bulk_create__students(self): + """ + Teacher can bulk create students. + """ + + user = self._login_teacher() + assert user.teacher.school is not None + + klass: t.Optional[Class] = user.teacher.class_teacher.first() + assert klass is not None + + self.client.bulk_create( + [ + { + "first_name": "Peter", + "student": {"klass": klass.access_code}, + }, + { + "first_name": "Mary", + "student": {"klass": klass.access_code}, + }, + ] + ) diff --git a/backend/api/views/user.py b/backend/api/views/user.py index 9cb6d706..813fd35d 100644 --- a/backend/api/views/user.py +++ b/backend/api/views/user.py @@ -6,6 +6,7 @@ import typing as t from codeforlife.user.models import User +from codeforlife.user.permissions import IsTeacher from codeforlife.user.views import UserViewSet as _UserViewSet from rest_framework.decorators import action from rest_framework.permissions import AllowAny @@ -19,6 +20,12 @@ class UserViewSet(_UserViewSet): http_method_names = ["get", "post", "patch", "delete"] serializer_class = UserSerializer + def get_permissions(self): + if self.action == "bulk": + return [IsTeacher()] + + return super().get_permissions() + @action(detail=False, methods=["post"], permission_classes=[AllowAny]) def is_unique_email(self, request): """Checks if an email is unique."""