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

fix: reset students password #88

Merged
merged 8 commits into from
Feb 20, 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
16 changes: 7 additions & 9 deletions codeforlife/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
"""
© Ocado Group
Created on 20/02/2024 at 09:28:26(+00:00).
"""

from pathlib import Path

from .version import __version__

BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR.joinpath("data")


from .version import __version__

from . import (
kurono,
service,
user,
)
USER_DIR = BASE_DIR.joinpath("user")
10 changes: 10 additions & 0 deletions codeforlife/request.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
"""
© Ocado Group
Created on 19/02/2024 at 15:28:22(+00:00).

Override default request objects.
"""

import typing as t

from django.contrib.auth.models import AnonymousUser
Expand All @@ -9,16 +16,19 @@
from .user.models.session import SessionStore


# pylint: disable-next=missing-class-docstring
class WSGIRequest(_WSGIRequest):
session: SessionStore
user: t.Union[User, AnonymousUser]


# pylint: disable-next=missing-class-docstring
class HttpRequest(_HttpRequest):
session: SessionStore
user: t.Union[User, AnonymousUser]


# pylint: disable-next=missing-class-docstring,abstract-method
class Request(_Request):
session: SessionStore
user: t.Union[User, AnonymousUser]
20 changes: 20 additions & 0 deletions codeforlife/response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""
© Ocado Group
Created on 19/02/2024 at 15:31:09(+00:00).

Override default response objects.
"""

import typing as t

from rest_framework import status as _status
from rest_framework.response import Response as _Response


class NonFieldErrorsResponse(_Response):
"""When errors occur that are not tied to one data field in the request."""

def __init__(self, data: t.List[str], **kwargs):
kwargs["data"] = {"non_field_errors": data}
kwargs["status"] = _status.HTTP_400_BAD_REQUEST
super().__init__(**kwargs)
50 changes: 43 additions & 7 deletions codeforlife/serializers/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@
from rest_framework.serializers import ModelSerializer as _ModelSerializer
from rest_framework.serializers import ValidationError as _ValidationError

from ..types import DataDict
from ..types import DataDict, OrderedDataDict
from .base import BaseSerializer

AnyModel = t.TypeVar("AnyModel", bound=Model)


BulkCreateDataList = t.List[DataDict]
BulkUpdateDataDict = t.Dict[t.Any, DataDict]
Data = t.Union[BulkCreateDataList, BulkUpdateDataDict]


class ModelSerializer(
BaseSerializer,
_ModelSerializer[AnyModel],
Expand Down Expand Up @@ -129,9 +134,6 @@ def update(
Returns:
The models.
"""
instance.sort(key=lambda model: getattr(model, self.view.lookup_field))
validated_data.sort(key=lambda data: data[self.view.lookup_field_name])

# Models and data must have equal length and be ordered the same!
for model, data in zip(instance, validated_data):
for field, value in data.items():
Expand All @@ -150,17 +152,51 @@ def validate(self, attrs: t.List[DataDict]):
# If performing a bulk create.
if self.instance is None:
if len(attrs) == 0:
raise _ValidationError("Nothing to create.")
raise _ValidationError(
"Nothing to create.",
code="nothing_to_create",
)

# Else, performing a bulk update.
else:
if len(attrs) == 0:
raise _ValidationError("Nothing to update.")
raise _ValidationError(
"Nothing to update.",
code="nothing_to_update",
)
if len(attrs) != len(self.instance):
raise _ValidationError("Some models do not exist.")
raise _ValidationError(
"Some models do not exist.",
code="models_do_not_exist",
)

return attrs

def to_internal_value(self, data: Data):
# If performing a bulk create.
if self.instance is None:
data = t.cast(BulkCreateDataList, data)

return t.cast(
t.List[OrderedDataDict],
super().to_internal_value(data),
)

# Else, performing a bulk update.
data = t.cast(BulkUpdateDataDict, data)
data_items = list(data.items())

