Skip to content

Commit

Permalink
quick save
Browse files Browse the repository at this point in the history
  • Loading branch information
SKairinos committed Dec 7, 2023
1 parent bebdae4 commit 564e63d
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 89 deletions.
33 changes: 31 additions & 2 deletions codeforlife/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
"""

import typing as t
from datetime import timedelta

from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from .fields import *
Expand All @@ -16,10 +18,24 @@
class AbstractModel(models.Model):
"""Base model to be inherited by other models throughout the CFL system."""

class Manager(models.Manager):
"""Custom model manager to support CFL's system's operations."""

def delete(self):
"""Schedules all objects in the queryset for deletion."""

# TODO: only schedule for deletion.
super().delete()

objects: Manager = Manager()

# Type hints for Django's runtime-generated fields.
id: int
pk: int

# https://docs.djangoproject.com/en/3.2/ref/models/fields/#django.db.models.DateField.auto_now
# Default for how long to wait before a model is deleted.
delete_wait = timedelta(days=3)

last_saved_at = models.DateTimeField(
_("last saved at"),
auto_now=True,
Expand All @@ -39,12 +55,25 @@ class AbstractModel(models.Model):
" scheduled for deletion. This is used by our data warehouse to"
" transfer data that's been scheduled for deletion before it's"
" actually deleted. Data will actually be deleted in a CRON job"
" after this delete after."
" after this point in time."
),
)

class Meta:
abstract = True

# pylint: disable-next=arguments-differ
def delete(self, wait: t.Optional[timedelta] = None):
"""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.
"""

wait = wait or self.delete_wait
self.delete_after = timezone.now() + wait
self.save()


AnyModel = t.TypeVar("AnyModel", bound=AbstractModel)
63 changes: 44 additions & 19 deletions codeforlife/user/migrations/0001_initial.py

Large diffs are not rendered by default.

27 changes: 0 additions & 27 deletions codeforlife/user/migrations/0002_classstudentjoinrequest.py

This file was deleted.

3 changes: 3 additions & 0 deletions codeforlife/user/models/class_student_join_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from . import student as _student


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

Expand All @@ -35,3 +36,5 @@ class ClassStudentJoinRequest(AbstractModel):

class Meta:
unique_together = ["klass", "student"]
# TODO: check student is independent
# assert class is receiving requests
12 changes: 6 additions & 6 deletions codeforlife/user/models/klass.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ class Class(AbstractModel):
validators=[
MinLengthValidator(5),
RegexValidator(
regex=r"^[0-9a-zA-Z]*$",
message="ID must be alphanumeric.",
code="id_not_alphanumeric",
regex=r"^[0-9A-Z]*$",
message="ID must be alphanumeric with upper case characters.",
code="id_not_upper_alphanumeric",
),
],
)
Expand All @@ -66,13 +66,13 @@ class Class(AbstractModel):
),
)

accept_requests_until = models.DateTimeField(
receive_requests_until = models.DateTimeField(
_("accept student join requests until"),
null=True,
blank=True,
help_text=_(
"A point in the future until which requests from students to join"
" this class are accepted. Set to null if it's not accepting"
"A point in the future until which the class can receive requests"
" from students to join. Set to null if it's not accepting"
" requests."
),
)
Expand Down
30 changes: 20 additions & 10 deletions codeforlife/user/models/school_teacher_invitation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@
School teacher invitation model.
"""

from datetime import timedelta

from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

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


def _set_expires_at():
return lambda: timezone.now() + timedelta(days=7)


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

Expand All @@ -28,17 +36,19 @@ class SchoolTeacherInvitation(AbstractModel):
on_delete=models.CASCADE,
)

# created_at = models.DateTimeField(
# _("created at"),
# auto_now_add=True,
# help_text=_("When the teacher was invited to the school."),
# )

# expires_at = models.DateTimeField()
expires_at = models.DateTimeField(
_("is expired"),
default=_set_expires_at,
help_text=_("When the teacher was invited to the school."),
)

class Meta:
unique_together = ["school", "teacher"]

# @property
# def is_expired(self):
# return self.expires_at < timezone.now()
@property
def is_expired(self):
return self.expires_at < timezone.now()

