Skip to content

Commit

Permalink
Invite teacher (#264)
Browse files Browse the repository at this point in the history
* Invite teacher endpoints and tests

* Merge branch 'development' into invite_teacher

* Merge branch 'development' into invite_teacher

* Some cleanup

* Merge development

* igy

* isort

* Run code check and tests on every push

* pair programming

* fix basename

* can successfully accept invite

* house keeping

* rename tests

* ignore protected access

* Tests

* Imports

* Conflicts

* Feedback

* Upgrade PP

* Removing PR trigger

* All branches

* Feedback pt.2

* Update docstring format

* Feedback again

* Feedback part 1 bajillion

* Remvove strict check

* Black

* Rename invitations

* Update PP

Co-Authored-By: SKairinos <[email protected]>
  • Loading branch information
faucomte97 and SKairinos authored Feb 14, 2024
1 parent 552fbfe commit 9939972
Show file tree
Hide file tree
Showing 18 changed files with 638 additions and 55 deletions.
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Main

on:
pull_request:
push:
paths-ignore:
- "**/*.md"
Expand Down
4 changes: 2 additions & 2 deletions backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -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.2", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"}
codeforlife = {ref = "v0.13.4", 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"
Expand All @@ -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.2", git = "https://github.com/ocadotechnology/codeforlife-package-python.git", extras = ["dev"]}
codeforlife = {ref = "v0.13.4", 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"
Expand Down
20 changes: 10 additions & 10 deletions backend/Pipfile.lock

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

44 changes: 43 additions & 1 deletion backend/api/fixtures/school_1.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,47 @@
"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": "[email protected]",
"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": "[email protected]",
"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": "[email protected]",
"invited_teacher_is_admin": false,
"expiry": "9999-02-09 20:26:08.298402+00:00"
}
}
]
]
6 changes: 6 additions & 0 deletions backend/api/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
© Ocado Group
Created on 06/02/2024 at 15:13:00(+00:00).
"""

from common.models import SchoolTeacherInvitation
1 change: 1 addition & 0 deletions backend/api/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .auth_factor import AuthFactorSerializer
from .klass import ClassSerializer
from .school import SchoolSerializer
from .school_teacher_invitation import SchoolTeacherInvitationSerializer
from .student import StudentSerializer
from .teacher import TeacherSerializer
from .user import UserSerializer
57 changes: 57 additions & 0 deletions backend/api/serializers/school_teacher_invitation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
© Ocado Group
Created on 09/02/2024 at 16:14:00(+00:00).
"""

from datetime import timedelta

from codeforlife.serializers import ModelSerializer
from django.contrib.auth.hashers import make_password
from django.utils import timezone
from django.utils.crypto import get_random_string
from rest_framework import serializers

from ..models import SchoolTeacherInvitation


# pylint: disable-next=missing-class-docstring,too-many-ancestors
class SchoolTeacherInvitationSerializer(
ModelSerializer[SchoolTeacherInvitation]
):
first_name = serializers.CharField(source="invited_teacher_first_name")
last_name = serializers.CharField(source="invited_teacher_last_name")
email = serializers.EmailField(source="invited_teacher_email")
is_admin = serializers.BooleanField(source="invited_teacher_is_admin")

class Meta:
model = SchoolTeacherInvitation
fields = [
"id",
"first_name",
"last_name",
"email",
"is_admin",
"expiry",
]
extra_kwargs = {
"id": {"read_only": True},
"expiry": {"read_only": True},
}

def create(self, validated_data):
user = self.request_admin_school_teacher_user

token = get_random_string(length=32)
validated_data["token"] = make_password(token)
validated_data["school"] = user.teacher.school
validated_data["from_teacher"] = user.teacher
validated_data["expiry"] = timezone.now() + timedelta(days=30)

invitation = super().create(validated_data)
invitation._token = token
return invitation

def update(self, instance, validated_data):
instance.expiry = timezone.now() + timedelta(days=30)
instance.save()
return instance
61 changes: 56 additions & 5 deletions backend/api/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from itertools import groupby

from codeforlife.serializers import ModelListSerializer
from codeforlife.user.models import Class, Student, User, UserProfile
from codeforlife.user.models import Class, Student, 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 (
Expand Down Expand Up @@ -119,31 +119,82 @@ class UserSerializer(_UserSerializer):
class Meta(_UserSerializer.Meta):
fields = [
*_UserSerializer.Meta.fields,
"student",
"teacher",
"password",
"current_password",
]
extra_kwargs = {
**_UserSerializer.Meta.extra_kwargs,
"first_name": {"read_only": False},
"last_name": {"read_only": False, "required": False},
"email": {"read_only": False},
"password": {"write_only": True, "required": False},
}
list_serializer_class = UserListSerializer

def validate(self, attrs):
if self.view.action != "reset-password":
pass
if self.instance is not None and self.view.action != "reset-password":
# TODO: make current password required when changing self-profile.
pass

if "new_teacher" in attrs and "last_name" not in attrs:
raise serializers.ValidationError(
"Last name is required.", code="last_name_required"
)

return attrs

def validate_password(self, value: str):
"""
Validate the new password depending on user type.
:param value: the new password
"""
_validate_password(value, self.instance)

# If we're creating a new user, we do not yet have the user object.
# Therefore, we need to create a dummy user and pass it to the password
# validators so they know what type of user we have.
instance = self.instance
if not instance:
instance = User()

user_type: str = self.context["user_type"]
if user_type == "teacher":
Teacher(new_user=instance)
elif user_type == "student":
Student(new_user=instance)

_validate_password(value, instance)

return value

def create(self, validated_data):
user = User.objects.create_user(
username=validated_data["email"],
email=validated_data["email"],
password=validated_data["password"],
first_name=validated_data["first_name"],
last_name=validated_data.get("last_name"),
)

user_profile = UserProfile.objects.create(
user=user,
is_verified=self.context.get("is_verified", False),
)

if "new_teacher" in validated_data:
Teacher.objects.create(
user=user_profile,
new_user=user,
is_admin=validated_data["new_teacher"]["is_admin"],
school=self.context.get("school"),
)
elif "new_student" in validated_data:
pass # TODO

# TODO: Handle signing new user up to newsletter if checkbox ticked

return user

def update(self, instance, validated_data):
password = validated_data.get("password")

Expand Down
22 changes: 22 additions & 0 deletions backend/api/signals/school_teacher_invitation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""
© Ocado Group
Created on 09/02/2024 at 17:02:00(+00:00).
All signals for the SchoolTeacherInvitation model.
"""

from django.db.models.signals import post_save
from django.dispatch import receiver

from ..models import SchoolTeacherInvitation


# pylint: disable=unused-argument
@receiver(post_save, sender=SchoolTeacherInvitation)
def school_teacher_invitation__post_save(
sender, instance: SchoolTeacherInvitation, *args, **kwargs
):
"""Send invitation email to invited teacher."""

instance._token # TODO: send email to invited teacher with differing
# content based on whether the email is already linked to an account or not.
Loading

0 comments on commit 9939972

Please sign in to comment.