Skip to content

Commit

Permalink
feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
SKairinos committed Dec 11, 2023
1 parent 7a41243 commit 190dff8
Show file tree
Hide file tree
Showing 11 changed files with 102 additions and 19 deletions.
14 changes: 10 additions & 4 deletions codeforlife/user/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 3.2.20 on 2023-12-11 09:53
# Generated by Django 3.2.20 on 2023-12-11 14:36

from django.conf import settings
import django.core.validators
Expand Down Expand Up @@ -71,8 +71,6 @@ class Migration(migrations.Migration):
name='OtpBypassToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('last_saved_at', models.DateTimeField(auto_now=True, help_text='Record the last time the model was saved. This is used by our data warehouse to know what data was modified since the last scheduled data transfer from the database to the data warehouse.', verbose_name='last saved at')),
('delete_after', models.DateTimeField(blank=True, help_text="When this data is scheduled for deletion. Set to null if not 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 point in time.", null=True, verbose_name='delete after')),
('token', models.CharField(max_length=8, validators=[django.core.validators.MinLengthValidator(8)])),
],
options={
Expand Down Expand Up @@ -116,7 +114,7 @@ class Migration(migrations.Migration):
('last_saved_at', models.DateTimeField(auto_now=True, help_text='Record the last time the model was saved. This is used by our data warehouse to know what data was modified since the last scheduled data transfer from the database to the data warehouse.', verbose_name='last saved at')),
('delete_after', models.DateTimeField(blank=True, help_text="When this data is scheduled for deletion. Set to null if not 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 point in time.", null=True, verbose_name='delete after')),
('is_admin', models.BooleanField(default=False, help_text='Designates if the teacher has admin privileges.', verbose_name='is administrator')),
('school', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='teachers', to='user.school')),
('school', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='teachers', to='user.school')),
],
options={
'verbose_name': 'teacher',
Expand Down Expand Up @@ -202,6 +200,10 @@ class Migration(migrations.Migration):
name='otpbypasstoken',
unique_together={('user', 'token')},
),
migrations.AlterUniqueTogether(
name='class',
unique_together={('name', 'school')},
),
migrations.AlterUniqueTogether(
name='authfactor',
unique_together={('user', 'type')},
Expand All @@ -214,4 +216,8 @@ class Migration(migrations.Migration):
model_name='user',
constraint=models.CheckConstraint(check=models.Q(models.Q(('email__isnull', False), ('teacher__isnull', False)), models.Q(('email__isnull', True), ('student__isnull', False)), models.Q(('email__isnull', False), ('student__isnull', True), ('teacher__isnull', True)), _connector='OR'), name='user__email'),
),
migrations.AddConstraint(
model_name='user',
constraint=models.CheckConstraint(check=models.Q(models.Q(('last_name__isnull', False), ('teacher__isnull', False)), models.Q(('last_name__isnull', True), ('student__isnull', False)), models.Q(('last_name__isnull', False), ('student__isnull', True), ('teacher__isnull', True)), _connector='OR'), name='user__last_name'),
),
]
2 changes: 2 additions & 0 deletions codeforlife/user/models/klass.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class Manager(WarehouseModel.Manager["Class"]):
max_length=200,
)

