Skip to content

Commit

Permalink
fix model structure
Browse files Browse the repository at this point in the history
  • Loading branch information
SKairinos committed Dec 9, 2023
1 parent 4c34fd8 commit c058b83
Show file tree
Hide file tree
Showing 13 changed files with 101 additions and 44 deletions.
10 changes: 10 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
{
"black-formatter.path": [
".venv/bin/python",
"-m",
"black"
],
"black-formatter.args": [
"--config",
"pyproject.toml"
],
"mypy-type-checker.path": [
".venv/bin/python",
"-m",
"mypy"
],
"pylint.path": [
".venv/bin/python",
"-m",
Expand Down
15 changes: 12 additions & 3 deletions codeforlife/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from pathlib import Path
"""
© Ocado Group
Created on 09/12/2023 at 11:02:54(+00:00).
Entry point to the Code for Life package.
"""

import django_stubs_ext
import typing as t
from pathlib import Path

from .version import __version__

Expand All @@ -11,7 +17,10 @@
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR.joinpath("data")

django_stubs_ext.monkeypatch()
if t.TYPE_CHECKING:
import django_stubs_ext

django_stubs_ext.monkeypatch()

# ------------------------------------------------------------------------------

Expand Down
56 changes: 40 additions & 16 deletions codeforlife/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,28 @@
from .fields import *


class AbstractModel(models.Model):
"""Base model to be inherited by other models throughout the CFL system."""
class Model(models.Model):
"""Provide type hints for general model attributes."""

id: int
pk: int
objects: models.Manager
DoesNotExist: t.Type[ObjectDoesNotExist]

class Meta(TypedModelMeta):
abstract = True


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


class WarehouseModel(Model):
"""To be inherited by all models whose data is to be warehoused."""

class QuerySet(models.QuerySet):
"""Custom queryset to support CFL's system's operations."""

model: "AbstractModel" # type: ignore[assignment]
model: "WarehouseModel" # type: ignore[assignment]

def update(self, **kwargs):
"""Updates all models in the queryset and notes when they were last
Expand All @@ -44,18 +59,19 @@ def delete(self, wait: t.Optional[timedelta] = None):
Args:
wait: How long to wait before these model are deleted. If not
set, the class-level default value is used.
set, the class-level default value is used. To delete
immediately, set wait to 0 with timedelta().
"""

wait = wait or self.model.delete_wait
self.update(delete_after=timezone.now() + wait)
if wait is None:
wait = self.model.delete_wait

objects = models.Manager.from_queryset(QuerySet)()
if wait == timedelta():
super().delete()
else:
self.update(delete_after=timezone.now() + wait)

# Type hints for Django's runtime-generated fields.
id: int
pk: int
DoesNotExist: t.Type[ObjectDoesNotExist]
objects = models.Manager.from_queryset(QuerySet)()

# Default for how long to wait before a model is deleted.
delete_wait = timedelta(days=3)
Expand Down Expand Up @@ -89,18 +105,26 @@ class Meta(TypedModelMeta):
# pylint: disable-next=arguments-differ
def delete( # type: ignore[override]
self,
*args,
wait: t.Optional[timedelta] = None,
**kwargs,
):
"""Schedules the deletion of this model.
Args:
wait: How long to wait before this model is deleted. If not set, the
class-level default value is used.
class-level default value is used. To delete immediately, set
wait to 0 with timedelta().
"""

wait = wait or self.delete_wait
self.delete_after = timezone.now() + wait
self.save()
if wait is None:
wait = self.delete_wait

if wait == timedelta():
super().delete(*args, **kwargs)
else:
self.delete_after = timezone.now() + wait
self.save(*args, **kwargs)


AnyModel = t.TypeVar("AnyModel", bound=AbstractModel)
AnyWarehouseModel = t.TypeVar("AnyWarehouseModel", bound=WarehouseModel)
18 changes: 16 additions & 2 deletions codeforlife/tests/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@

import typing as t

from django.db.models import Model
from django.db.utils import IntegrityError
from django.test import TestCase

AnyModel = t.TypeVar("AnyModel", bound=Model)
from ..models import AnyModel, Model


class ModelTestCase(TestCase, t.Generic[AnyModel]):
Expand All @@ -38,3 +37,18 @@ def assert_raises_integrity_error(self, *args, **kwargs):
"""

return self.assertRaises(IntegrityError, *args, **kwargs)

def assert_does_not_exist(self, model_or_pk: t.Union[AnyModel, t.Any]):
"""Asserts the model does not exist.
Args:
model_or_pk: The model itself or its primary key.
"""

if isinstance(model_or_pk, Model):
with self.assertRaises(model_or_pk.DoesNotExist):
model_or_pk.refresh_from_db()
else:
model_class = self.get_model_class()
with self.assertRaises(model_class.DoesNotExist):
model_class.objects.get(pk=model_or_pk)
4 changes: 2 additions & 2 deletions codeforlife/user/models/auth_factor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta

from ...models import AbstractModel
from ...models import WarehouseModel
from . import user as _user


class AuthFactor(AbstractModel):
class AuthFactor(WarehouseModel):
"""A user's enabled authentication factors."""

class Type(models.TextChoices):
Expand Down
4 changes: 2 additions & 2 deletions codeforlife/user/models/class_student_join_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@

# from django.db import models

# from ...models import AbstractModel
# from ...models import WarehouseModel
# from . import klass as _class
# from . import student as _student


# # TODO: move to portal
# class ClassStudentJoinRequest(AbstractModel):
# class ClassStudentJoinRequest(WarehouseModel):
# """A request from a student to join a class."""

# klass: "_class.Class" = models.ForeignKey(
Expand Down
4 changes: 2 additions & 2 deletions codeforlife/user/models/klass.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta

from ...models import AbstractModel
from ...models import WarehouseModel
from . import school as _school
from . import student as _student
from . import teacher as _teacher


class Class(AbstractModel):
class Class(WarehouseModel):
"""A collection of students owned by a teacher."""

pk: str # type: ignore[assignment]
Expand Down
6 changes: 3 additions & 3 deletions codeforlife/user/models/otp_bypass_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta

from ...models import AbstractModel
from ...models import WarehouseModel
from . import user as _user


class OtpBypassToken(AbstractModel):
class OtpBypassToken(WarehouseModel):
"""
A one-time-use token that a user can use to bypass their OTP auth factor.
Each user has a limited number of OTP-bypass tokens.
Expand Down Expand Up @@ -80,7 +80,7 @@ def key(otp_bypass_token: OtpBypassToken):
return super().bulk_create(otp_bypass_tokens, *args, **kwargs)

objects: Manager = Manager.from_queryset( # type: ignore[misc]
AbstractModel.QuerySet
WarehouseModel.QuerySet
)() # type: ignore[assignment]

user: "_user.User" = models.ForeignKey( # type: ignore[assignment]
Expand Down
6 changes: 3 additions & 3 deletions codeforlife/user/models/school.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,22 @@
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta

from ...models import AbstractModel
from ...models import WarehouseModel
from ...models.fields import Country, UkCounty
from . import klass as _class
from . import student as _student
from . import teacher as _teacher


class School(AbstractModel):
class School(WarehouseModel):
"""A collection of teachers and students."""

# pylint: disable-next=missing-class-docstring
class Manager(models.Manager["School"]):
pass

objects: Manager = Manager.from_queryset( # type: ignore[misc]
AbstractModel.QuerySet
WarehouseModel.QuerySet
)() # type: ignore[assignment]

teachers: QuerySet["_teacher.Teacher"]
Expand Down
4 changes: 2 additions & 2 deletions codeforlife/user/models/school_teacher_invitation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# from django.utils import timezone
# from django.utils.translation import gettext_lazy as _

# from ...models import AbstractModel
# from ...models import WarehouseModel
# from . import school as _school
# from . import teacher as _teacher

Expand All @@ -21,7 +21,7 @@


# # TODO: move to portal
# class SchoolTeacherInvitation(AbstractModel):
# class SchoolTeacherInvitation(WarehouseModel):
# """An invitation for a teacher to join a school."""

# school: "_school.School" = models.ForeignKey(
Expand Down
6 changes: 3 additions & 3 deletions codeforlife/user/models/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta

from ...models import AbstractModel
from ...models import WarehouseModel
from . import klass as _class
from . import school as _school
from . import user as _user


class Student(AbstractModel):
class Student(WarehouseModel):
"""A user's student profile."""

# pylint: disable-next=missing-class-docstring
Expand Down Expand Up @@ -108,7 +108,7 @@ def bulk_create_users(
return _user.User.objects.bulk_create(users, *args, **kwargs)

objects: Manager = Manager.from_queryset( # type: ignore[misc]
AbstractModel.QuerySet
WarehouseModel.QuerySet
)() # type: ignore[assignment]

user: "_user.User"
Expand Down
6 changes: 3 additions & 3 deletions codeforlife/user/models/teacher.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta

from ...models import AbstractModel
from ...models import WarehouseModel
from . import klass as _class
from . import school as _school
from . import student as _student
from . import user as _user


class Teacher(AbstractModel):
class Teacher(WarehouseModel):
"""A user's teacher profile."""

# pylint: disable-next=missing-class-docstring
Expand All @@ -40,7 +40,7 @@ def create_user(self, teacher: t.Dict[str, t.Any], **fields):
)

objects: Manager = Manager.from_queryset( # type: ignore[misc]
AbstractModel.QuerySet
WarehouseModel.QuerySet
)() # type: ignore[assignment]

user: "_user.User"
Expand Down
6 changes: 3 additions & 3 deletions codeforlife/user/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta

from ...models import AbstractModel
from ...models import WarehouseModel
from . import auth_factor as _auth_factor
from . import otp_bypass_token as _otp_bypass_token
from . import session as _session
from . import student as _student
from . import teacher as _teacher


class User(AbstractBaseUser, AbstractModel, PermissionsMixin):
class User(AbstractBaseUser, WarehouseModel, PermissionsMixin):
"""A user within the CFL system."""

USERNAME_FIELD = "email"
Expand Down Expand Up @@ -108,7 +108,7 @@ def create_superuser(self, password: str, first_name: str, **fields):
return self._create_user(password, first_name=first_name, **fields)

objects: Manager = Manager.from_queryset( # type: ignore[misc]
AbstractModel.QuerySet
WarehouseModel.QuerySet
)() # type: ignore[assignment]

session: "_session.Session"
Expand Down

0 comments on commit c058b83

Please sign in to comment.