Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create class #262

Merged
merged 11 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.12.4", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"}
codeforlife = {ref = "v0.12.8", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"}
django = "==3.2.23"
djangorestframework = "==3.13.1"
django-cors-headers = "==4.1.0"
Expand Down
10 changes: 3 additions & 7 deletions backend/Pipfile.lock

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

94 changes: 92 additions & 2 deletions backend/api/serializers/klass.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,100 @@
Created on 24/01/2024 at 12:14:21(+00:00).
"""

import string

from codeforlife.user.models import Class, Teacher
from codeforlife.user.serializers import ClassSerializer as _ClassSerializer
from django.utils.crypto import get_random_string
from rest_framework import serializers


# pylint: disable-next=missing-class-docstring
# pylint: disable-next=missing-class-docstring,too-many-ancestors
class ClassSerializer(_ClassSerializer):
teacher = serializers.IntegerField(
source="teacher.id",
required=False,
)

read_classmates_data = serializers.BooleanField(
source="classmates_data_viewable",
)

receive_requests_until = serializers.DateTimeField(
source="accept_requests_until",
required=False,
)

class Meta(_ClassSerializer.Meta):
pass
extra_kwargs = {
**_ClassSerializer.Meta.extra_kwargs,
"name": {"read_only": False},
}

# pylint: disable-next=missing-function-docstring
def validate_teacher(self, value: int):
queryset = Teacher.objects.filter(id=value)
if not queryset.exists():
raise serializers.ValidationError(
"This teacher does not exist.",
code="does_not_exist",
)

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.",
code="not_in_school",
)
if value != user.teacher.id and not user.teacher.is_admin:
raise serializers.ValidationError(
"Cannot assign another teacher if you're not admin.",
code="not_admin",
)

return value

# TODO: set unique_together=("name", "school") for in new Class model.
# 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,
name=value,
).exists():
raise serializers.ValidationError(
"Name already taken.",
code="name_not_unique",
)

return value

def create(self, validated_data):
# TODO: move generation logic to new Class model.
access_code = None
while (
access_code is None
or Class.objects.filter(access_code=access_code).exists()
):
access_code = get_random_string(
length=5,
allowed_chars=string.ascii_uppercase,
)

# TODO: set school to teacher's school on new Class model.
return super().create(
{
"access_code": access_code,
"name": validated_data["name"],
"teacher_id": (
validated_data["teacher"]["id"]
if "teacher" in validated_data
else self.request_school_teacher_user.teacher.id
),
"classmates_data_viewable": validated_data[
"classmates_data_viewable"
],
"accept_requests_until": validated_data.get(
"accept_requests_until"
),
}
)
1 change: 0 additions & 1 deletion backend/api/serializers/school.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,6 @@ class SchoolSerializer(_SchoolSerializer):

uk_county = serializers.ChoiceField( # type: ignore[assignment]
source="county",
default="",
choices=[
"Aberdeen City",
"Aberdeenshire",
Expand Down
123 changes: 123 additions & 0 deletions backend/api/tests/serializers/test_klass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""
© Ocado Group
Created on 05/02/2024 at 15:31:59(+00:00).
"""

from codeforlife.tests import ModelSerializerTestCase
from codeforlife.user.models import Class, SchoolTeacherUser, Teacher

from ...serializers import ClassSerializer


# pylint: disable-next=missing-class-docstring
class ClassSerializerTestCase(ModelSerializerTestCase[Class]):
model_serializer_class = ClassSerializer
fixtures = ["school_1"]

def setUp(self):
self.school_teacher_user = SchoolTeacherUser.objects.get(
email="[email protected]"
)
self.class_1 = Class.objects.get(name="Class 1 @ School 1")

def test_validate_teacher__does_not_exist(self):
"""
Teacher must exist.
"""

self.assert_validate_field(
name="teacher",
value=-1,
error_code="does_not_exist",
)

def test_validate_teacher__not_in_school(self):
"""
Teacher must be in school.
"""

teacher = Teacher.objects.exclude(
school=self.school_teacher_user.teacher.school
).first()
assert teacher

self.assert_validate_field(
name="teacher",
value=teacher.id,
error_code="not_in_school",
context={
"request": self.init_request("POST", self.school_teacher_user)
},
)

def test_validate_teacher__not_admin(self):
"""
Teacher cannot assign another teacher if they're not an admin.
"""

assert not self.school_teacher_user.teacher.is_admin

teacher = (
Teacher.objects.filter(
school=self.school_teacher_user.teacher.school
)
.exclude(pk=self.school_teacher_user.teacher.pk)
.first()
)
assert teacher

self.assert_validate_field(
name="teacher",
value=teacher.id,
error_code="not_admin",
context={
"request": self.init_request("POST", self.school_teacher_user)
},
)

def test_validate_name__name_not_unique(self):
"""
Class names must be unique per school.
"""

self.assert_validate_field(
name="name",
value=self.class_1.name,
error_code="name_not_unique",
context={
"request": self.init_request("POST", self.school_teacher_user)
},
)

def test_create__teacher(self):
"""
Can successfully create with setting the teacher field.
"""

self.assert_create(
{
"name": "ExampleClass",
"teacher": {
"id": self.school_teacher_user.teacher.id,
},
"classmates_data_viewable": False,
}
)

def test_create__no_teacher(self):
"""
Can successfully create without setting the teacher field.
"""

self.assert_create(
{
"name": "ExampleClass",
"classmates_data_viewable": False,
},
new_data={
"teacher": self.school_teacher_user.teacher.id,
},
context={
"request": self.init_request("POST", self.school_teacher_user),
},
)
8 changes: 4 additions & 4 deletions backend/api/tests/serializers/test_student.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def test_validate_klass__teacher_not_in_school(self):
name="klass",
value="",
error_code="teacher_not_in_school",
user=user,
context={"request": self.init_request("POST", user)},
)

def test_validate_klass__does_not_exist(self):
Expand All @@ -47,7 +47,7 @@ def test_validate_klass__does_not_exist(self):
name="klass",
value="",
error_code="does_not_exist",
user=user,
context={"request": self.init_request("POST", user)},
)

def test_validate_klass__teacher_not_in_same_school(self):
Expand All @@ -68,7 +68,7 @@ def test_validate_klass__teacher_not_in_same_school(self):
name="klass",
value=klass.access_code,
error_code="teacher_not_in_same_school",
user=user,
context={"request": self.init_request("POST", user)},
)

def test_validate_klass__teacher_not_admin_or_class_owner(self):
Expand All @@ -93,5 +93,5 @@ def test_validate_klass__teacher_not_admin_or_class_owner(self):
name="klass",
value=klass.access_code,
error_code="teacher_not_admin_or_class_owner",
user=user,
context={"request": self.init_request("POST", user)},
)
Loading
Loading