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

Portal frontend 45 #360

Merged
merged 5 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ name = "pypi"
# 5. Run `pipenv install --dev` in your terminal.

[packages]
codeforlife = {ref = "v0.18.4", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"}
codeforlife = {ref = "v0.18.6", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"}
# 🚫 Don't add [packages] below that are inherited from the CFL package.
pyjwt = "==2.6.0" # TODO: upgrade to latest version
# TODO: Needed by RR. Remove when RR has moved to new system.
Expand All @@ -32,7 +32,7 @@ django-sekizai = "==2.0.0"
django-classy-tags = "==2.0.0"

[dev-packages]
codeforlife = {ref = "v0.18.4", git = "https://github.com/ocadotechnology/codeforlife-package-python.git", extras = ["dev"]}
codeforlife = {ref = "v0.18.6", git = "https://github.com/ocadotechnology/codeforlife-package-python.git", extras = ["dev"]}
# codeforlife = {file = "../codeforlife-package-python", editable = true, extras = ["dev"]}
# 🚫 Don't add [dev-packages] below that are inherited from the CFL package.

Expand Down
982 changes: 335 additions & 647 deletions Pipfile.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/api/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""

from .auth_factor import AuthFactorSerializer
from .klass import ClassSerializer
from .klass import ReadClassSerializer, WriteClassSerializer
from .school import SchoolSerializer
from .school_teacher_invitation import (
AcceptSchoolTeacherInvitationSerializer,
Expand All @@ -25,6 +25,7 @@
from .user import (
CreateUserSerializer,
HandleIndependentUserJoinClassRequestSerializer,
ReadUserSerializer,
RegisterEmailToNewsletter,
RequestUserPasswordResetSerializer,
ResetUserPasswordSerializer,
Expand Down
29 changes: 25 additions & 4 deletions src/api/serializers/klass.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@
import string

from codeforlife.serializers import ModelListSerializer
from codeforlife.user.models import Class, Teacher, User
from codeforlife.user.models import (
Class,
SchoolTeacher,
SchoolTeacherUser,
Teacher,
User,
)
from codeforlife.user.serializers import BaseUserSerializer
from codeforlife.user.serializers import ClassSerializer as _ClassSerializer
from codeforlife.user.serializers import TeacherSerializer as _TeacherSerializer
from django.utils.crypto import get_random_string
from rest_framework import serializers

Expand All @@ -16,7 +24,7 @@
# pylint: disable=too-many-ancestors


class ClassListSerializer(ModelListSerializer[User, Class]):
class WriteClassListSerializer(ModelListSerializer[User, Class]):
def update(self, instance, validated_data):
for klass, data in zip(instance, validated_data):
klass.name = data.get("name", klass.name)
Expand All @@ -26,7 +34,7 @@ def update(self, instance, validated_data):
return instance


class ClassSerializer(_ClassSerializer):
class WriteClassSerializer(_ClassSerializer):
read_classmates_data = serializers.BooleanField(
source="classmates_data_viewable",
)
Expand All @@ -42,7 +50,7 @@ class Meta(_ClassSerializer.Meta):
"name": {"read_only": False},
"teacher": {"required": False},
}
list_serializer_class = ClassListSerializer
list_serializer_class = WriteClassListSerializer

def validate_teacher(self, value: Teacher):
user = self.request.school_teacher_user
Expand Down Expand Up @@ -97,3 +105,16 @@ def create(self, validated_data):
),
}
)


class ReadClassSerializer(_ClassSerializer):
class TeacherSerializer(_TeacherSerializer[SchoolTeacher]):
user = BaseUserSerializer[SchoolTeacherUser](
source="new_user",
read_only=True,
)

class Meta(_TeacherSerializer.Meta):
fields = [*_TeacherSerializer.Meta.fields, "user"]

teacher = TeacherSerializer(read_only=True)
6 changes: 3 additions & 3 deletions src/api/serializers/klass_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
User,
)

