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 school #70

Merged
merged 7 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
153 changes: 105 additions & 48 deletions codeforlife/tests/model_view_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from django.db.models import Model
from django.db.models.query import QuerySet
from django.urls import reverse as _reverse
from django.urls import reverse
from django.utils import timezone
from django.utils.http import urlencode
from pyotp import TOTP
Expand All @@ -22,7 +22,14 @@

from ..permissions import Permission
from ..serializers import ModelSerializer
from ..user.models import AuthFactor, User
from ..user.models import (
AuthFactor,
NonSchoolTeacherUser,
SchoolTeacherUser,
StudentUser,
TeacherUser,
User,
)
from ..views import ModelViewSet

AnyModel = t.TypeVar("AnyModel", bound=Model)
Expand Down Expand Up @@ -181,39 +188,6 @@ def parse_data(data):
"Data does not equal serialized model.",
)

def reverse(
self,
action: str,
model: t.Optional[AnyModel] = None,
**kwargs,
):
"""Get the reverse URL for the model view set's action.

Args:
action: The name of the action.
model: The model to look up.

Returns:
The reversed URL.
"""

reverse_kwargs = kwargs.pop("kwargs", {})
if model is not None:
reverse_kwargs[self._model_view_set_class.lookup_field] = getattr(
model,
self._model_view_set_class.lookup_field,
)

return _reverse(
viewname=kwargs.pop(
"viewname",
# pylint: disable-next=no-member
f"{self._test_case.basename}-{action}",
),
kwargs=reverse_kwargs,
**kwargs,
)