def refresh(self):
self.expires_at = _set_expires_at()
self.save()
27 changes: 21 additions & 6 deletions codeforlife/user/models/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from django.contrib.auth.hashers import make_password
from django.core.validators import MinLengthValidator
from django.db import models
from django.db.models import Q
from django.db.models.query import QuerySet
from django.utils.translation import gettext_lazy as _

Expand Down Expand Up @@ -81,7 +82,8 @@ def make_random_direct_login_key(self):
"_class_student_join_request.ClassStudentJoinRequest"
]

school: "_school.School" = models.OneToOneField(
# Is this needed or can it be inferred from klass.
school: "_school.School" = models.ForeignKey(
"user.School",
related_name="students",
null=True,
Expand All @@ -92,19 +94,32 @@ def make_random_direct_login_key(self):
klass: "_class.Class" = models.ForeignKey(
"user.Class",
related_name="students",
null=True,
editable=False,
on_delete=models.CASCADE,
)

direct_login_key = models.CharField(
_("direct login key"),
unique=True,
max_length=64,
second_password = models.CharField( # TODO: make nullable
_("secondary password"),
max_length=64, # investigate hash length
editable=False,
help_text=_(
"A unique key that allows a student to log directly into their"
"account."
"account." # TODO
),
validators=[MinLengthValidator(64)],
)

# TODO: add direct reference to teacher
# TODO: add meta constraint for school & direct_login_key

class Meta:
constraints = [
models.CheckConstraint(
check=(
Q(school__isnull=True, klass__isnull=True)
| Q(school__isnull=False, klass__isnull=False)
),
name="student__school_is_null_and_class_is_null",
),
]
9 changes: 6 additions & 3 deletions codeforlife/user/models/teacher.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
class Teacher(AbstractModel):
"""A user's teacher profile."""

class Manager(models.Manager): # pylint: disable=missing-class-docstring
# pylint: disable-next=missing-class-docstring
class Manager(AbstractModel.Manager):
def create_user(self, teacher: t.Dict[str, t.Any], **fields):
"""Create a user with a teacher profile.
Expand All @@ -45,16 +46,18 @@ def create_user(self, teacher: t.Dict[str, t.Any], **fields):
"_school_teacher_invitation.SchoolTeacherInvitation"
]

school: "_school.School" = models.OneToOneField(
school: "_school.School" = models.ForeignKey(
"user.School",
related_name="teachers",
null=True,
editable=False,
on_delete=models.CASCADE,
on_delete=models.SET_NULL,
)

is_admin = models.BooleanField(
_("is administrator"),
default=False,
help_text=_("Designates if the teacher has admin privileges."),
)

# TODO: add direct reference to students
22 changes: 6 additions & 16 deletions codeforlife/user/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
class User(AbstractUser, AbstractModel):
"""A user within the CFL system."""

# Fixes bug with object referencing.
objects: UserManager = UserManager()
class Manager(UserManager, AbstractModel.Manager):
"""Combines the user manager and CFL model manager."""

objects: Manager = Manager()

session: "_session.Session"
auth_factors: QuerySet["_auth_factor.AuthFactor"]
Expand Down Expand Up @@ -65,8 +67,8 @@ class Meta: # pylint: disable=missing-class-docstring
constraints = [
models.CheckConstraint(
check=(
(Q(teacher__isnull=True) & Q(student__isnull=False))
| (Q(teacher__isnull=False) & Q(student__isnull=True))
Q(teacher__isnull=True, student__isnull=False)
| Q(teacher__isnull=False, student__isnull=True)
),
name="user__teacher_is_null_or_student_is_null",
),
Expand All @@ -80,15 +82,3 @@ def is_authenticated(self):
return not self.session.session_auth_factors.exists()
except _session.Session.DoesNotExist:
return False

@property
def is_teacher(self):
"""Check if the user is a teacher."""

return _teacher.Teacher.objects.filter(user=self).exists()

@property
def is_student(self):
"""Check if the user is a student."""

return _student.Student.objects.filter(user=self).exists()

0 comments on commit 564e63d

Please sign in to comment.