# TODO: phase out and use django's permission system.
read_classmates_data = models.BooleanField(
_("read classmates data"),
default=False,
Expand All @@ -87,3 +88,4 @@ class Manager(WarehouseModel.Manager["Class"]):
class Meta(TypedModelMeta):
verbose_name = _("class")
verbose_name_plural = _("classes")
unique_together = ["name", "school"]
5 changes: 2 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,10 @@
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta

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


class OtpBypassToken(WarehouseModel):
class OtpBypassToken(models.Model):
"""
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 All @@ -31,7 +30,7 @@ class OtpBypassToken(WarehouseModel):
)

# pylint: disable-next=missing-class-docstring
class Manager(WarehouseModel.Manager["OtpBypassToken"]):
class Manager(models.Manager["OtpBypassToken"]):
def create(self, token: str, **kwargs): # type: ignore[override]
"""Create an OTP-bypass token.
Expand Down
3 changes: 1 addition & 2 deletions codeforlife/user/models/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,4 @@ def teacher(self):
The student's class-teacher.
"""

if self.klass:
return self.klass.teacher
return self.klass.teacher
2 changes: 1 addition & 1 deletion codeforlife/user/models/teacher.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ def create_user(self, teacher: t.Dict[str, t.Any], **fields):
"user.School",
related_name="teachers",
null=True,
editable=False,
on_delete=models.SET_NULL,
)

# TODO: phase out and use django's permission system.
is_admin = models.BooleanField(
_("is administrator"),
default=False,
Expand Down
24 changes: 20 additions & 4 deletions codeforlife/user/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,6 @@ def create_superuser(self, password: str, first_name: str, **fields):
blank=True,
)

# TODO: is last name required for teachers?
# TODO: are students allowed to have a last name?
last_name = models.CharField(
_("last name"),
max_length=150,
Expand Down Expand Up @@ -198,9 +196,9 @@ class Meta(TypedModelMeta):
verbose_name = _("user")
verbose_name_plural = _("users")
constraints = [
# pylint: disable=unsupported-binary-operation
models.CheckConstraint(
check=(
# pylint: disable-next=unsupported-binary-operation
Q(
teacher__isnull=True,
student__isnull=False,
Expand All @@ -218,7 +216,6 @@ class Meta(TypedModelMeta):
),
models.CheckConstraint(
check=(
# pylint: disable-next=unsupported-binary-operation
Q(
teacher__isnull=False,
email__isnull=False,
Expand All @@ -235,6 +232,25 @@ class Meta(TypedModelMeta):
),
name="user__email",
),
models.CheckConstraint(
check=(
Q(
teacher__isnull=False,
last_name__isnull=False,
)
| Q(
student__isnull=False,
last_name__isnull=True,
)
| Q(
teacher__isnull=True,
student__isnull=True,
last_name__isnull=False,
)
),
name="user__last_name",
),
# pylint: enable=unsupported-binary-operation
]

@property
Expand Down
1 change: 1 addition & 0 deletions codeforlife/user/tests/models/test_otp_bypass_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def setUp(self):
self.user = User.objects.get(id=2)

# TODO: test docstrings.
# TODO: fix unit tests.

def test_bulk_create(self):
token = get_random_string(8)
Expand Down
2 changes: 2 additions & 0 deletions codeforlife/user/tests/models/test_teacher.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def test_objects__create_user(self):
teacher_fields = {"is_admin": True}
user_fields = {
"first_name": "first_name",
"last_name": "last_name",
"email": "[email protected]",
"password": "password",
}
Expand All @@ -42,6 +43,7 @@ def test_objects__create_user(self):
)

assert user.first_name == user_fields["first_name"]
assert user.last_name == user_fields["last_name"]
assert user.email == user_fields["email"]
assert user.password != user_fields["password"]
assert user.check_password(user_fields["password"])
Expand Down
65 changes: 61 additions & 4 deletions codeforlife/user/tests/models/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def test_constraints__profile(self):
User.objects.create_user(
password="password",
first_name="student_and_teacher",
last_name="last_name",
student=student,
teacher=teacher,
)
Expand All @@ -55,6 +56,7 @@ def test_constraints__email__teacher(self):
User.objects.create_user(
password="password",
first_name="teacher",
last_name="last_name",
teacher=teacher,
)

Expand Down Expand Up @@ -86,6 +88,53 @@ def test_constraints__email__indy(self):
User.objects.create_user(
password="password",
first_name="first_name",
last_name="last_name",
)

def test_constraints__last_name__teacher(self):
"""
Teachers must have a last name.
"""

teacher = Teacher.objects.create()

with self.assert_raises_integrity_error():
User.objects.create_user(
password="password",
first_name="teacher",
email="[email protected]",
teacher=teacher,
)

def test_constraints__last_name__students(self):
"""
Students can't have a last name.
"""

student = Student.objects.create(
auto_gen_password="password",
klass=self.klass__AB123,
school=self.school__1,
)

with self.assert_raises_integrity_error():
User.objects.create_user(
password="password",
first_name="student",
last_name="last_name",
student=student,
)

def test_constraints__last_name__indy(self):
"""
Independents must have a last name.
"""

with self.assert_raises_integrity_error():
User.objects.create_user(
password="password",
first_name="teacher",
email="[email protected]",
)

def test_objects__create(self):
Expand All @@ -103,13 +152,15 @@ def test_objects__create_user__teacher(self):

user_fields = {
"first_name": "first_name",
"last_name": "last_name",
"email": "[email protected]",
"password": "password",
"teacher": Teacher.objects.create(),
}

user = User.objects.create_user(**user_fields) # type: ignore[arg-type]
assert user.first_name == user_fields["first_name"]
assert user.last_name == user_fields["last_name"]
assert user.email == user_fields["email"]
assert user.password != user_fields["password"]
assert user.check_password(user_fields["password"])
Expand Down Expand Up @@ -143,12 +194,14 @@ def test_objects__create_user__indy(self):

user_fields = {
"first_name": "first_name",
"last_name": "last_name",
"email": "[email protected]",
"password": "password",
}

user = User.objects.create_user(**user_fields)
assert user.first_name == user_fields["first_name"]
assert user.last_name == user_fields["last_name"]
assert user.email == user_fields["email"]
assert user.password != user_fields["password"]
assert user.check_password(user_fields["password"])
Expand All @@ -160,15 +213,17 @@ def test_objects__create_superuser__teacher(self):

user_fields = {
"first_name": "first_name",
"last_name": "last_name",
"email": "[email protected]",
"password": "password",
"teacher": Teacher.objects.create(),
}

user = User.objects.create_superuser(
**user_fields
) # type: ignore[arg-type]
**user_fields # type: ignore[arg-type]
)
assert user.first_name == user_fields["first_name"]
assert user.last_name == user_fields["last_name"]
assert user.email == user_fields["email"]
assert user.password != user_fields["password"]
assert user.check_password(user_fields["password"])
Expand All @@ -192,8 +247,8 @@ def test_objects__create_superuser__student(self):
}

user = User.objects.create_superuser(
**user_fields
) # type: ignore[arg-type]
**user_fields # type: ignore[arg-type]
)
assert user.first_name == user_fields["first_name"]
assert user.password != user_fields["password"]
assert user.check_password(user_fields["password"])
Expand All @@ -208,12 +263,14 @@ def test_objects__create_superuser__indy(self):

user_fields = {
"first_name": "first_name",
"last_name": "last_name",
"email": "[email protected]",
"password": "password",
}

user = User.objects.create_superuser(**user_fields)
assert user.first_name == user_fields["first_name"]
assert user.last_name == user_fields["last_name"]
assert user.email == user_fields["email"]
assert user.password != user_fields["password"]
assert user.check_password(user_fields["password"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from ...models import User


class TestAbstract(ModelTestCase[User]):
class TestWarehouse(ModelTestCase[User]):
"""
Tests the abstract model inherited by other models.
Expand Down
1 change: 1 addition & 0 deletions codeforlife/user/tests/views/test_klass.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,4 @@ def test_retrieve__student__same_school__in_class(self):
self._retrieve_class(user.student.class_field)

# TODO: other retrieve and list tests
# TODO: fix unit tests.

0 comments on commit 190dff8

Please sign in to comment.