# pylint: disable-next=too-many-arguments
def generic(
self,
Expand Down Expand Up @@ -278,7 +252,8 @@ def create(
# pylint: enable=line-too-long

response: Response = self.post(
self.reverse("list"),
# pylint: disable-next=no-member
self._test_case.reverse_action("list"),
data=json.dumps(data),
content_type="application/json",
status_code_assertion=status_code_assertion,
Expand Down Expand Up @@ -316,7 +291,8 @@ def bulk_create(
# pylint: enable=line-too-long

response: Response = self.post(
self.reverse("bulk"),
# pylint: disable-next=no-member
self._test_case.reverse_action("bulk"),
data=json.dumps(data),
content_type="application/json",
status_code_assertion=status_code_assertion,
Expand Down Expand Up @@ -356,7 +332,8 @@ def retrieve(
# pylint: enable=line-too-long

response: Response = self.get(
self.reverse("detail", model),
# pylint: disable-next=no-member
self._test_case.reverse_action("detail", model),
status_code_assertion=status_code_assertion,
**kwargs,
)
Expand Down Expand Up @@ -403,7 +380,11 @@ def list(
).exists(), "List must exclude some models for a valid test."

response: Response = self.get(
f"{self.reverse('list')}?{urlencode(filters or {})}",
(
# pylint: disable-next=no-member
self._test_case.reverse_action("list")
+ f"?{urlencode(filters or {})}"
),
status_code_assertion=status_code_assertion,
**kwargs,
)
Expand Down Expand Up @@ -445,7 +426,8 @@ def partial_update(
# pylint: enable=line-too-long

response: Response = self.patch(
self.reverse("detail", model),
# pylint: disable-next=no-member
self._test_case.reverse_action("detail", model),
data=json.dumps(data),
content_type="application/json",
status_code_assertion=status_code_assertion,
Expand Down Expand Up @@ -490,7 +472,8 @@ def bulk_partial_update(
# pylint: enable=line-too-long

response: Response = self.patch(
self.reverse("bulk"),
# pylint: disable-next=no-member
self._test_case.reverse_action("bulk"),
data=json.dumps(data),
content_type="application/json",
status_code_assertion=status_code_assertion,
Expand Down Expand Up @@ -532,7 +515,8 @@ def destroy(
"""

response: Response = self.delete(
self.reverse("detail", model),
# pylint: disable-next=no-member
self._test_case.reverse_action("detail", model),
status_code_assertion=status_code_assertion,
**kwargs,
)
Expand Down Expand Up @@ -569,7 +553,8 @@ def bulk_destroy(
"""

response: Response = self.delete(
self.reverse("bulk"),
# pylint: disable-next=no-member
self._test_case.reverse_action("bulk"),
data=json.dumps(lookup_values),
content_type="application/json",
status_code_assertion=status_code_assertion,
Expand Down Expand Up @@ -612,12 +597,16 @@ def login(self, **credentials):

return user

def login_teacher(self, is_admin: t.Optional[bool] = None, **credentials):
def login_teacher(
self,
is_admin: t.Optional[bool] = None,
**credentials,
) -> TeacherUser:
# pylint: disable=line-too-long
"""Log in a user and assert they are a teacher.

Args:
is_admin: Whether or not the teacher is an admin. Set none if a teacher can be either or.
is_admin: Whether or not the teacher is an admin. Set to None if the teacher can be either or.

Returns:
The teacher-user.
Expand All @@ -626,12 +615,52 @@ def login_teacher(self, is_admin: t.Optional[bool] = None, **credentials):

user = self.login(**credentials)
assert user.teacher
assert user.teacher.school
if is_admin is not None:
assert is_admin == user.teacher.is_admin

return user

def login_student(self, **credentials):
def login_school_teacher(
self,
is_admin: t.Optional[bool] = None,
**credentials,
):
# pylint: disable=line-too-long
"""Log in a user and assert they are a school-teacher.

Args:
is_admin: Whether or not the teacher is an admin. Set to None if the teacher can be either or.

Returns:
The school-teacher-user.
"""
# pylint: enable=line-too-long

user = self.login_teacher(is_admin, **credentials)
assert user.teacher.school
return t.cast(SchoolTeacherUser, user)

def login_non_school_teacher(
self,
is_admin: t.Optional[bool] = None,
**credentials,
):
# pylint: disable=line-too-long
"""Log in a user and assert they are a non-school-teacher.

Args:
is_admin: Whether or not the teacher is an admin. Set to None if the teacher can be either or.

Returns:
The non-school-teacher-user.
"""
# pylint: enable=line-too-long

user = self.login_teacher(is_admin, **credentials)
assert not user.teacher.school
return t.cast(NonSchoolTeacherUser, user)

def login_student(self, **credentials) -> StudentUser:
"""Log in a user and assert they are a student.

Returns:
Expand All @@ -643,7 +672,8 @@ def login_student(self, **credentials):
assert user.student.class_field.teacher.school
return user

def login_indy(self, **credentials):
# TODO: set return type to IndependentUser when we use the new data models.
def login_indy(self, **credentials) -> StudentUser:
"""Log in an independent and assert they are a student.

Returns:
Expand Down Expand Up @@ -690,6 +720,33 @@ def setUpClass(cls):

return super().setUpClass()

def reverse_action(
self,
name: str,
model: t.Optional[AnyModel] = None,
**kwargs,
):
"""Get the reverse URL for the model view set's action.

Args:
name: The name of the action.
model: The model to look up.

Returns:
The reversed URL for the model view set's action.
"""

reverse_kwargs = kwargs.pop("kwargs", {})
if model is not None:
lookup_field = self.model_view_set_class.lookup_field
reverse_kwargs[lookup_field] = getattr(model, lookup_field)

return reverse(
viewname=kwargs.pop("viewname", f"{self.basename}-{name}"),
kwargs=reverse_kwargs,
**kwargs,
)

def assert_get_permissions(
self,
permissions: t.List[Permission],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Generated by Django 3.2.20 on 2024-02-05 10:04

import django.contrib.auth.models
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('common', '0045_otp'),
('user', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='NonSchoolTeacher',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('common.teacher',),
),
migrations.CreateModel(
name='NonSchoolTeacherUser',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('user.user',),
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='SchoolTeacher',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('common.teacher',),
),
migrations.CreateModel(
name='SchoolTeacherUser',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('user.user',),
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='StudentUser',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('user.user',),
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='TeacherUser',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('user.user',),
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]
11 changes: 9 additions & 2 deletions codeforlife/user/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,12 @@
from .session import Session
from .session_auth_factor import SessionAuthFactor
from .student import Student
from .teacher import Teacher
from .user import User, UserProfile # TODO: remove UserProfile
from .teacher import NonSchoolTeacher, SchoolTeacher, Teacher
from .user import ( # TODO: remove UserProfile
NonSchoolTeacherUser,
SchoolTeacherUser,
StudentUser,
TeacherUser,
User,
UserProfile,
)
Loading
Loading