# Models and data are required to be sorted by the lookup field.
data_items.sort(key=lambda item: item[0])
self.instance.sort(
key=lambda model: getattr(model, self.view.lookup_field)
)

return t.cast(
t.List[OrderedDataDict],
super().to_internal_value([item[1] for item in data_items]),
)

# pylint: disable-next=useless-parent-delegation,arguments-renamed
def to_representation(self, instance: t.List[AnyModel]) -> t.List[DataDict]:
return super().to_representation(instance)
138 changes: 80 additions & 58 deletions codeforlife/tests/api_request_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,33 @@
JSONParser,
MultiPartParser,
)
from rest_framework.request import Request
from rest_framework.test import APIRequestFactory as _APIRequestFactory

from ..request import Request
from ..user.models import User


class APIRequestFactory(_APIRequestFactory):
"""Custom API request factory that returns DRF's Request object."""

def request(self, user: t.Optional[User] = None, **kwargs):
wsgi_request = t.cast(WSGIRequest, super().request(**kwargs))

request = Request(
wsgi_request,
parsers=[
JSONParser(),
FormParser(),
MultiPartParser(),
FileUploadParser(),
],
)

if user:
request.user = user

return request

# pylint: disable-next=too-many-arguments
def generic(
self,
Expand All @@ -32,45 +50,34 @@ def generic(
user: t.Optional[User] = None,
**extra
):
wsgi_request = t.cast(
WSGIRequest,
return t.cast(
Request,
super().generic(
method,
path or "/",
data or "",
content_type or "application/json",
secure,
user=user,
**extra,
),
)

request = Request(
wsgi_request,
parsers=[
JSONParser(),
FormParser(),
MultiPartParser(),
FileUploadParser(),
],
)

if user:
request.user = user

return request

def get( # type: ignore[override]
self,
path: t.Optional[str] = None,
data: t.Any = None,
user: t.Optional[User] = None,
**extra
):
return super().get(
path or "/",
data,
user=user,
**extra,
return t.cast(
Request,
super().get(
path or "/",
data,
user=user,
**extra,
),
)

# pylint: disable-next=too-many-arguments
Expand All @@ -87,13 +94,16 @@ def post( # type: ignore[override]
if format is None and content_type is None:
format = "json"

return super().post(
path or "/",
data,
format,
content_type,
user=user,
**extra,
return t.cast(
Request,
super().post(
path or "/",
data,
format,
content_type,
user=user,
**extra,
),
)

# pylint: disable-next=too-many-arguments
Expand All @@ -110,13 +120,16 @@ def put( # type: ignore[override]
if format is None and content_type is None:
format = "json"

return super().put(
path or "/",
data,
format,
content_type,
user=user,
**extra,
return t.cast(
Request,
super().put(
path or "/",
data,
format,
content_type,
user=user,
**extra,
),
)

# pylint: disable-next=too-many-arguments
Expand All @@ -133,13 +146,16 @@ def patch( # type: ignore[override]
if format is None and content_type is None:
format = "json"

return super().patch(
path or "/",
data,
format,
content_type,
user=user,
**extra,
return t.cast(
Request,
super().patch(
path or "/",
data,
format,
content_type,
user=user,
**extra,
),
)

# pylint: disable-next=too-many-arguments
Expand All @@ -156,13 +172,16 @@ def delete( # type: ignore[override]
if format is None and content_type is None:
format = "json"

return super().delete(
path or "/",
data,
format,
content_type,
user=user,
**extra,
return t.cast(
Request,
super().delete(
path or "/",
data,
format,
content_type,
user=user,
**extra,
),
)

# pylint: disable-next=too-many-arguments
Expand All @@ -179,11 +198,14 @@ def options( # type: ignore[override]
if format is None and content_type is None:
format = "json"

return super().options(
path or "/",
data or {},
format,
content_type,
user=user,
**extra,
return t.cast(
Request,
super().options(
path or "/",
data or {},
format,
content_type,
user=user,
**extra,
),
)
Loading
Loading