Skip to content

Commit

Permalink
feat: bulk create students
Browse files Browse the repository at this point in the history
  • Loading branch information
SKairinos committed Jan 29, 2024
1 parent 58f0d58 commit 83ec7b8
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 69 deletions.
2 changes: 1 addition & 1 deletion backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
126 changes: 61 additions & 65 deletions backend/Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/api/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
36 changes: 36 additions & 0 deletions backend/api/serializers/student.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions backend/api/serializers/teacher.py
Original file line number Diff line number Diff line change
@@ -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
97 changes: 96 additions & 1 deletion backend/api/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit 83ec7b8

Please sign in to comment.