from .klass import ClassSerializer
from .klass import WriteClassSerializer

# pylint: disable=missing-class-docstring


class TestClassSerializer(ModelSerializerTestCase[User, Class]):
model_serializer_class = ClassSerializer
class TestWriteClassSerializer(ModelSerializerTestCase[User, Class]):
model_serializer_class = WriteClassSerializer
fixtures = ["school_1", "school_2"]

def setUp(self):
Expand Down
40 changes: 40 additions & 0 deletions src/api/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@
Class,
ContactableUser,
IndependentUser,
SchoolTeacher,
Student,
StudentUser,
Teacher,
TeacherUser,
User,
)
from codeforlife.user.serializers import (
BaseUserSerializer as _BaseUserSerializer,
)
from codeforlife.user.serializers import ClassSerializer as _ClassSerializer
from codeforlife.user.serializers import TeacherSerializer as _TeacherSerializer
from codeforlife.user.serializers import UserSerializer as _UserSerializer
from django.conf import settings
from django.contrib.auth.password_validation import (
Expand Down Expand Up @@ -435,3 +439,39 @@
user.add_contact_to_dot_digital()

return user


class ReadUserSerializer(_UserSerializer[User]):
class ClassSerializer(_ClassSerializer):
class TeacherSerializer(_TeacherSerializer[SchoolTeacher]):
user = _BaseUserSerializer[TeacherUser](
source="new_user", read_only=True
)

class Meta(_TeacherSerializer.Meta):
fields = [*_TeacherSerializer.Meta.fields, "user"]

teacher = TeacherSerializer(read_only=True)

requesting_to_join_class = ClassSerializer( # type: ignore[assignment]
source="new_student.pending_class_request",
read_only=True,
)

def to_representation(self, instance):
representation = super().to_representation(instance)

Check warning on line 462 in src/api/serializers/user.py

View check run for this annotation

Codecov / codecov/patch

src/api/serializers/user.py#L462

Added line #L462 was not covered by tests

try:
if (

Check warning on line 465 in src/api/serializers/user.py

View check run for this annotation

Codecov / codecov/patch

src/api/serializers/user.py#L464-L465

Added lines #L464 - L465 were not covered by tests
instance.new_student
and instance.new_student.pending_class_request
):
representation[

Check warning on line 469 in src/api/serializers/user.py

View check run for this annotation

Codecov / codecov/patch

src/api/serializers/user.py#L469

Added line #L469 was not covered by tests
"requesting_to_join_class"
] = self.ClassSerializer(
instance.new_student.pending_class_request
).data
except Student.DoesNotExist:
pass

Check warning on line 475 in src/api/serializers/user.py

View check run for this annotation

Codecov / codecov/patch

src/api/serializers/user.py#L474-L475

Added lines #L474 - L475 were not covered by tests

return representation

Check warning on line 477 in src/api/serializers/user.py

View check run for this annotation

Codecov / codecov/patch

src/api/serializers/user.py#L477

Added line #L477 was not covered by tests
11 changes: 8 additions & 3 deletions src/api/views/klass.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@
from rest_framework import status
from rest_framework.response import Response

from ..serializers import ClassSerializer
from ..serializers import ReadClassSerializer, WriteClassSerializer


# pylint: disable-next=missing-class-docstring,too-many-ancestors
class ClassViewSet(_ClassViewSet):
http_method_names = ["get", "post", "patch", "delete"]
serializer_class = ClassSerializer

def get_permissions(self):
# Bulk actions not allowed for classes.
# Only bulk-partial-update allowed for classes.
if self.action == "bulk":
return (
[OR(IsTeacher(is_admin=True), IsTeacher(in_class=True))]
Expand All @@ -31,6 +30,12 @@ def get_permissions(self):

return super().get_permissions()

def get_serializer_class(self):
if self.action in ["create", "partial_update", "bulk"]:
return WriteClassSerializer

return ReadClassSerializer

def destroy(self, request, *args, **kwargs):
klass = self.get_object()

Expand Down
31 changes: 31 additions & 0 deletions src/api/views/klass_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.utils import timezone
from rest_framework import status

from ..serializers import ReadClassSerializer, WriteClassSerializer
from .klass import ClassViewSet


Expand Down Expand Up @@ -103,6 +104,36 @@ def test_get_permissions__retrieve(self):
action="retrieve",
)

# test: get serializer class

def test_get_serializer_class__create(self):
"""Creating a class has a dedicated serializer."""
self.assert_get_serializer_class(
serializer_class=WriteClassSerializer,
action="create",
)

def test_get_serializer_class__partial_update(self):
"""Partially updating a class has a dedicated serializer."""
self.assert_get_serializer_class(
serializer_class=WriteClassSerializer,
action="partial_update",
)

def test_get_serializer_class__retrieve(self):
"""Retrieving a class has a dedicated serializer."""
self.assert_get_serializer_class(
serializer_class=ReadClassSerializer,
action="retrieve",
)

def test_get_serializer_class__list(self):
"""Listing classes has a dedicated serializer."""
self.assert_get_serializer_class(
serializer_class=ReadClassSerializer,
action="list",
)

# test: default actions

def test_create__self(self):
Expand Down
3 changes: 2 additions & 1 deletion src/api/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from ..serializers import (
CreateUserSerializer,
HandleIndependentUserJoinClassRequestSerializer,
ReadUserSerializer,
RegisterEmailToNewsletter,
RequestUserPasswordResetSerializer,
ResetUserPasswordSerializer,
Expand Down Expand Up @@ -96,7 +97,7 @@ def get_serializer_class(self):
if self.action == "register_to_newsletter":
return RegisterEmailToNewsletter

return super().get_serializer_class()
return ReadUserSerializer

def get_queryset(self, user_class=User):
if self.action in ["reset_password", "verify_email_address"]:
Expand Down
6 changes: 3 additions & 3 deletions src/api/views/user_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
UserProfile,
)
from codeforlife.user.permissions import IsIndependent, IsTeacher
from codeforlife.user.serializers import UserSerializer
from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.contrib.auth.tokens import (
Expand All @@ -45,6 +44,7 @@
from ..serializers import (
CreateUserSerializer,
HandleIndependentUserJoinClassRequestSerializer,
ReadUserSerializer,
RegisterEmailToNewsletter,
RequestUserPasswordResetSerializer,
ResetUserPasswordSerializer,
Expand Down Expand Up @@ -303,14 +303,14 @@ def test_get_serializer_class__partial_update(self):
def test_get_serializer_class__retrieve(self):
"""Retrieving a user uses the general serializer."""
self.assert_get_serializer_class(
serializer_class=UserSerializer,
serializer_class=ReadUserSerializer,
action="retrieve",
)

def test_get_serializer_class__list(self):
"""Listing users uses the general serializer."""
self.assert_get_serializer_class(
serializer_class=UserSerializer,
serializer_class=ReadUserSerializer,
action="list",
)

Expand Down
20 changes: 0 additions & 20 deletions src/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,8 @@
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""

from aimmo import urls as aimmo_urls # type: ignore[import-untyped]
from codeforlife.urls import get_urlpatterns
from django.urls import include, path
from portal.views.aimmo.dashboard import ( # type: ignore[import-untyped]
StudentAimmoDashboard,
TeacherAimmoDashboard,
)

from .api.urls import urlpatterns

Expand All @@ -30,21 +25,6 @@
include("game.urls"),
name="rapidrouter",
),
path(
"teach/kurono/dashboard/",
TeacherAimmoDashboard.as_view(),
name="teacher_aimmo_dashboard",
),
path(
"play/kurono/dashboard/",
StudentAimmoDashboard.as_view(),
name="student_aimmo_dashboard",
),
path(
"kurono/",
include(aimmo_urls),
name="kurono",
),
*get_urlpatterns(urlpatterns, include_user_urls=False),
path(
"api/sso/",
Expand Down