From 8a0c9558e0fdd29370313eac9053142c79545154 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 14 May 2026 08:59:47 +0000 Subject: [PATCH 1/7] fix model schema --- codeforlife/models/fields/decorators.py | 24 ++ codeforlife/types.py | 7 +- .../0004_client_side_encryption_part_2.py | 238 ++++++++++++++++++ codeforlife/user/models/__init__.py | 6 +- codeforlife/user/models/klass.py | 59 ++--- codeforlife/user/models/other.py | 73 ++---- codeforlife/user/models/school.py | 37 ++- codeforlife/user/models/user/__init__.py | 9 +- codeforlife/user/models/user/user.py | 117 ++++----- codeforlife/user/serializers/klass.py | 3 +- codeforlife/user/serializers/school.py | 3 +- codeforlife/user/serializers/user.py | 13 +- 12 files changed, 379 insertions(+), 210 deletions(-) create mode 100644 codeforlife/models/fields/decorators.py create mode 100644 codeforlife/user/migrations/0004_client_side_encryption_part_2.py diff --git a/codeforlife/models/fields/decorators.py b/codeforlife/models/fields/decorators.py new file mode 100644 index 00000000..5cf89d83 --- /dev/null +++ b/codeforlife/models/fields/decorators.py @@ -0,0 +1,24 @@ +""" +© Ocado Group +Created on 13/05/2026 at 12:17:05(+01:00). +""" + +from functools import wraps + +from ...types import PropertySetter, Validator + + +def validated_field_setter(*validators: Validator): + """Decorator to apply validators to a property setter method.""" + + def decorator(fset: PropertySetter) -> PropertySetter: + @wraps(fset) + def wrapped(instance, value): + for validator in validators: + validator(value) # should raise ValidationError if invalid + + return fset(instance, value) + + return wrapped + + return decorator diff --git a/codeforlife/types.py b/codeforlife/types.py index 2f660587..7a01abc2 100644 --- a/codeforlife/types.py +++ b/codeforlife/types.py @@ -21,7 +21,12 @@ DataDict = t.Dict[str, t.Any] OrderedDataDict = t.OrderedDict[str, t.Any] -Validators = t.Sequence[t.Callable] +PropertyGetter = t.Callable[[t.Any], t.Any] +PropertySetter = t.Callable[[t.Any, t.Any], None] +PropertyDeleter = t.Callable[[t.Any], None] + +Validator = t.Callable[[t.Any], None] +Validators = t.Sequence[Validator] LogLevel = t.Literal[ "CRITICAL", diff --git a/codeforlife/user/migrations/0004_client_side_encryption_part_2.py b/codeforlife/user/migrations/0004_client_side_encryption_part_2.py new file mode 100644 index 00000000..78303c37 --- /dev/null +++ b/codeforlife/user/migrations/0004_client_side_encryption_part_2.py @@ -0,0 +1,238 @@ +from django.db import migrations + +from ...models.fields import EncryptedTextField, Sha256Field + +user_migrations = [ + # Username + migrations.RemoveField( + model_name="user", + name="_username_plain", + ), + migrations.AlterField( + model_name="user", + name="_username_enc", + field=EncryptedTextField( + associated_data="username", + verbose_name="username", + db_column="username_enc", + ), + ), + migrations.AlterField( + model_name="user", + name="_username_hash", + field=Sha256Field( + unique=True, + editable=False, + max_length=64, + verbose_name="username hash", + db_column="username_hash", + ), + ), + # First name + migrations.RemoveField( + model_name="user", + name="_first_name_plain", + ), + migrations.AlterField( + model_name="user", + name="_first_name_enc", + field=EncryptedTextField( + associated_data="first_name", + verbose_name="first name", + db_column="first_name_enc", + ), + ), + migrations.AlterField( + model_name="user", + name="_first_name_hash", + field=Sha256Field( + editable=False, + max_length=64, + verbose_name="first name hash", + db_column="first_name_hash", + ), + ), + # Last name + migrations.RemoveField( + model_name="user", + name="_last_name_plain", + ), + migrations.AlterField( + model_name="user", + name="_last_name_enc", + field=EncryptedTextField( + associated_data="last_name", + verbose_name="last name", + db_column="last_name_enc", + ), + ), + # Email + migrations.RemoveField( + model_name="user", + name="_email_plain", + ), + migrations.AlterField( + model_name="user", + name="_email_enc", + field=EncryptedTextField( + associated_data="email", + verbose_name="email address", + db_column="email_enc", + ), + ), + migrations.AlterField( + model_name="user", + name="_email_hash", + field=Sha256Field( + editable=False, + max_length=64, + verbose_name="email hash", + db_column="email_hash", + ), + ), +] + +class_migrations = [ + # Name + migrations.RemoveField( + model_name="class", + name="_name_plain", + ), + migrations.AlterField( + model_name="class", + name="_name_enc", + field=EncryptedTextField( + associated_data="name", + verbose_name="name", + db_column="name_enc", + ), + ), + migrations.AddField( + model_name="class", + name="_name_hash", + field=Sha256Field( + null=True, + editable=False, + max_length=64, + verbose_name="name hash", + db_column="name_hash", + ), + ), + # Access code + migrations.RemoveField( + model_name="class", + name="_access_code_plain", + ), +] + +school_teacher_invitation_migrations = [ + # Token + migrations.RemoveField( + model_name="schoolteacherinvitation", + name="_token_plain", + ), + migrations.AlterField( + model_name="schoolteacherinvitation", + name="_token_enc", + field=EncryptedTextField( + associated_data="token", + verbose_name="token", + db_column="token_enc", + ), + ), + migrations.AlterField( + model_name="schoolteacherinvitation", + name="_token_hash", + field=Sha256Field( + editable=False, + max_length=64, + verbose_name="token hash", + db_column="token_hash", + ), + ), + # First name + migrations.RemoveField( + model_name="schoolteacherinvitation", + name="_invited_teacher_first_name_plain", + ), + migrations.AlterField( + model_name="schoolteacherinvitation", + name="_invited_teacher_first_name_enc", + field=EncryptedTextField( + associated_data="invited_teacher_first_name", + verbose_name="invited teacher first name", + db_column="invited_teacher_first_name_enc", + ), + ), + # Last name + migrations.RemoveField( + model_name="schoolteacherinvitation", + name="_invited_teacher_last_name_plain", + ), + migrations.AlterField( + model_name="schoolteacherinvitation", + name="_invited_teacher_last_name_enc", + field=EncryptedTextField( + associated_data="invited_teacher_last_name", + verbose_name="invited teacher last name", + db_column="invited_teacher_last_name_enc", + ), + ), + # Email + migrations.RemoveField( + model_name="schoolteacherinvitation", + name="_invited_teacher_email_plain", + ), + migrations.AlterField( + model_name="schoolteacherinvitation", + name="_invited_teacher_email_enc", + field=EncryptedTextField( + associated_data="invited_teacher_email", + verbose_name="invited teacher email", + db_column="invited_teacher_email_enc", + ), + ), +] + +school_migrations = [ + # Name + migrations.RemoveField( + model_name="school", + name="_name_plain", + ), + migrations.AlterField( + model_name="school", + name="_name_enc", + field=EncryptedTextField( + associated_data="name", + verbose_name="name", + db_column="name_enc", + ), + ), + migrations.AddField( + model_name="school", + name="_name_hash", + field=Sha256Field( + null=True, + unique=True, + editable=False, + max_length=64, + verbose_name="name hash", + db_column="name_hash", + ), + ), +] + + +class Migration(migrations.Migration): + + dependencies = [ + ("user", "0003_client_side_encryption_part_1"), + ] + + operations = [ + *user_migrations, + *class_migrations, + *school_teacher_invitation_migrations, + *school_migrations, + ] diff --git a/codeforlife/user/models/__init__.py b/codeforlife/user/models/__init__.py index bafb5754..1ce07249 100644 --- a/codeforlife/user/models/__init__.py +++ b/codeforlife/user/models/__init__.py @@ -4,7 +4,7 @@ """ from .auth_factor import AuthFactor -from .klass import Class, class_name_validators +from .klass import Class from .other import ( DailyActivity, JoinReleaseStudent, @@ -13,7 +13,7 @@ UserSession, ) from .otp_bypass_token import OtpBypassToken -from .school import School, school_name_validators +from .school import School from .session import Session from .session_auth_factor import SessionAuthFactor from .student import Independent, Student @@ -43,6 +43,4 @@ TypedUser, User, UserProfile, - user_first_name_validators, - user_last_name_validators, ) diff --git a/codeforlife/user/models/klass.py b/codeforlife/user/models/klass.py index 6896a930..24794acc 100644 --- a/codeforlife/user/models/klass.py +++ b/codeforlife/user/models/klass.py @@ -14,6 +14,7 @@ from ...models import EncryptedModel from ...models.fields import EncryptedTextField, Sha256Field +from ...models.fields.decorators import validated_field_setter from ...types import Validators from ...validators import ( UnicodeAlphanumericCharSetValidator, @@ -31,20 +32,6 @@ TypedModelMeta = object -class_access_code_validators: Validators = [ - MinLengthValidator(5), - MaxLengthValidator(5), - UppercaseAsciiAlphanumericCharSetValidator(), -] - -class_name_validators: Validators = [ - UnicodeAlphanumericCharSetValidator( - spaces=True, - special_chars="-_", - ) -] - - class ClassModelManager(EncryptedModel.Manager["Class"]): """Manager for Class model.""" @@ -65,38 +52,39 @@ class Class(EncryptedModel): associated_data = "class" field_aliases = { - "name": {"_name_plain", "_name_enc"}, - "access_code": { - "_access_code_plain", - "_access_code_enc", - "_access_code_hash", - }, + "name": {"_name_hash", "_name_enc"}, + "access_code": {"_access_code_enc", "_access_code_hash"}, } # -------------------------------------------------------------------------- # Name # -------------------------------------------------------------------------- - _name_plain: str - _name_plain = models.CharField(max_length=200) # type: ignore[assignment] + _name_hash = Sha256Field( + verbose_name=_("name hash"), + db_column="name_hash", + null=True, + ) _name_enc = EncryptedTextField( associated_data="name", db_column="name_enc", - null=True, verbose_name=_("name"), ) + name_validators: Validators = [ + MaxLengthValidator(200), + UnicodeAlphanumericCharSetValidator(spaces=True, special_chars="-_"), + ] + @property def name(self): """Get the name of the class.""" - if self._name_enc is not None: - return EncryptedTextField.get(self, "_name_enc") - return self._name_plain + return EncryptedTextField.get(self, "_name_enc") @name.setter + @validated_field_setter(*name_validators) def name(self, value: str): """Set the name of the class.""" - self._name_plain = value EncryptedTextField.set(self, value, "_name_enc") # -------------------------------------------------------------------------- @@ -117,11 +105,6 @@ def name(self, value: str): null=True, db_column="access_code_hash", ) - _access_code_plain: t.Optional[str] - _access_code_plain = models.CharField( # type: ignore[assignment] - max_length=5, - null=True, - ) _access_code_enc = EncryptedTextField( associated_data="access_code", null=True, @@ -129,17 +112,21 @@ def name(self, value: str): db_column="access_code_enc", ) + access_code_validators: Validators = [ + MinLengthValidator(5), + MaxLengthValidator(5), + UppercaseAsciiAlphanumericCharSetValidator(), + ] + @property def access_code(self): """Get the access code for the class.""" - if self._access_code_enc is not None: - return EncryptedTextField.get(self, "_access_code_enc") - return self._access_code_plain + return EncryptedTextField.get(self, "_access_code_enc") @access_code.setter + @validated_field_setter(*access_code_validators) def access_code(self, value: t.Optional[str]): """Set the access code for the class.""" - self._access_code_plain = value EncryptedTextField.set(self, value, "_access_code_enc") Sha256Field.set(self, value, "_access_code_hash") diff --git a/codeforlife/user/models/other.py b/codeforlife/user/models/other.py index ef135c26..f909e1a4 100644 --- a/codeforlife/user/models/other.py +++ b/codeforlife/user/models/other.py @@ -10,12 +10,16 @@ import typing as t from uuid import uuid4 +from django.core.validators import MaxLengthValidator from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ from ...models import EncryptedModel from ...models.fields import EncryptedTextField, Sha256Field +from ...models.fields.decorators import validated_field_setter +from ...types import Validators +from .user import User if t.TYPE_CHECKING: # pragma: no cover from datetime import datetime @@ -235,19 +239,10 @@ class SchoolTeacherInvitation(EncryptedModel): associated_data = "school_teacher_invitation" field_aliases = { - "token": {"_token_plain", "_token_enc", "_token_hash"}, - "invited_teacher_first_name": { - "_invited_teacher_first_name_plain", - "_invited_teacher_first_name_enc", - }, - "invited_teacher_last_name": { - "_invited_teacher_last_name_plain", - "_invited_teacher_last_name_enc", - }, - "invited_teacher_email": { - "_invited_teacher_email_plain", - "_invited_teacher_email_enc", - }, + "token": {"_token_enc", "_token_hash"}, + "invited_teacher_first_name": {"_invited_teacher_first_name_enc"}, + "invited_teacher_last_name": {"_invited_teacher_last_name_enc"}, + "invited_teacher_email": {"_invited_teacher_email_enc"}, } # -------------------------------------------------------------------------- @@ -256,29 +251,25 @@ class SchoolTeacherInvitation(EncryptedModel): _token_hash = Sha256Field( verbose_name=_("token hash"), - null=True, db_column="token_hash", ) - _token_plain: str - _token_plain = models.CharField(max_length=88) # type: ignore[assignment] _token_enc = EncryptedTextField( associated_data="token", - null=True, verbose_name=_("token"), db_column="token_enc", ) + token_validators: Validators = [MaxLengthValidator(88)] + @property def token(self): """Get the decrypted token value.""" - if self._token_enc is not None: - return EncryptedTextField.get(self, "_token_enc") - return self._token_plain + return EncryptedTextField.get(self, "_token_enc") @token.setter + @validated_field_setter(*token_validators) def token(self, value: str): """Sets the token value.""" - self._token_plain = value EncryptedTextField.set(self, value, "_token_enc") Sha256Field.set(self, value, "_token_hash") @@ -304,14 +295,8 @@ def token(self, value: str): # First name # -------------------------------------------------------------------------- - _invited_teacher_first_name_plain: str - # pylint: disable-next=line-too-long - _invited_teacher_first_name_plain = models.CharField( # type: ignore[assignment] - max_length=150 - ) # Same as User model _invited_teacher_first_name_enc = EncryptedTextField( associated_data="invited_teacher_first_name", - null=True, verbose_name=_("invited teacher first name"), db_column="invited_teacher_first_name_enc", ) @@ -319,30 +304,20 @@ def token(self, value: str): @property def invited_teacher_first_name(self): """Get the decrypted invited teacher first name value.""" - if self._invited_teacher_first_name_enc is not None: - return EncryptedTextField.get( - self, "_invited_teacher_first_name_enc" - ) - return self._invited_teacher_first_name_plain + return EncryptedTextField.get(self, "_invited_teacher_first_name_enc") @invited_teacher_first_name.setter + @validated_field_setter(*User.first_name_validators) def invited_teacher_first_name(self, value: str): """Sets the invited teacher first name value.""" - self._invited_teacher_first_name_plain = value EncryptedTextField.set(self, value, "_invited_teacher_first_name_enc") # -------------------------------------------------------------------------- # Last name # -------------------------------------------------------------------------- - _invited_teacher_last_name_plain: str - # pylint: disable-next=line-too-long - _invited_teacher_last_name_plain = models.CharField( # type: ignore[assignment] - max_length=150 - ) # Same as User model _invited_teacher_last_name_enc = EncryptedTextField( associated_data="invited_teacher_last_name", - null=True, verbose_name=_("invited teacher last name"), db_column="invited_teacher_last_name_enc", ) @@ -350,30 +325,20 @@ def invited_teacher_first_name(self, value: str): @property def invited_teacher_last_name(self): """Get the decrypted invited teacher last name value.""" - if self._invited_teacher_last_name_enc is not None: - return EncryptedTextField.get( - self, "_invited_teacher_last_name_enc" - ) - return self._invited_teacher_last_name_plain + return EncryptedTextField.get(self, "_invited_teacher_last_name_enc") @invited_teacher_last_name.setter + @validated_field_setter(*User.last_name_validators) def invited_teacher_last_name(self, value: str): """Sets the invited teacher last name value.""" - self._invited_teacher_last_name_plain = value EncryptedTextField.set(self, value, "_invited_teacher_last_name_enc") # -------------------------------------------------------------------------- # Email # -------------------------------------------------------------------------- - # TODO: Switch to a CharField to be able to hold hashed value - _invited_teacher_email_plain: str - _invited_teacher_email_plain = ( - models.EmailField() # type: ignore[assignment] - ) # Same as User model _invited_teacher_email_enc = EncryptedTextField( associated_data="invited_teacher_email", - null=True, verbose_name=_("invited teacher email"), db_column="invited_teacher_email_enc", ) @@ -381,14 +346,12 @@ def invited_teacher_last_name(self, value: str): @property def invited_teacher_email(self): """Get the decrypted invited teacher email value.""" - if self._invited_teacher_email_enc is not None: - return EncryptedTextField.get(self, "_invited_teacher_email_enc") - return self._invited_teacher_email_plain + return EncryptedTextField.get(self, "_invited_teacher_email_enc") @invited_teacher_email.setter + @validated_field_setter(*User.email_validators) def invited_teacher_email(self, value: str): """Sets the invited teacher email value.""" - self._invited_teacher_email_plain = value EncryptedTextField.set(self, value, "_invited_teacher_email_enc") # -------------------------------------------------------------------------- diff --git a/codeforlife/user/models/school.py b/codeforlife/user/models/school.py index 8fca15f9..dcb4bf82 100644 --- a/codeforlife/user/models/school.py +++ b/codeforlife/user/models/school.py @@ -5,13 +5,15 @@ import typing as t +from django.core.validators import MaxLengthValidator from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django_countries.fields import CountryField from ...models import DataEncryptionKeyModel -from ...models.fields import EncryptedTextField +from ...models.fields import EncryptedTextField, Sha256Field +from ...models.fields.decorators import validated_field_setter from ...types import Validators from ...validators import UnicodeAlphanumericCharSetValidator @@ -19,15 +21,6 @@ from datetime import datetime -# TODO: add to School.name field-validators in new schema. -school_name_validators: Validators = [ - UnicodeAlphanumericCharSetValidator( - spaces=True, - special_chars="'.", - ) -] - - class SchoolModelManager(DataEncryptionKeyModel.Manager["School"]): """Manager for School model.""" @@ -45,7 +38,7 @@ class School(DataEncryptionKeyModel): associated_data = "school" field_aliases = { - "name": {"_name_plain", "_name_enc"}, + "name": {"_name_hash", "_name_enc"}, } # -------------------------------------------------------------------------- @@ -53,29 +46,35 @@ class School(DataEncryptionKeyModel): # -------------------------------------------------------------------------- # pylint: disable=duplicate-code - _name_plain: str - _name_plain = models.CharField( # type: ignore[assignment] - max_length=200, + _name_hash = Sha256Field( + verbose_name=_("name hash"), + db_column="name_hash", unique=True, + null=True, ) _name_enc = EncryptedTextField( associated_data="name", - null=True, verbose_name=_("name"), db_column="name_enc", ) + name_validators: Validators = [ + MaxLengthValidator(200), + UnicodeAlphanumericCharSetValidator( + spaces=True, + special_chars="'.", + ), + ] + @property def name(self): """Get the school's name.""" - if self._name_enc is not None: - return EncryptedTextField.get(self, "_name_enc") - return self._name_plain + return EncryptedTextField.get(self, "_name_enc") @name.setter + @validated_field_setter(*name_validators) def name(self, value: str): """Set the school's name.""" - self._name_plain = value EncryptedTextField.set(self, value, "_name_enc") # pylint: enable=duplicate-code diff --git a/codeforlife/user/models/user/__init__.py b/codeforlife/user/models/user/__init__.py index adb027cb..eee8fc16 100644 --- a/codeforlife/user/models/user/__init__.py +++ b/codeforlife/user/models/user/__init__.py @@ -26,14 +26,7 @@ from .school_teacher import SchoolTeacherUser, SchoolTeacherUserManager from .student import StudentUser, StudentUserManager from .teacher import TeacherUser, TeacherUserManager -from .user import ( - AnyUser, - User, - UserManager, - UserProfile, - user_first_name_validators, - user_last_name_validators, -) +from .user import AnyUser, User, UserManager, UserProfile # pylint: disable-next=invalid-name TypedUser = t.Union[ diff --git a/codeforlife/user/models/user/user.py b/codeforlife/user/models/user/user.py index 343e4819..3a39fa02 100644 --- a/codeforlife/user/models/user/user.py +++ b/codeforlife/user/models/user/user.py @@ -12,6 +12,7 @@ from django.contrib.auth.models import PermissionsMixin from django.contrib.auth.models import UserManager as _UserManager from django.contrib.auth.validators import UnicodeUsernameValidator +from django.core.validators import MaxLengthValidator, validate_email from django.db import models from django.db.models.query import QuerySet from django.utils import timezone @@ -20,6 +21,7 @@ from ....models import AbstractBaseUser, DataEncryptionKeyModel from ....models.fields import EncryptedTextField, Sha256Field +from ....models.fields.decorators import validated_field_setter from ....types import Validators from ....validators import UnicodeAlphanumericCharSetValidator @@ -31,21 +33,6 @@ from ..teacher import Teacher -# TODO: add to model validators in new schema. -user_first_name_validators: Validators = [ - UnicodeAlphanumericCharSetValidator( - spaces=True, - special_chars="-'", - ) -] -user_last_name_validators: Validators = [ - UnicodeAlphanumericCharSetValidator( - spaces=True, - special_chars="-'", - ) -] - - AnyUser = t.TypeVar("AnyUser", bound="User") @@ -77,11 +64,11 @@ def _create_user_object( # def normalize_email(cls, email): # return super().normalize_email(email).lower() - # def get_by_natural_key(self, username): - # return self.get(_username_hash__sha256=username) + def get_by_natural_key(self, username): + return self.get(_username_hash__sha256=username) - # async def aget_by_natural_key(self, username): - # return await self.aget(_username_hash__sha256=username) + async def aget_by_natural_key(self, username): + return await self.aget(_username_hash__sha256=username) # pylint: enable=missing-function-docstring @@ -112,19 +99,15 @@ class User(AbstractBaseUser, PermissionsMixin, DataEncryptionKeyModel): associated_data = "user" field_aliases = { - "username": {"_username_plain", "_username_enc", "_username_hash"}, - "first_name": { - "_first_name_plain", - "_first_name_enc", - "_first_name_hash", - }, - "last_name": {"_last_name_plain", "_last_name_enc"}, - "email": {"_email_plain", "_email_enc", "_email_hash"}, + "username": {"_username_enc", "_username_hash"}, + "first_name": {"_first_name_enc", "_first_name_hash"}, + "last_name": {"_last_name_enc"}, + "email": {"_email_enc", "_email_hash"}, } - EMAIL_FIELD = "_email_plain" - USERNAME_FIELD = "_username_plain" - REQUIRED_FIELDS = ["_email_plain"] + EMAIL_FIELD = "email" + USERNAME_FIELD = "_username_hash" + REQUIRED_FIELDS = ["email"] credential_fields = frozenset(["email", "password"]) _password: t.Optional[str] @@ -144,39 +127,27 @@ class User(AbstractBaseUser, PermissionsMixin, DataEncryptionKeyModel): verbose_name=_("username hash"), db_column="username_hash", unique=True, - null=True, - ) - _username_plain = models.CharField( - _("username"), - max_length=150, - unique=True, - help_text=_( - "Required. 150 characters or fewer. " - "Letters, digits and @/./+/-/_ only." - ), - validators=[UnicodeUsernameValidator()], - error_messages={ - "unique": _("A user with that username already exists."), - }, ) _username_enc = EncryptedTextField( associated_data="username", db_column="username_enc", - null=True, verbose_name=_("username"), ) + username_validators: Validators = [ + MaxLengthValidator(150), + UnicodeUsernameValidator(), + ] + @property def username(self): """The user's username.""" - if self._username_enc is not None: - return EncryptedTextField.get(self, "_username_enc") - return self._username_plain + return EncryptedTextField.get(self, "_username_enc") @username.setter + @validated_field_setter(*username_validators) def username(self, value: str): """Set the user's username.""" - self._username_plain = value EncryptedTextField.set(self, value, "_username_enc") Sha256Field.set(self, value, "_username_hash") @@ -187,29 +158,30 @@ def username(self, value: str): _first_name_hash = Sha256Field( verbose_name=_("first name hash"), db_column="first_name_hash", - null=True, - ) - _first_name_plain = models.CharField( - _("first name"), max_length=150, blank=True ) _first_name_enc = EncryptedTextField( associated_data="first_name", db_column="first_name_enc", - null=True, verbose_name=_("first name"), ) + first_name_validators: Validators = [ + MaxLengthValidator(150), + UnicodeAlphanumericCharSetValidator( + spaces=True, + special_chars="-'", + ), + ] + @property def first_name(self): """The user's first name.""" - if self._first_name_enc is not None: - return EncryptedTextField.get(self, "_first_name_enc") - return self._first_name_plain + return EncryptedTextField.get(self, "_first_name_enc") @first_name.setter + @validated_field_setter(*first_name_validators) def first_name(self, value: str): """Set the user's first name.""" - self._first_name_plain = value EncryptedTextField.set(self, value, "_first_name_enc") Sha256Field.set(self, value, "_first_name_hash") @@ -217,27 +189,29 @@ def first_name(self, value: str): # Last name # -------------------------------------------------------------------------- - _last_name_plain = models.CharField( - _("last name"), max_length=150, blank=True - ) _last_name_enc = EncryptedTextField( associated_data="last_name", db_column="last_name_enc", - null=True, verbose_name=_("last name"), ) + last_name_validators: Validators = [ + MaxLengthValidator(150), + UnicodeAlphanumericCharSetValidator( + spaces=True, + special_chars="-'", + ), + ] + @property def last_name(self): """The user's last name.""" - if self._last_name_enc is not None: - return EncryptedTextField.get(self, "_last_name_enc") - return self._last_name_plain + return EncryptedTextField.get(self, "_last_name_enc") @last_name.setter + @validated_field_setter(*last_name_validators) def last_name(self, value: str): """Set the user's last name.""" - self._last_name_plain = value EncryptedTextField.set(self, value, "_last_name_enc") # -------------------------------------------------------------------------- @@ -247,28 +221,25 @@ def last_name(self, value: str): _email_hash = Sha256Field( verbose_name=_("email hash"), db_column="email_hash", - null=True, ) - _email_plain = models.EmailField(_("email address"), blank=True) _email_enc = EncryptedTextField( associated_data="email", db_column="email_enc", - null=True, verbose_name=_("email address"), ) + email_validators: Validators = [validate_email, MaxLengthValidator(254)] + @property def email(self): """The user's email address.""" - if self._email_enc is not None: - return EncryptedTextField.get(self, "_email_enc") - return self._email_plain + return EncryptedTextField.get(self, "_email_enc") @email.setter + @validated_field_setter(*email_validators) def email(self, value: str): """Set the user's email address.""" value = self.__class__.objects.normalize_email(value) - self._email_plain = value EncryptedTextField.set(self, value, "_email_enc") Sha256Field.set(self, value, "_email_hash") diff --git a/codeforlife/user/serializers/klass.py b/codeforlife/user/serializers/klass.py index 42b212cb..33ba8b2f 100644 --- a/codeforlife/user/serializers/klass.py +++ b/codeforlife/user/serializers/klass.py @@ -8,7 +8,6 @@ from ...serializers import ModelSerializer from ..models import Class from ..models import User as RequestUser -from ..models import class_name_validators # pylint: disable=missing-class-docstring # pylint: disable=too-many-ancestors @@ -22,7 +21,7 @@ class ClassSerializer(ModelSerializer[RequestUser, Class]): # TODO: add to model validators in new schema. name = serializers.CharField( - validators=class_name_validators, + validators=Class.name_validators, max_length=200, read_only=True, ) diff --git a/codeforlife/user/serializers/school.py b/codeforlife/user/serializers/school.py index 13542019..b070fac9 100644 --- a/codeforlife/user/serializers/school.py +++ b/codeforlife/user/serializers/school.py @@ -8,7 +8,6 @@ from ...serializers import ModelSerializer from ..models import School from ..models import User as RequestUser -from ..models import school_name_validators # pylint: disable=missing-class-docstring # pylint: disable=too-many-ancestors @@ -17,7 +16,7 @@ class SchoolSerializer(ModelSerializer[RequestUser, School]): # TODO: add to model validators in new schema. name = serializers.CharField( - validators=school_name_validators, + validators=School.name_validators, max_length=200, read_only=True, ) diff --git a/codeforlife/user/serializers/user.py b/codeforlife/user/serializers/user.py index 3109f61b..5daffe2a 100644 --- a/codeforlife/user/serializers/user.py +++ b/codeforlife/user/serializers/user.py @@ -8,14 +8,7 @@ from rest_framework import serializers from ...serializers import ModelSerializer -from ..models import ( - AnyUser, - Student, - Teacher, - User, - user_first_name_validators, - user_last_name_validators, -) +from ..models import AnyUser, Student, Teacher, User from .student import StudentSerializer from .teacher import TeacherSerializer @@ -30,14 +23,14 @@ class BaseUserSerializer( ): # TODO: add to model validators in new schema. first_name = serializers.CharField( - validators=user_first_name_validators, + validators=User.first_name_validators, max_length=150, read_only=True, ) # TODO: add to model validators in new schema. last_name = serializers.CharField( - validators=user_last_name_validators, + validators=User.last_name_validators, max_length=150, read_only=True, ) From 80328376e310f7b2c1a76edd339eba042c9a19e3 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 14 May 2026 09:46:12 +0000 Subject: [PATCH 2/7] remove `_plain` queries --- codeforlife/legacy/helpers/emails.py | 4 ++-- codeforlife/legacy/helpers/generators.py | 14 ++++++++++---- codeforlife/legacy/tests/test_models.py | 10 ++++++---- codeforlife/legacy/tests/utils/classes.py | 2 +- codeforlife/legacy/tests/utils/organisation.py | 6 +++--- codeforlife/legacy/tests/utils/student.py | 4 ++-- codeforlife/legacy/tests/utils/user.py | 2 +- codeforlife/user/models/student.py | 2 +- codeforlife/user/models/user/contactable.py | 5 +---- 9 files changed, 27 insertions(+), 22 deletions(-) diff --git a/codeforlife/legacy/helpers/emails.py b/codeforlife/legacy/helpers/emails.py index 9126c8d6..365aa43c 100644 --- a/codeforlife/legacy/helpers/emails.py +++ b/codeforlife/legacy/helpers/emails.py @@ -378,7 +378,7 @@ def update_indy_email(user, request, data): if new_email != "" and new_email != user.email: changing_email = True - users_with_email = User.objects.filter(_email_plain=new_email) + users_with_email = User.objects.filter(_email_hash__sha256=new_email) send_dotdigital_email( campaign_ids["email_change_notification"], @@ -399,7 +399,7 @@ def update_email(user: Teacher or Student, request, data): if new_email != "" and new_email != user.new_user.email: changing_email = True - users_with_email = User.objects.filter(_email_plain=new_email) + users_with_email = User.objects.filter(_email_hash__sha256=new_email) send_dotdigital_email( campaign_ids["email_change_notification"], diff --git a/codeforlife/legacy/helpers/generators.py b/codeforlife/legacy/helpers/generators.py index 5ef608a6..e509413a 100644 --- a/codeforlife/legacy/helpers/generators.py +++ b/codeforlife/legacy/helpers/generators.py @@ -16,19 +16,23 @@ def get_random_username(): while True: random_username = uuid4().hex[:30] # generate a random username - if not User.objects.filter(_username_plain=random_username).exists(): + if not User.objects.filter( + _username_hash__sha256=random_username + ).exists(): return random_username def generate_new_student_name(orig_name): - if not Student.objects.filter(new_user___username_plain=orig_name).exists(): + if not Student.objects.filter( + new_user___username_hash__sha256=orig_name + ).exists(): return orig_name i = 1 while True: new_name = orig_name + str(i) if not Student.objects.filter( - new_user___username_plain=new_name + new_user___username_hash__sha256=new_name ).exists(): return new_name i += 1 @@ -40,7 +44,9 @@ def generate_access_code(): random.choice(string.ascii_uppercase) for _ in range(5) ) - if not Class.objects.filter(_access_code_plain=access_code).exists(): + if not Class.objects.filter( + _access_code_hash__sha256=access_code + ).exists(): return access_code diff --git a/codeforlife/legacy/tests/test_models.py b/codeforlife/legacy/tests/test_models.py index 05bf8027..2c2ac65f 100644 --- a/codeforlife/legacy/tests/test_models.py +++ b/codeforlife/legacy/tests/test_models.py @@ -39,7 +39,9 @@ def test_indep_student_pending_class_request_on_delete(self): klass.anonymise() - indep_student = Student.objects.get(new_user___username_plain=username) + indep_student = Student.objects.get( + new_user___username_hash__sha256=username + ) assert indep_student.pending_class_request is None @@ -68,9 +70,9 @@ def test_school_admins(self): join_teacher_to_organisation(email2, school.name) join_teacher_to_organisation(email3, school.name, is_admin=True) - teacher1 = Teacher.objects.get(new_user___username_plain=email1) - teacher2 = Teacher.objects.get(new_user___username_plain=email2) - teacher3 = Teacher.objects.get(new_user___username_plain=email3) + teacher1 = Teacher.objects.get(new_user___username_hash__sha256=email1) + teacher2 = Teacher.objects.get(new_user___username_hash__sha256=email2) + teacher3 = Teacher.objects.get(new_user___username_hash__sha256=email3) assert len(school.admins()) == 2 assert teacher1 in school.admins() diff --git a/codeforlife/legacy/tests/utils/classes.py b/codeforlife/legacy/tests/utils/classes.py index 8762d83a..52695790 100644 --- a/codeforlife/legacy/tests/utils/classes.py +++ b/codeforlife/legacy/tests/utils/classes.py @@ -36,7 +36,7 @@ def create_class_directly( if class_name is not None: name = class_name - teacher = Teacher.objects.get(new_user___email_plain=teacher_email) + teacher = Teacher.objects.get(new_user___email_hash__sha256=teacher_email) klass = Class.objects.create( name=name, access_code=access_code, teacher=teacher diff --git a/codeforlife/legacy/tests/utils/organisation.py b/codeforlife/legacy/tests/utils/organisation.py index 7c0aed99..8b33845b 100644 --- a/codeforlife/legacy/tests/utils/organisation.py +++ b/codeforlife/legacy/tests/utils/organisation.py @@ -20,7 +20,7 @@ def create_organisation_directly(teacher_email, **kwargs): school = School.objects.create(name=name, country="GB") - teacher = Teacher.objects.get(new_user___email_plain=teacher_email) + teacher = Teacher.objects.get(new_user___email_hash__sha256=teacher_email) teacher.school = school teacher.is_admin = True teacher.save() @@ -29,8 +29,8 @@ def create_organisation_directly(teacher_email, **kwargs): def join_teacher_to_organisation(teacher_email, org_name, is_admin=False): - teacher = Teacher.objects.get(new_user___email_plain=teacher_email) - school = School.objects.get(_name_plain=org_name) + teacher = Teacher.objects.get(new_user___email_hash__sha256=teacher_email) + school = School.objects.get(_name_hash__sha256=org_name) teacher.school = school teacher.is_admin = is_admin diff --git a/codeforlife/legacy/tests/utils/student.py b/codeforlife/legacy/tests/utils/student.py index 2885abf6..7c0935f3 100644 --- a/codeforlife/legacy/tests/utils/student.py +++ b/codeforlife/legacy/tests/utils/student.py @@ -34,7 +34,7 @@ def create_school_student_directly(access_code) -> Tuple[str, str, Student]: """ name, password = generate_school_details() - klass = Class.objects.get(_access_code_plain=access_code) + klass = Class.objects.get(_access_code_hash__sha256=access_code) student = Student.objects.schoolFactory(klass, name, password) return name, password, student @@ -42,7 +42,7 @@ def create_school_student_directly(access_code) -> Tuple[str, str, Student]: def create_student_with_direct_login(access_code) -> Tuple[Student, str]: name, password = generate_school_details() - klass = Class.objects.get(_access_code_plain=access_code) + klass = Class.objects.get(_access_code_hash__sha256=access_code) # use random string for direct login) login_id, hashed_login_id = generate_login_id() diff --git a/codeforlife/legacy/tests/utils/user.py b/codeforlife/legacy/tests/utils/user.py index 3d804ceb..b36c65f6 100644 --- a/codeforlife/legacy/tests/utils/user.py +++ b/codeforlife/legacy/tests/utils/user.py @@ -10,7 +10,7 @@ def get_superuser(): """Get a superuser for testing, or create one if there isn't one.""" try: - return User.objects.get(_username_plain="superuser") + return User.objects.get(_username_hash__sha256="superuser") except User.DoesNotExist: return User.objects.create_superuser( "superuser", "superuser@codeforlife.education", "password" diff --git a/codeforlife/user/models/student.py b/codeforlife/user/models/student.py index 2a175a82..082bbd2b 100644 --- a/codeforlife/user/models/student.py +++ b/codeforlife/user/models/student.py @@ -33,7 +33,7 @@ def get_random_username(self): while True: random_username = uuid4().hex[:30] # generate a random username if not User.objects.filter( - _username_plain=random_username + _username_hash__sha256=random_username ).exists(): return random_username diff --git a/codeforlife/user/models/user/contactable.py b/codeforlife/user/models/user/contactable.py index 5b5c52f4..8669d48d 100644 --- a/codeforlife/user/models/user/contactable.py +++ b/codeforlife/user/models/user/contactable.py @@ -22,10 +22,7 @@ # pylint: disable-next=missing-class-docstring,too-few-public-methods class ContactableUserManager(UserManager[AnyUser], t.Generic[AnyUser]): - def filter_users(self, queryset: QuerySet[User]): - return queryset.exclude(_email_plain__isnull=True).exclude( - _email_plain="" - ) + pass # pylint: disable-next=too-many-ancestors From 964f66818655008d8761ba2150d6ae743693a055 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 14 May 2026 10:13:29 +0000 Subject: [PATCH 3/7] break migration into 2 parts --- .../0004_client_side_encryption_part_2.py | 250 ++---------------- .../0005_client_side_encryption_part_3.py | 236 +++++++++++++++++ codeforlife/user/models/klass.py | 1 - codeforlife/user/models/school.py | 1 - 4 files changed, 260 insertions(+), 228 deletions(-) create mode 100644 codeforlife/user/migrations/0005_client_side_encryption_part_3.py diff --git a/codeforlife/user/migrations/0004_client_side_encryption_part_2.py b/codeforlife/user/migrations/0004_client_side_encryption_part_2.py index 78303c37..73df7857 100644 --- a/codeforlife/user/migrations/0004_client_side_encryption_part_2.py +++ b/codeforlife/user/migrations/0004_client_side_encryption_part_2.py @@ -1,227 +1,6 @@ from django.db import migrations -from ...models.fields import EncryptedTextField, Sha256Field - -user_migrations = [ - # Username - migrations.RemoveField( - model_name="user", - name="_username_plain", - ), - migrations.AlterField( - model_name="user", - name="_username_enc", - field=EncryptedTextField( - associated_data="username", - verbose_name="username", - db_column="username_enc", - ), - ), - migrations.AlterField( - model_name="user", - name="_username_hash", - field=Sha256Field( - unique=True, - editable=False, - max_length=64, - verbose_name="username hash", - db_column="username_hash", - ), - ), - # First name - migrations.RemoveField( - model_name="user", - name="_first_name_plain", - ), - migrations.AlterField( - model_name="user", - name="_first_name_enc", - field=EncryptedTextField( - associated_data="first_name", - verbose_name="first name", - db_column="first_name_enc", - ), - ), - migrations.AlterField( - model_name="user", - name="_first_name_hash", - field=Sha256Field( - editable=False, - max_length=64, - verbose_name="first name hash", - db_column="first_name_hash", - ), - ), - # Last name - migrations.RemoveField( - model_name="user", - name="_last_name_plain", - ), - migrations.AlterField( - model_name="user", - name="_last_name_enc", - field=EncryptedTextField( - associated_data="last_name", - verbose_name="last name", - db_column="last_name_enc", - ), - ), - # Email - migrations.RemoveField( - model_name="user", - name="_email_plain", - ), - migrations.AlterField( - model_name="user", - name="_email_enc", - field=EncryptedTextField( - associated_data="email", - verbose_name="email address", - db_column="email_enc", - ), - ), - migrations.AlterField( - model_name="user", - name="_email_hash", - field=Sha256Field( - editable=False, - max_length=64, - verbose_name="email hash", - db_column="email_hash", - ), - ), -] - -class_migrations = [ - # Name - migrations.RemoveField( - model_name="class", - name="_name_plain", - ), - migrations.AlterField( - model_name="class", - name="_name_enc", - field=EncryptedTextField( - associated_data="name", - verbose_name="name", - db_column="name_enc", - ), - ), - migrations.AddField( - model_name="class", - name="_name_hash", - field=Sha256Field( - null=True, - editable=False, - max_length=64, - verbose_name="name hash", - db_column="name_hash", - ), - ), - # Access code - migrations.RemoveField( - model_name="class", - name="_access_code_plain", - ), -] - -school_teacher_invitation_migrations = [ - # Token - migrations.RemoveField( - model_name="schoolteacherinvitation", - name="_token_plain", - ), - migrations.AlterField( - model_name="schoolteacherinvitation", - name="_token_enc", - field=EncryptedTextField( - associated_data="token", - verbose_name="token", - db_column="token_enc", - ), - ), - migrations.AlterField( - model_name="schoolteacherinvitation", - name="_token_hash", - field=Sha256Field( - editable=False, - max_length=64, - verbose_name="token hash", - db_column="token_hash", - ), - ), - # First name - migrations.RemoveField( - model_name="schoolteacherinvitation", - name="_invited_teacher_first_name_plain", - ), - migrations.AlterField( - model_name="schoolteacherinvitation", - name="_invited_teacher_first_name_enc", - field=EncryptedTextField( - associated_data="invited_teacher_first_name", - verbose_name="invited teacher first name", - db_column="invited_teacher_first_name_enc", - ), - ), - # Last name - migrations.RemoveField( - model_name="schoolteacherinvitation", - name="_invited_teacher_last_name_plain", - ), - migrations.AlterField( - model_name="schoolteacherinvitation", - name="_invited_teacher_last_name_enc", - field=EncryptedTextField( - associated_data="invited_teacher_last_name", - verbose_name="invited teacher last name", - db_column="invited_teacher_last_name_enc", - ), - ), - # Email - migrations.RemoveField( - model_name="schoolteacherinvitation", - name="_invited_teacher_email_plain", - ), - migrations.AlterField( - model_name="schoolteacherinvitation", - name="_invited_teacher_email_enc", - field=EncryptedTextField( - associated_data="invited_teacher_email", - verbose_name="invited teacher email", - db_column="invited_teacher_email_enc", - ), - ), -] - -school_migrations = [ - # Name - migrations.RemoveField( - model_name="school", - name="_name_plain", - ), - migrations.AlterField( - model_name="school", - name="_name_enc", - field=EncryptedTextField( - associated_data="name", - verbose_name="name", - db_column="name_enc", - ), - ), - migrations.AddField( - model_name="school", - name="_name_hash", - field=Sha256Field( - null=True, - unique=True, - editable=False, - max_length=64, - verbose_name="name hash", - db_column="name_hash", - ), - ), -] +from ...models.fields import Sha256Field class Migration(migrations.Migration): @@ -231,8 +10,27 @@ class Migration(migrations.Migration): ] operations = [ - *user_migrations, - *class_migrations, - *school_teacher_invitation_migrations, - *school_migrations, + migrations.AddField( + model_name="school", + name="_name_hash", + field=Sha256Field( + null=True, + unique=True, + editable=False, + max_length=64, + verbose_name="name hash", + db_column="name_hash", + ), + ), + migrations.AddField( + model_name="class", + name="_name_hash", + field=Sha256Field( + null=True, + editable=False, + max_length=64, + verbose_name="name hash", + db_column="name_hash", + ), + ), ] diff --git a/codeforlife/user/migrations/0005_client_side_encryption_part_3.py b/codeforlife/user/migrations/0005_client_side_encryption_part_3.py new file mode 100644 index 00000000..0b84122b --- /dev/null +++ b/codeforlife/user/migrations/0005_client_side_encryption_part_3.py @@ -0,0 +1,236 @@ +from django.db import migrations + +from ...models.fields import EncryptedTextField, Sha256Field + +user_migrations = [ + # Username + migrations.RemoveField( + model_name="user", + name="_username_plain", + ), + migrations.AlterField( + model_name="user", + name="_username_enc", + field=EncryptedTextField( + associated_data="username", + verbose_name="username", + db_column="username_enc", + ), + ), + migrations.AlterField( + model_name="user", + name="_username_hash", + field=Sha256Field( + unique=True, + editable=False, + max_length=64, + verbose_name="username hash", + db_column="username_hash", + ), + ), + # First name + migrations.RemoveField( + model_name="user", + name="_first_name_plain", + ), + migrations.AlterField( + model_name="user", + name="_first_name_enc", + field=EncryptedTextField( + associated_data="first_name", + verbose_name="first name", + db_column="first_name_enc", + ), + ), + migrations.AlterField( + model_name="user", + name="_first_name_hash", + field=Sha256Field( + editable=False, + max_length=64, + verbose_name="first name hash", + db_column="first_name_hash", + ), + ), + # Last name + migrations.RemoveField( + model_name="user", + name="_last_name_plain", + ), + migrations.AlterField( + model_name="user", + name="_last_name_enc", + field=EncryptedTextField( + associated_data="last_name", + verbose_name="last name", + db_column="last_name_enc", + ), + ), + # Email + migrations.RemoveField( + model_name="user", + name="_email_plain", + ), + migrations.AlterField( + model_name="user", + name="_email_enc", + field=EncryptedTextField( + associated_data="email", + verbose_name="email address", + db_column="email_enc", + ), + ), + migrations.AlterField( + model_name="user", + name="_email_hash", + field=Sha256Field( + editable=False, + max_length=64, + verbose_name="email hash", + db_column="email_hash", + ), + ), +] + +class_migrations = [ + # Name + migrations.RemoveField( + model_name="class", + name="_name_plain", + ), + migrations.AlterField( + model_name="class", + name="_name_enc", + field=EncryptedTextField( + associated_data="name", + verbose_name="name", + db_column="name_enc", + ), + ), + migrations.AlterField( + model_name="class", + name="_name_hash", + field=Sha256Field( + editable=False, + max_length=64, + verbose_name="name hash", + db_column="name_hash", + ), + ), + # Access code + migrations.RemoveField( + model_name="class", + name="_access_code_plain", + ), +] + +school_teacher_invitation_migrations = [ + # Token + migrations.RemoveField( + model_name="schoolteacherinvitation", + name="_token_plain", + ), + migrations.AlterField( + model_name="schoolteacherinvitation", + name="_token_enc", + field=EncryptedTextField( + associated_data="token", + verbose_name="token", + db_column="token_enc", + ), + ), + migrations.AlterField( + model_name="schoolteacherinvitation", + name="_token_hash", + field=Sha256Field( + editable=False, + max_length=64, + verbose_name="token hash", + db_column="token_hash", + ), + ), + # First name + migrations.RemoveField( + model_name="schoolteacherinvitation", + name="_invited_teacher_first_name_plain", + ), + migrations.AlterField( + model_name="schoolteacherinvitation", + name="_invited_teacher_first_name_enc", + field=EncryptedTextField( + associated_data="invited_teacher_first_name", + verbose_name="invited teacher first name", + db_column="invited_teacher_first_name_enc", + ), + ), + # Last name + migrations.RemoveField( + model_name="schoolteacherinvitation", + name="_invited_teacher_last_name_plain", + ), + migrations.AlterField( + model_name="schoolteacherinvitation", + name="_invited_teacher_last_name_enc", + field=EncryptedTextField( + associated_data="invited_teacher_last_name", + verbose_name="invited teacher last name", + db_column="invited_teacher_last_name_enc", + ), + ), + # Email + migrations.RemoveField( + model_name="schoolteacherinvitation", + name="_invited_teacher_email_plain", + ), + migrations.AlterField( + model_name="schoolteacherinvitation", + name="_invited_teacher_email_enc", + field=EncryptedTextField( + associated_data="invited_teacher_email", + verbose_name="invited teacher email", + db_column="invited_teacher_email_enc", + ), + ), +] + +school_migrations = [ + # Name + migrations.RemoveField( + model_name="school", + name="_name_plain", + ), + migrations.AlterField( + model_name="school", + name="_name_enc", + field=EncryptedTextField( + associated_data="name", + verbose_name="name", + db_column="name_enc", + ), + ), + migrations.AlterField( + model_name="school", + name="_name_hash", + field=Sha256Field( + unique=True, + editable=False, + max_length=64, + verbose_name="name hash", + db_column="name_hash", + ), + ), +] + + +class Migration(migrations.Migration): + + dependencies = [ + ("user", "0004_client_side_encryption_part_2"), + ] + + operations = [ + *user_migrations, + *class_migrations, + *school_teacher_invitation_migrations, + *school_migrations, + ] diff --git a/codeforlife/user/models/klass.py b/codeforlife/user/models/klass.py index 24794acc..e8e139f4 100644 --- a/codeforlife/user/models/klass.py +++ b/codeforlife/user/models/klass.py @@ -63,7 +63,6 @@ class Class(EncryptedModel): _name_hash = Sha256Field( verbose_name=_("name hash"), db_column="name_hash", - null=True, ) _name_enc = EncryptedTextField( associated_data="name", diff --git a/codeforlife/user/models/school.py b/codeforlife/user/models/school.py index dcb4bf82..08da7021 100644 --- a/codeforlife/user/models/school.py +++ b/codeforlife/user/models/school.py @@ -50,7 +50,6 @@ class School(DataEncryptionKeyModel): verbose_name=_("name hash"), db_column="name_hash", unique=True, - null=True, ) _name_enc = EncryptedTextField( associated_data="name", From abb5d21f54acfa2f2aa7d12a7f02e159ccb8e3b5 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 14 May 2026 10:21:45 +0000 Subject: [PATCH 4/7] make token unique --- .../migrations/0004_client_side_encryption_part_2.py | 12 ++++++++++++ .../migrations/0005_client_side_encryption_part_3.py | 1 + codeforlife/user/models/other.py | 1 + 3 files changed, 14 insertions(+) diff --git a/codeforlife/user/migrations/0004_client_side_encryption_part_2.py b/codeforlife/user/migrations/0004_client_side_encryption_part_2.py index 73df7857..6bebcfbc 100644 --- a/codeforlife/user/migrations/0004_client_side_encryption_part_2.py +++ b/codeforlife/user/migrations/0004_client_side_encryption_part_2.py @@ -10,6 +10,18 @@ class Migration(migrations.Migration): ] operations = [ + migrations.AlterField( + model_name="schoolteacherinvitation", + name="_token_hash", + field=Sha256Field( + null=True, + db_column="token_hash", + editable=False, + max_length=64, + unique=True, + verbose_name="token hash", + ), + ), migrations.AddField( model_name="school", name="_name_hash", diff --git a/codeforlife/user/migrations/0005_client_side_encryption_part_3.py b/codeforlife/user/migrations/0005_client_side_encryption_part_3.py index 0b84122b..1a07e61d 100644 --- a/codeforlife/user/migrations/0005_client_side_encryption_part_3.py +++ b/codeforlife/user/migrations/0005_client_side_encryption_part_3.py @@ -143,6 +143,7 @@ model_name="schoolteacherinvitation", name="_token_hash", field=Sha256Field( + unique=True, editable=False, max_length=64, verbose_name="token hash", diff --git a/codeforlife/user/models/other.py b/codeforlife/user/models/other.py index f909e1a4..e57b214a 100644 --- a/codeforlife/user/models/other.py +++ b/codeforlife/user/models/other.py @@ -252,6 +252,7 @@ class SchoolTeacherInvitation(EncryptedModel): _token_hash = Sha256Field( verbose_name=_("token hash"), db_column="token_hash", + unique=True, ) _token_enc = EncryptedTextField( associated_data="token", From e024d8660c0925c8ef34d4709a17471e63531bbe Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 14 May 2026 15:38:17 +0000 Subject: [PATCH 5/7] remove plain fields --- codeforlife/user/fixtures/google_users.json | 4 - codeforlife/user/fixtures/independent.json | 8 -- codeforlife/user/fixtures/legacy.json | 86 ------------------- .../user/fixtures/non_school_teacher.json | 8 -- codeforlife/user/fixtures/school_1.json | 19 ---- codeforlife/user/fixtures/school_2.json | 13 --- codeforlife/user/fixtures/school_3.json | 9 -- 7 files changed, 147 deletions(-) diff --git a/codeforlife/user/fixtures/google_users.json b/codeforlife/user/fixtures/google_users.json index c9d02b8b..f7b6a397 100644 --- a/codeforlife/user/fixtures/google_users.json +++ b/codeforlife/user/fixtures/google_users.json @@ -5,15 +5,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6yMZoeJAUbdxMaVb2rDtkRymEfNCh6Z57unJt0wfyDhpR2SuwDh6iqEWHcwKOiTLFSd2PBjTXnQ==", "_email_hash": "ee95f43c0012fa1a9d5771313a7034cf94af568b0588b34ca20c34c25701af78", - "_email_plain": "google.teacher@noschool.com", "_first_name_enc": "ZmFrZV9lbmM6KXn5yJbvaIAq1O8qATyemFxIK7GJRkWh1tdv/QaBc/4PQw==", "_first_name_hash": "5463f3f79e73077256f212d9e43a2752447cb7b238cda23004926bc0b9c5076f", - "_first_name_plain": "Google", "_last_name_enc": "ZmFrZV9lbmM6DYRgnaINtnv2v6s09apDac1iGUCekmo7k4MuZ5TVKwhWdUE=", - "_last_name_plain": "Teacher", "_username_enc": "ZmFrZV9lbmM6neI4BdO9uaVQxpJirgXjv1zaBdn5G850jNO4G+yLhIFggC2qe6BUBfvGOIfPHcRpOLNjeC/eVA==", "_username_hash": "ee95f43c0012fa1a9d5771313a7034cf94af568b0588b34ca20c34c25701af78", - "_username_plain": "google.teacher@noschool.com", "dek": "ZmFrZV9lbmM658n6erabwmKyOMZuC5pc34LlWgk2C+OounRdomC5y4HgPHs82e9t36Ht5XDh3oMcduegpgH6KtzPYofw", "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" } diff --git a/codeforlife/user/fixtures/independent.json b/codeforlife/user/fixtures/independent.json index 67feca8b..5d00cca9 100644 --- a/codeforlife/user/fixtures/independent.json +++ b/codeforlife/user/fixtures/independent.json @@ -5,15 +5,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6BbOMTZgfEvfGWqqJMXewiatccqnrYAwXP0TL2Yg0XcRCkDjU88u6fN7m92841onGLZd38g==", "_email_hash": "8efc7683cec3a31792d0c22f49a1cc374ba2ae2199b3cb8d228a7cbabe8370b2", - "_email_plain": "indy.requester@email.com", "_first_name_enc": "ZmFrZV9lbmM6pCLIN/sWZNWKMxn/nDrhIxPqZJOvgjlMCMMbSP6LavQ=", "_first_name_hash": "c90d2a9004eccdad8bec752f90ffe656955c44a81f984d3d32b827f17b473d7c", - "_first_name_plain": "Indy", "_last_name_enc": "ZmFrZV9lbmM6/3o0FjosbmbS1oVTg10ezTJdBjsJBeuKxIHRFNBmZPASWT+9dA==", - "_last_name_plain": "Requester", "_username_enc": "ZmFrZV9lbmM6JGWj0OAH3zI4LTiClJre31xNzlULtOKIzFOS6JgTsFfMkeDtW6VSWme6PwPwn294DfkYbw==", "_username_hash": "8efc7683cec3a31792d0c22f49a1cc374ba2ae2199b3cb8d228a7cbabe8370b2", - "_username_plain": "indy.requester@email.com", "dek": "ZmFrZV9lbmM6L2LWK8EitcxU86uC69XSb5nbOso2Hsy7FjBH14+a3er944CPFMfyBfV0x3Hs3fxjYHMqP/hUpoPxRx6g", "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" } @@ -41,15 +37,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6cGgLFQ0NIcL8dvu9xyEsz5Stwyvl7Qp9OYPPnCQAv/+ZO6yF2vjR6iI1", "_email_hash": "63e90930d7617f596f1006362382d81aecb8308c5a9893007f78c50e954e6d1a", - "_email_plain": "indy@email.com", "_first_name_enc": "ZmFrZV9lbmM6P0Fsz+sx2cuXNgTPn0AoikMvFz67Uy2F6X+I2kCPTOg=", "_first_name_hash": "c90d2a9004eccdad8bec752f90ffe656955c44a81f984d3d32b827f17b473d7c", - "_first_name_plain": "Indy", "_last_name_enc": "ZmFrZV9lbmM6aOhhzg9mD3C0ROh1lCuDC1XKskZ6DYh6ajmv7jRUFFx4GyBY4w==", - "_last_name_plain": "NoRequest", "_username_enc": "ZmFrZV9lbmM6Mex03gOlRJsMhPARyGhxY10G8lrOTR0l7LSV3AeH2IgllqHHMkd8fkvB", "_username_hash": "63e90930d7617f596f1006362382d81aecb8308c5a9893007f78c50e954e6d1a", - "_username_plain": "indy@email.com", "dek": "ZmFrZV9lbmM6kNpc5tLn74rFhZeIxmToumntwifCY5oqPfrL3VTp7Xa962lNlx3jEdSyUbn2WjHaAHLrKWINQQX9f6dp", "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" } diff --git a/codeforlife/user/fixtures/legacy.json b/codeforlife/user/fixtures/legacy.json index 8b7e56ba..2ab706ec 100644 --- a/codeforlife/user/fixtures/legacy.json +++ b/codeforlife/user/fixtures/legacy.json @@ -236,7 +236,6 @@ "fields": { "_name_enc": "ZmFrZV9lbmM6OZ5fpw/7lnlu6VCmZ1D9j/ypiKMWMpPS4xgpUVojgcT7kmoAI8VUeao08V7yLsHnE+SyTew=", "_name_hash": "de785da99b1a6796945de0ead083eb3990fee70371a7d998682b450429ef8c1e", - "_name_plain": "Swiss Federal Polytechnic", "country": "GB", "county": "nan", "creation_time": null, @@ -298,10 +297,8 @@ "fields": { "_access_code_enc": "ZmFrZV9lbmM6yepC98pMREdly4JPlDeSOnh4SE5txcFKptSmTM+RP03F", "_access_code_hash": "7dcbfe80fd523896acdad8310fe14d804dad5ae7e4dc4975815f3d5ff76aa963", - "_access_code_plain": "AB123", "_name_enc": "ZmFrZV9lbmM6rfVJBo3PCRlQYodcqpqSFxZp7dYSoex1a8m6nuPs+pP1xkSkBg==", "_name_hash": "9640c03014663d3047ca86523c62baf8e3cbb208a08c6f839ce649b1aac341d2", - "_name_plain": "Class 101", "accept_requests_until": null, "always_accept_requests": true, "classmates_data_viewable": true, @@ -317,10 +314,8 @@ "fields": { "_access_code_enc": "ZmFrZV9lbmM633mwB2JzuTaoAcD+HEj6ZQUYY9ZM2Ch0gwrBtbawUJce", "_access_code_hash": "ee16d5af3f31578ab0327da3257b212fe2beefcadef4f6b8ad6c992d153b90aa", - "_access_code_plain": "AB124", "_name_enc": "ZmFrZV9lbmM6q8B+3LwD7ohrKPUED05T82/7CtGc2VslUmlVOG7o9tba5SG+Ag==", "_name_hash": "d14218762d835f7a9c96377522b0c34da735efa982ea5a7d041bb5f0522cb3f3", - "_name_plain": "Class 102", "accept_requests_until": null, "always_accept_requests": true, "classmates_data_viewable": true, @@ -336,10 +331,8 @@ "fields": { "_access_code_enc": "ZmFrZV9lbmM6ZZ3FDZRvlw+E1lK4dxpki1idTSRAOt9hCFPUfcdeEmfm", "_access_code_hash": "a895a482b72ec418311b8a9677bcf2c690425993f563f58d4a15b9586753b88b", - "_access_code_plain": "AB125", "_name_enc": "ZmFrZV9lbmM60/Rt59QRAWw4AcoT0nKDqrFm25OgmMhKElgPi2A/e2oTsy9ZZg==", "_name_hash": "e1782a43b71afd648ae245648d45e617665bc1ab9a65c6c05324eb1818bad644", - "_name_plain": "Class 103", "accept_requests_until": null, "always_accept_requests": true, "classmates_data_viewable": true, @@ -355,10 +348,8 @@ "fields": { "_access_code_enc": "ZmFrZV9lbmM6lX8pCVUFOyvveBTehUr60jztsv49yrwUJTsZALhc2w89", "_access_code_hash": "38045227bf3c575c643dc1b34a1e70669f35b90225495bbeba0a53ae5ba82c81", - "_access_code_plain": "RL123", "_name_enc": "ZmFrZV9lbmM61dGjSEe2fv5xma2w6WNAaxOJ4yJ28pd7TJkdh1eh9LqgDchS/3hDrY+j77I=", "_name_hash": "cb7730d71c019fd442db0db09d27e49a1d84f01e79a99a82a949b0c6ed24d508", - "_name_plain": "Young Coders 101", "accept_requests_until": null, "always_accept_requests": true, "classmates_data_viewable": true, @@ -374,10 +365,8 @@ "fields": { "_access_code_enc": "ZmFrZV9lbmM6tmPl174+K04tllPCnHhyfux6Kw7XPt/dWsDEfr62yjHP", "_access_code_hash": "732f2cd0915bdea50771ce7c92a4e1b8ea83f6d3323b95dd07619e5ea7f0fd06", - "_access_code_plain": "PO123", "_name_enc": "ZmFrZV9lbmM6SwqcaZ2noGygc4dHOKUsHTL7lWU79/WrqGgK9XqIgAUQjQmUpJZKLGE9nTHRxQ0=", "_name_hash": "72b62d2a95a4fcf7e4f7853953f0bc20858a57f9e7c2c9203203a293a13631a9", - "_name_plain": "Portaladmin's class", "accept_requests_until": null, "always_accept_requests": true, "classmates_data_viewable": true, @@ -596,15 +585,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6dl/MQRUjrYLzJ8SGT14bzsvvpDfI00N7KlFT01peYBfPDY0mfBC3iT5Dj7hDmvX/yaUeY56NT20=", "_email_hash": "6229d653107a83cfe416912e62307e9b3bfa5185b06deb74db27576e291ba301", - "_email_plain": "codeforlife-portal@ocado.com", "_first_name_enc": "ZmFrZV9lbmM6ZPlIinlRmmI7yo7cj16Hxm4N1WVBWgTkcKwRvxvB3Jfjmg==", "_first_name_hash": "dc0c24809d18768f01857048c816eb3885cb627a759bd2dd5314970adafdaf3e", - "_first_name_plain": "Portal", "_last_name_enc": "ZmFrZV9lbmM6aRN4l2rGBRhy1m+Qh2RWRvNmADOqa+g4Sn/eUOwvUULe", - "_last_name_plain": "Admin", "_username_enc": "ZmFrZV9lbmM6W68OaTj6IrJ6dAduESf+9QJrchey291ox5c1UWYQxdiU2zCxNL54aG0oFKCTBZxBJM77J8bHIpk=", "_username_hash": "6229d653107a83cfe416912e62307e9b3bfa5185b06deb74db27576e291ba301", - "_username_plain": "codeforlife-portal@ocado.com", "date_joined": "2026-02-04T16:02:33.631Z", "dek": "ZmFrZV9lbmM6nS1csorNRFGQ3FknHozzLOObhQ/iXXf2hH92eeOBy8roL2HNlrK14npw6TYEV7VJpPVyXo17WZp/C6J+", "groups": [], @@ -622,15 +607,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM67sZxLliYMKm3uz98nOwt8214HZeWxLDLAiwm2MgLgBzkU4wxAvou4swZvmSm6kM8pnR5XxyuHXR5TA==", "_email_hash": "9cb6a6152b4d3dfbad65576fb4f6688b56d73cbc65b36b2a7fbb313a1352d3c4", - "_email_plain": "alberteinstein@codeforlife.com", "_first_name_enc": "ZmFrZV9lbmM6dygqD/GL7Zx+cDPU0dlncOJFQhUZrEJkEWlTHALit+s5Xg==", "_first_name_hash": "920e5149d016b9ef005052b8f52ec833b0974ecf7fb25dc8efd42d8cb1912bf2", - "_first_name_plain": "Albert", "_last_name_enc": "ZmFrZV9lbmM6AnE2DA2bdWH5SpZDB5F5PlypOzoZ07TeA2vLqfILyZbm9Y5c", - "_last_name_plain": "Einstein", "_username_enc": "ZmFrZV9lbmM6kEa5T9wBkXTu33fwbGtFe9rZz2whtqtE+NmBnapbN8aFySphoMJNKjgGIxm9kl1OlmbiYVDjcIQcyQ==", "_username_hash": "9cb6a6152b4d3dfbad65576fb4f6688b56d73cbc65b36b2a7fbb313a1352d3c4", - "_username_plain": "alberteinstein@codeforlife.com", "date_joined": "2026-02-04T16:02:34.051Z", "dek": "ZmFrZV9lbmM6BLODLjFqwOGQ2F1gfhBxUDu0x+ysKL/THNOI4YaJWkERqeJ176yQerprvm1m8/kUquwMqV56MMYOcJg+", "groups": [], @@ -648,15 +629,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6kzvUIAUyszIHT6fIJ1fyXPwH+XJScZHIf7FU+g5WkdvRj07vlPDJFJKRzoEWiDQ3JTT0izE=", "_email_hash": "586984c6c88ed9d06cbb6d64e746ca1c7b71dc8782f68a075aece08fd2b5557a", - "_email_plain": "maxplanck@codeforlife.com", "_first_name_enc": "ZmFrZV9lbmM6Ago4UQZ3oxNQyigyT60Lb+fyVC87pZtkjk1MpUf41Q==", "_first_name_hash": "337aa91f0cb585a80fee292721d7e78119da4aa52be054785bf1069302979e5e", - "_first_name_plain": "Max", "_last_name_enc": "ZmFrZV9lbmM6ZdaW5MUAIckllddasnE72Xm6ZSxl9Z7lR3Hjyc8TDeeNhg==", - "_last_name_plain": "Planck", "_username_enc": "ZmFrZV9lbmM6NdA7tbr7z1HLyLHeAH4ndHuTQjFHvhixbXqR2vU6ImdteCCd7aceSCjXXCn3U/1JPMnOWKo=", "_username_hash": "586984c6c88ed9d06cbb6d64e746ca1c7b71dc8782f68a075aece08fd2b5557a", - "_username_plain": "maxplanck@codeforlife.com", "date_joined": "2026-02-04T16:02:34.252Z", "dek": "ZmFrZV9lbmM6V20JnUxKv0GdaX13KdF574HPbkmjD7RJj7/rFr4wQVtQfCIjK2MHcFryVDsMq2eIJ72uaxXc/1FFSkDp", "groups": [], @@ -674,15 +651,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6LwlZEk/dUtTEdlbhapucNrJocYIk9xPu5Y1NMg9hnmIndIj2+gvjuruwfVXOgGZbA6vQUw==", "_email_hash": "6df8a9b30d61359c2f434a45e881fd4461899af9f5df0e05fd15ba57e5885878", - "_email_plain": "ramleith@codeforlife.com", "_first_name_enc": "ZmFrZV9lbmM6MP5pw/kzO6fiWqUFK0ZxqwsWrY66EuDvC1jVIAtulw==", "_first_name_hash": "62ea6bdedfb21184d0fb2342a6109dfd8ecbe0ed5b80ec8db8b2b414ac01423c", - "_first_name_plain": "Ram", "_last_name_enc": "ZmFrZV9lbmM6Jn1K31v4LzeQ/qZffGAGMZG0/J6biO5+Y1DEnEOI4CeK", - "_last_name_plain": "Leith", "_username_enc": "ZmFrZV9lbmM69F34/PbRct5acqudNvNPo4mrJ5YZqvhfjDtKBrdAT/JR7XRfzfPE3dklvAmdu4uEOnIBZQ==", "_username_hash": "6df8a9b30d61359c2f434a45e881fd4461899af9f5df0e05fd15ba57e5885878", - "_username_plain": "ramleith@codeforlife.com", "date_joined": "2026-02-04T16:02:34.448Z", "dek": "ZmFrZV9lbmM6TcuSFYFFWC8rXPukd/0YiijU8IaKU4lHNAHMjD2eDz8ncLZsgebHW5bRnJAx+WvX8vZoCtlDPcHrEAuh", "groups": [], @@ -700,15 +673,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6btpo9a5rZlczOWXCdwHL5xOSQxCZc/emn8X0E7IcKyFF7C0VS6LmOVBYa/MqEmol91LPCDXB225KEmo=", "_email_hash": "55a63981ea426d7470560ecfaf3b53c5ecd90e036edd8eca2b1a33c84178908e", - "_email_plain": "leonardodavinci@codeforlife.com", "_first_name_enc": "ZmFrZV9lbmM6ZT6hCR6NYDU208WG5Zt2Q18itxlQtSKfLZ/3c5K+bxRw43K6", "_first_name_hash": "9e84472777869ca6205d06421ed265c1c3cfcdfc35a7de158201227c3c541198", - "_first_name_plain": "Leonardo", "_last_name_enc": "ZmFrZV9lbmM6Eiq20D36p1cMlSdqcy5Mtqq3hm22MTDChEcibQg985YsRXE=", - "_last_name_plain": "DaVinci", "_username_enc": "ZmFrZV9lbmM6KUios32zumvdsLvZifvFVDEdGgpgw5dYlyh5EokO0X7851hQXMj1HPI8WoPePOtAQF+ZYLIv31AumHU=", "_username_hash": "55a63981ea426d7470560ecfaf3b53c5ecd90e036edd8eca2b1a33c84178908e", - "_username_plain": "leonardodavinci@codeforlife.com", "date_joined": "2026-02-04T16:02:34.641Z", "dek": "ZmFrZV9lbmM6Muw/vyJnAHIelig6sSUZZCY8bk1SS5TCasFP5Sk7U8o5llwR60mq7jAJoLkVme26sZJ0ULm27NyLEvKm", "groups": [], @@ -726,15 +695,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM65Pk8pnQv+HKcPHLwoXfdCg5l++CiG/neKwfiJtMFNQhqRVU7N4SrdhFq08gcZm19S/L4zYbSbXvLeg==", "_email_hash": "1c5f6e85fe62be2048d5a3867eff53db48f9bcd6da889b92e68668a4a464afd2", - "_email_plain": "galileogalilei@codeforlife.com", "_first_name_enc": "ZmFrZV9lbmM6FGiykuz+Sx8x0lgoaRGOOBtiL2fRSKJDSIMLZoxttrQl4fo=", "_first_name_hash": "cae481802ba39e14537dda2d0125c791bab6e0b4e323e9833591a5de6c50207e", - "_first_name_plain": "Galileo", "_last_name_enc": "ZmFrZV9lbmM6zQGTj4h6SuBQ+GHF0VPEIZO+bu/PsWXhzhyuHeHIefBYmHE=", - "_last_name_plain": "Galilei", "_username_enc": "ZmFrZV9lbmM6Y4pOAxErjYVL1xULqX20AJveq6DGfsJqA6tQNtZdoOaeRFAwKUNvdCM/A/vm8LBq6uTpNLGUpudfOA==", "_username_hash": "1c5f6e85fe62be2048d5a3867eff53db48f9bcd6da889b92e68668a4a464afd2", - "_username_plain": "galileogalilei@codeforlife.com", "date_joined": "2026-02-04T16:02:34.839Z", "dek": "ZmFrZV9lbmM60JSmixBDvrfWaRyP8idNvMR78k1YyavGZ+lBOARPt05D1PqVrxAP+HK3qajPuD2iizikhEgSh6dF5iyb", "groups": [], @@ -752,15 +717,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6nE5NOakc9jQBJf7sLqY2uiRil9e9sUEShSNLiSoHg5FqK/CY2Do/sbV+1A6oxxB85d3HZuL+4Q==", "_email_hash": "4fc8d589140865b27bc27623cb547e2a24f9b985ab2e1dd0b5b5c035d367c4b6", - "_email_plain": "isaacnewton@codeforlife.com", "_first_name_enc": "ZmFrZV9lbmM6eCpZHFZkWzQ/hv8G9psJxOAqvezmqYDq0RSpDJlEptGK", "_first_name_hash": "c49fb23f0176885dece511c58087b7f23019c343b9861e2d70926600577a4611", - "_first_name_plain": "Isaac", "_last_name_enc": "ZmFrZV9lbmM6xN/iK8bQby0voDF9a35nJ83At3HgLkGbAptBWLtTNtnZzg==", - "_last_name_plain": "Newton", "_username_enc": "ZmFrZV9lbmM62SGXHdjBqA5Y+dD5VvE3AykKRYr7Rr6b+QvCaibuKwggnxtlx0AGgi1fNYQswsuoa3sskN5e3Q==", "_username_hash": "4fc8d589140865b27bc27623cb547e2a24f9b985ab2e1dd0b5b5c035d367c4b6", - "_username_plain": "isaacnewton@codeforlife.com", "date_joined": "2026-02-04T16:02:35.036Z", "dek": "ZmFrZV9lbmM6AgiWjZ7GQyBK5l4VurLVygmXkszQHL3kZhN33iYzyC1HMfWWMVI/ggxa+oNbq2olRmVHguF8uNzBvTo3", "groups": [], @@ -778,15 +739,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6QTQVq47caqRoq8EK0uUN1ygFV+XvmYPEXq5onCbdj13XdI6DfrCAlx7W7gFAABbpkBCJhQYZ0cETjw==", "_email_hash": "6bd23daa66a426a27655016f084d3fa65a49e2e5d264701b6d84e550e415638d", - "_email_plain": "richardfeynman@codeforlife.com", "_first_name_enc": "ZmFrZV9lbmM6KD7poCabt24+OPDhCz9fiZcUqF+PrVEHoIT9gwFbShzh3JU=", "_first_name_hash": "0a8a5deb9f8f9a8f0e96af7630e59b6fdc0a2528095a5a382c3d417da9c97f7e", - "_first_name_plain": "Richard", "_last_name_enc": "ZmFrZV9lbmM6JgmcA+18EuPfUeWJSuXju16Z0h+HDj9m/Xn51LaSe+Ef2TA=", - "_last_name_plain": "Feynman", "_username_enc": "ZmFrZV9lbmM6ahl5fcghPgCSSN8QVcwIyupa5kZvvO9vOiUoJdZRSUbB/aLgN2tPBaCA8QiR6/lkO2182l5Y/mr3Aw==", "_username_hash": "6bd23daa66a426a27655016f084d3fa65a49e2e5d264701b6d84e550e415638d", - "_username_plain": "richardfeynman@codeforlife.com", "date_joined": "2026-02-04T16:02:35.230Z", "dek": "ZmFrZV9lbmM67Iqyt69l+yrc2BMF1r5L+UE0BIUZ0HfFN3baguyKwLTeNtTFj1HOcVJAuFtoV+hvIT+1oY7x0Fqul03j", "groups": [], @@ -804,15 +761,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6kQeK+uVf0/b+O1SZqONvKrsfffa8C3pSKCQa6uyF1lPK6FwDVl2vQNCPL5ak/4+a7ja7A00S4i5lpJZr8g==", "_email_hash": "1ab9d2f1ae8ef6a5e99cee8948c326e3404aceb89b8d72215e297a1cfc8393a6", - "_email_plain": "alexanderflemming@codeforlife.com", "_first_name_enc": "ZmFrZV9lbmM6ZVz+1VG3scf9MoEfVA/ShO5cJNBE9riNqQ/2I20l/OsroQMZrA==", "_first_name_hash": "e78f1c3e180b8af2c8dfc79ec04d65ac565361c887584100e67890cdb70ab52b", - "_first_name_plain": "Alexander", "_last_name_enc": "ZmFrZV9lbmM6jxxUTKIDlEj/scNJ2jRVZujwmG8nYCfRqepcQi7lnBz/vYSI", - "_last_name_plain": "Flemming", "_username_enc": "ZmFrZV9lbmM6g1cd/om7Tco3iOqnDa5z3mU8wFau/Mq7xTEH6EJWEGe9d68cGq0SmwovpAWW6HmDxF3STUJ6PV8kECAZiA==", "_username_hash": "1ab9d2f1ae8ef6a5e99cee8948c326e3404aceb89b8d72215e297a1cfc8393a6", - "_username_plain": "alexanderflemming@codeforlife.com", "date_joined": "2026-02-04T16:02:35.422Z", "dek": "ZmFrZV9lbmM6hGzSBoLIbLU4BDj32rxVFgg6uArfWek5BfmFo5ZoBjV2MhQN5m0dBvuVjzb0t6fTY5d2nMzlUcdIofrF", "groups": [], @@ -830,15 +783,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM64e5Vzz8oKJYjbYI7+2ZF7XuH8wPevFgtQC2X5bc0pWJ5H9bETgDrFyA4sMZaU9Tg9kv+qws4haGfGTg=", "_email_hash": "0c92dd15cc629b84ea9436c965f944543cfedd41ced6c8f2594a26ee50cf3ea5", - "_email_plain": "danielbernoulli@codeforlife.com", "_first_name_enc": "ZmFrZV9lbmM65SWBLZni/laQtNAl64mXA79twEZho+651VCt311rAguFzA==", "_first_name_hash": "6bc6d4fc17ad3131d3417aebb876bdec85a697de1a69734b9d113c800e72aaf3", - "_first_name_plain": "Daniel", "_last_name_enc": "ZmFrZV9lbmM60eUQSPe5OAsCeRt1gzrwLa7EVSar6AnRbZG3gHztcvdth8IrvA==", - "_last_name_plain": "Bernoulli", "_username_enc": "ZmFrZV9lbmM6hCfOHVID4fPRgzyP+oOM6gHabOlxDQwjLsbLuw0ngMJ1SapLlj2nC9fk48FecXELl/JXwRS1/PI5sEY=", "_username_hash": "0c92dd15cc629b84ea9436c965f944543cfedd41ced6c8f2594a26ee50cf3ea5", - "_username_plain": "danielbernoulli@codeforlife.com", "date_joined": "2026-02-04T16:02:35.611Z", "dek": "ZmFrZV9lbmM6J4zSpKtOmn7y3Y3C6x4OVGcb+e0E+AW85tNIEZb2pSDinQqzUkft/oBWqEYTqk8llh1JDVFmia1hKBjl", "groups": [], @@ -856,15 +805,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6Bkpg2XLc10D1UmbpVkCI0lg4MJD0zeGOfT07lzxvvkgA52wMafu63U9ylmvgukYRBJr3bWG1XLI=", "_email_hash": "1a93bc8026b39e9d71124c74cef2f072d4e41625ea3b12f4009f104dce962e2f", - "_email_plain": "indianajones@codeforlife.com", "_first_name_enc": "ZmFrZV9lbmM62ghGxpAhcBGTgECH3XG5lhv3/nGPelk1SYi2on3HmiQnnEs=", "_first_name_hash": "9859180db8b64c329244146e3cfda4e6c42fd38f8ef9fc0f272af81298c6c02a", - "_first_name_plain": "Indiana", "_last_name_enc": "ZmFrZV9lbmM6excgvA44+7Tzd9kxwsurRR0/7JddJ4ONlEhYSqS+e1XH", - "_last_name_plain": "Jones", "_username_enc": "ZmFrZV9lbmM6u79RxJjXjQOn3DUiaBeGPzBlfNd5dfa4/yAAH5j3Wwnc57AflACzCpfF713nnD00CTFftFYTf9Y=", "_username_hash": "1a93bc8026b39e9d71124c74cef2f072d4e41625ea3b12f4009f104dce962e2f", - "_username_plain": "indianajones@codeforlife.com", "date_joined": "2026-02-04T16:02:35.803Z", "dek": "ZmFrZV9lbmM6RJvZthmhY//D7zh7NTsTeIBZxSsBClnGVaP30RcuabLz1xYLUNNnumUOdygscg+BDZwxu8cwTD2anSyk", "groups": [], @@ -882,12 +827,9 @@ "fields": { "_first_name_enc": "ZmFrZV9lbmM6kjfxs345zoTZfqKXkDHtfcIY8poSjk8VFfhMq1knz/8=", "_first_name_hash": "91886b94f7298f93affde2a4c4bb1e1ec836b5da66507834c964dc8db418fa14", - "_first_name_plain": "Noah", "_last_name_enc": "ZmFrZV9lbmM6eHb/bRCUVrG5ESV5qHfbsiBvRelPu7wOHL8ci370OvxasRpm", - "_last_name_plain": "Monaghan", "_username_enc": "ZmFrZV9lbmM6AtzaoZyVd75YQrEOaNqUxr14E2Z+9i3KJuJ0cgFi2gzAI7zw85E=", "_username_hash": "a1e49779f7ae17103934e74a1eea820997bea85334002ec14ffbdeaa2d626eee", - "_username_plain": "media noah", "date_joined": "2026-02-04T16:02:35.999Z", "dek": "ZmFrZV9lbmM6ODiZ6w5NfOc9/9oP8JH+F0AKguKkTdwnguKY7IUw4yMv7bkTGS0ZDr6lRMeUSB2bUDAAeG/s4o4Rvbsa", "groups": [], @@ -905,12 +847,9 @@ "fields": { "_first_name_enc": "ZmFrZV9lbmM6BWoSB7BYxH4FxpQhkd4TvieVMj0N3sPdLhCNohLeetFyrg==", "_first_name_hash": "050fa7990069876e75ceb3fe387a3f10b5beec63612c5b4e500cfcf246daa312", - "_first_name_plain": "Elliot", "_last_name_enc": "ZmFrZV9lbmM69YjNMSfcXFg3pGr58K77NGxqGG3nV21qsO+sSnFPRvbd", - "_last_name_plain": "Sharp", "_username_enc": "ZmFrZV9lbmM6P8vG8taJXnEQWOdAwlh7FzalVSbQzMwdx2aku3PZK9wt99K3A9K9/g==", "_username_hash": "efa5e3d6015a02b88470d47080508a671bfb539b41d18e1893db820f95a228cb", - "_username_plain": "media elliot", "date_joined": "2026-02-04T16:02:36.195Z", "dek": "ZmFrZV9lbmM6E2af4DEGdnVy8AdysOxQWAr/XZjM1paohrFtzwcU8ErHa4Mds1bm5zK1WoPEwxqoEPD99/XZfBuw7/oa", "groups": [], @@ -928,12 +867,9 @@ "fields": { "_first_name_enc": "ZmFrZV9lbmM663tdyyomEQ2QzpHpj3ZX8IldQVNNvwZoKn7SuywzsKUDvA==", "_first_name_hash": "969bd503b94e99bb283caa34c5e28fcb8cebfb278111bff1f76ed1f8d08c2720", - "_first_name_plain": "Tajmae", "_last_name_enc": "ZmFrZV9lbmM6Hb+2sb0IFHydvEnx3koqRuZDYKraKhtD1gLI7XSs5hrM6g==", - "_last_name_plain": "Joseph", "_username_enc": "ZmFrZV9lbmM631Qr0w4wBZN1Phdldl9WY+1n3NrBZ6QUraZY4p0ZmC8v+9NnUvTvgA==", "_username_hash": "b154013ded73a161c8ceae084060ed319504d9a3690b569cc54d39b3e985a0bc", - "_username_plain": "media tajmae", "date_joined": "2026-02-04T16:02:36.394Z", "dek": "ZmFrZV9lbmM6lHwaHwEAEkTsDE+w+LnUw/c/ra+TdL5/dU9xPFIW92I98HzZg50AxHu4n2lKGe/FTzNrR4uCFzFQ/+IL", "groups": [], @@ -951,12 +887,9 @@ "fields": { "_first_name_enc": "ZmFrZV9lbmM6xiOckTwack87H3A7PBretwem+4On+TLQK7vQ40/0FpPNCX8=", "_first_name_hash": "8e450f405264e612fb62f915830166cd43c03612b82c19c0c52aaee93183b0bb", - "_first_name_plain": "Carlton", "_last_name_enc": "ZmFrZV9lbmM69RSRhxmcba/7l7H8s49DQTlBtOLzTQUsgdggRgHLYzUELA==", - "_last_name_plain": "Joseph", "_username_enc": "ZmFrZV9lbmM6DPlTHgfxXTVL0L2uxDpQ+IpoVZymQQMhhSOnTnW8fCtJSV25jdQ+pRw=", "_username_hash": "e18c768a720dfab2514b66d44c2b0cbf54d5e28f9e68dbc0df5dfde76df02f5e", - "_username_plain": "media carlton", "date_joined": "2026-02-04T16:02:36.589Z", "dek": "ZmFrZV9lbmM6An/cveM3ZXcZM7QxIgYd4WK7/M3yJiWAe8Vo+pc7PL0TNogWsWByiNucIQqlmfIZARMQcJC/p3pJx1QQ", "groups": [], @@ -974,12 +907,9 @@ "fields": { "_first_name_enc": "ZmFrZV9lbmM6OtOk2SsoMWAc38TD0Q41fmaYTkUR0+0nt2Rw/JuDkgb5", "_first_name_hash": "8424a2115bf1ee8fa253b59cd46cd6487c1b53b51201e593c08a12c390d70ad4", - "_first_name_plain": "Nadal", "_last_name_enc": "ZmFrZV9lbmM6f8mE9Wc19ubz9l+FnWRJU4dXhOwbl3Vwc2ISVdXXF4GmrbeK+AMRwdZVwDI=", - "_last_name_plain": "Spencer-Jennings", "_username_enc": "ZmFrZV9lbmM6DDox9dauGKr9FLYT8aNae6nM84ZP0SO66osdvk/A+JR5toUdN+N/", "_username_hash": "9d75c6e6c5f4793504f8c8be317bb007ff69ffbc9caa0e80acfef15ee15f661b", - "_username_plain": "media nadal", "date_joined": "2026-02-04T16:02:36.792Z", "dek": "ZmFrZV9lbmM6VtLPjgaz4N8xsAHY6fKs7B4Q6dUNJLcu0e8i02/cbPVf+Xcx/OYE//dzmldTRcj0BEhRE5HG6EV9yJGB", "groups": [], @@ -997,12 +927,9 @@ "fields": { "_first_name_enc": "ZmFrZV9lbmM6j0YLTeDH49wQDBGAGGslQq+XvbkmjBOjZz/81chsQ6CgduE=", "_first_name_hash": "8d81abd4d9f4dd116d4473915b068c9790f2229ab72fbcd974daff57ebf62717", - "_first_name_plain": "Freddie", "_last_name_enc": "ZmFrZV9lbmM6qqw2DvU2GxSQXO2EqczZa2U8FVS4mRcyIZoDpbhoEeI=", - "_last_name_plain": "Goff", "_username_enc": "ZmFrZV9lbmM6eWftc/iNk2tjFElPm7fCJpsZ1iMQKF5L6M866R+GpcCflBTD9p+YzRI=", "_username_hash": "aab046dad21372920e2322ed46ad43a8574963ffd7033e1bd5a6285ad07c3b60", - "_username_plain": "media freddie", "date_joined": "2026-02-04T16:02:37.009Z", "dek": "ZmFrZV9lbmM6e0QkyGSRPJxtPLH/c5zTXK5jkOET732kHyRwCTpiQhiSqj3Ar80GkMDhWaXGvqx4qpDLrzZDLZftBe/8", "groups": [], @@ -1020,12 +947,9 @@ "fields": { "_first_name_enc": "ZmFrZV9lbmM6UerbiIlXuCiufRtvMUVZ+JauFHZ3y0Fp/GZl7ISGn/o=", "_first_name_hash": "5081dca0f5b49f2fdfda5543c6bbeef8e733838de7b8e642064f9b8edc0e24b5", - "_first_name_plain": "Leon", "_last_name_enc": "ZmFrZV9lbmM6bxT3gxf8cicQPv5nGIKLHApI/oxYN3k15TGYe4vwZec0", - "_last_name_plain": "Scott", "_username_enc": "ZmFrZV9lbmM6iW7kADgeYsJU5l1soljcMXYut3ZM9ax1Xop0oYhAw87tLOln0wM=", "_username_hash": "f34fecd51a3f21f82a61f6577abf7b69aac0e43bb23c6ad6003ec5df6a7ed54e", - "_username_plain": "media leon", "date_joined": "2026-02-04T16:02:37.216Z", "dek": "ZmFrZV9lbmM6ivzHA5Vfn50gnAP3KwRspc9mnTuE4qCr43qbCfPHpFP5D5YwNqW8OQnDdDwe/o5a9DdNJ3n85j7MpPU2", "groups": [], @@ -1043,12 +967,9 @@ "fields": { "_first_name_enc": "ZmFrZV9lbmM609cs4YE+Kj0atwhR0SOrJyuO9vSA74sqShdgLD7KRQ+C", "_first_name_hash": "c25e694e58d404373d63ef5dd2b2a413dab9c64aab028d735401c166813a87fa", - "_first_name_plain": "Betty", "_last_name_enc": "ZmFrZV9lbmM604sjV50gIcumJ2c/iDCvy3Siq/kVoTKllzvkyghd6A2hMoE=", - "_last_name_plain": "Kessell", "_username_enc": "ZmFrZV9lbmM6yFnuAvCj5RFXC8Iv+rljmAU/zzrUYIerAMmuXI/c4TFVi7+gSaGC", "_username_hash": "6120be5842a74a935c8d31a810ce2901fece2ab29d35d2d3fa6083b908a44434", - "_username_plain": "media betty", "date_joined": "2026-02-04T16:02:37.413Z", "dek": "ZmFrZV9lbmM648Mk4GDSMJHDA77reE0u/16wjHi8ujc2YxH6XPgOWc4S85G389Y5//r/RJKhonqH7X1pTIhiUZFbHz5F", "groups": [], @@ -1066,12 +987,9 @@ "fields": { "_first_name_enc": "ZmFrZV9lbmM6kEW6JpB05mjWAPE/xrxMrbFNoyD6ZBNVl+YCZI/q3+LRROg=", "_first_name_hash": "959e43f80903c037d87f933a2225789fbdbd71e0dea09fc1854a51e8d2f9b17e", - "_first_name_plain": "Deleted", "_last_name_enc": "ZmFrZV9lbmM6gwxaGy9/aDQWDEVUwUverJ6wmk3ohaTkvF5GYq6koNg=", - "_last_name_plain": "User", "_username_enc": "ZmFrZV9lbmM6kUOy1ae7Y1ElSiAiDXnKIZxPkOs1saBSjmH+UCEThmkOR7lKe9Nlmza/K+mW1Slr0UutzlVaqMLU3yWT", "_username_hash": "daa37b0878d74480285044dccc67750ad597b99719dc7323462cbe16e0b0271d", - "_username_plain": "4271ee7b7ce94e34a58d1f4e82025280", "date_joined": "2026-02-04T16:02:37.614Z", "dek": "ZmFrZV9lbmM6Ct68dRt74lesvjRcTVtlbUlPC8xt85anig+MaarMMZAUvcFmkJmno57KkQUR8S5/o5ElV9Bpn3RDpxeS", "groups": [], @@ -1089,15 +1007,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6v3CMqzgcHkXFUuH/E5WTE6Y1uXORrpJHu9VhYRdzAlYByeUliJUAkavcHMXo38zMc1HN92jTgwg=", "_email_hash": "d10b776ab1e76347e63692528b0e4e97a5e3288b5f3c918476577a96b9255463", - "_email_plain": "adminstudent@codeforlife.com", "_first_name_enc": "ZmFrZV9lbmM6WI6wHI7iEIUv0n0GxIsHsV75X85C0vRUlSEmAMnBr8aQOaDLa8RN", "_first_name_hash": "689afa6b012cd1c17a5b32dbf80482ccb8049f87af317883a23e7144b84242b2", - "_first_name_plain": "Portaladmin", "_last_name_enc": "ZmFrZV9lbmM6++cVFuCbPR0sIbZk2qM4m46PJKlZleoblSe6PWeixQl61Qk=", - "_last_name_plain": "Student", "_username_enc": "ZmFrZV9lbmM6jzxttMaup8QyTqNObb0h8Xwo1Un3ptf+BjbsOKFtTNwQ8dyEdNJLHvUa5D0ulpl85uxQzgyh898=", "_username_hash": "d10b776ab1e76347e63692528b0e4e97a5e3288b5f3c918476577a96b9255463", - "_username_plain": "adminstudent@codeforlife.com", "date_joined": "2026-02-04T16:02:40.242Z", "dek": "ZmFrZV9lbmM6BoYrbuFtVkzXFPjmHBZLlD971sOH30A01sG1em/a2PlmhBmN/i1FPqXZEBXkx9GU/QG5J1EQzGQqM0A3", "groups": [], diff --git a/codeforlife/user/fixtures/non_school_teacher.json b/codeforlife/user/fixtures/non_school_teacher.json index 2615f6d2..21d0f21b 100644 --- a/codeforlife/user/fixtures/non_school_teacher.json +++ b/codeforlife/user/fixtures/non_school_teacher.json @@ -5,15 +5,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM66eERLU4ywXZv8HpJEjyu3AJj13quStWLJbiKre21Mggqgh81l+viMglF/QBQ4fFM", "_email_hash": "7cc99dd2f335a068ac421d2cec8f04687fa34d37d313718f56ebc6b6c3c09d2c", - "_email_plain": "teacher@noschool.com", "_first_name_enc": "ZmFrZV9lbmM6naDBVRP/vWnfgWPmHiirA6COL71ARyfg0pnyeQ3DYlU=", "_first_name_hash": "7554c6b4930e0ac2c4d7fb5e7e266797b0444953067a0d7a17e835b518229e84", - "_first_name_plain": "John", "_last_name_enc": "ZmFrZV9lbmM6BVE1wAneS3Mcvb4o3lxNbaV8U2yFpq7pBiP5ai8FWA==", - "_last_name_plain": "Doe", "_username_enc": "ZmFrZV9lbmM6MHbn0VZ2WdBaxfuGEuU3zzyBsvI2NKWmKsqaipMhi1ie0aNmcBfnZqPRYM0bqM/q", "_username_hash": "7cc99dd2f335a068ac421d2cec8f04687fa34d37d313718f56ebc6b6c3c09d2c", - "_username_plain": "teacher@noschool.com", "dek": "ZmFrZV9lbmM6t5dLPgCaHfVJ+ZUMvIpOeZEo7pODNwUpd8exyX9ilp3COjdmNReUbCIeYVVyppQpim4aOw3EaOo6AInr", "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" } @@ -40,15 +36,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6ymTYrJvQBbledTYMf1QxnBLLJKtZOAY2eSrLKWn1Ku9Bt8UfjyAv9Kdcq22Nlw4KEUXUF9B+5uWFrII=", "_email_hash": "7f34bbe92504e0ca5c2235f2f64bf2686a7d040690a45948074247cad2eb5c71", - "_email_plain": "unverified.teacher@noschool.com", "_first_name_enc": "ZmFrZV9lbmM6TYaKDI0fEu4AoVsf35i4onELsjpPePS3+0aTi7bJ/cz6Uu6Cr3I=", "_first_name_hash": "47c086ea8af2fcd653408088d2d3809d3d46fd2698755a4a208d5fb78ebc205b", - "_first_name_plain": "Unverified", "_last_name_enc": "ZmFrZV9lbmM6045ESPZ+3oNUHhw8P27QOxgaRxBEbAkT1nI9LGBZeTcrWqU=", - "_last_name_plain": "Teacher", "_username_enc": "ZmFrZV9lbmM6kP4+WygnDIbz2v7bQVTJ9A5dJ/ngwqjhV1FG727D9fVpjRBqpZR7ZzDCpJLKGL+O/ErnM/v9cQjyRRA=", "_username_hash": "7f34bbe92504e0ca5c2235f2f64bf2686a7d040690a45948074247cad2eb5c71", - "_username_plain": "unverified.teacher@noschool.com", "dek": "ZmFrZV9lbmM6CxNXI5JH7pjDVV3SMaAS/lVuDNePBDKJPAiV4Gi/4iVcgxG6hvQ20IJX34nFw/U4mHaHCpTwSgWfSydl", "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" } diff --git a/codeforlife/user/fixtures/school_1.json b/codeforlife/user/fixtures/school_1.json index 701f05c4..bf5ad95e 100644 --- a/codeforlife/user/fixtures/school_1.json +++ b/codeforlife/user/fixtures/school_1.json @@ -5,7 +5,6 @@ "fields": { "_name_enc": "ZmFrZV9lbmM67d4VVL/VNF+xT49FGteqYAb5nww953xETgWD4Dbq/Wx+Jtoe", "_name_hash": "ae1a1256e88d98ce7870314ac36e23b7ab5163d4bdc6b72617b4c25a2324f256", - "_name_plain": "School 1", "country": "GB", "county": "Hertfordshire", "dek": "ZmFrZV9lbmM66Fc74ze1ks+0BusoBSk4Dq31Ze7K2owjJGHhyqul8ZDPLqqAdJfTXCTK7l9f0EHNZBrAHwioXhVm3iFi" @@ -17,15 +16,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6nDdT0za7YSuF59q1VhuJvT8qHbDLH+/ap65hrlVnwgSRkpRcj452UPQ/3IQ3H5s=", "_email_hash": "d23e2f365e919be1cf5e29a5472801d54bd75c6f360bce7c37674506bc63a9ce", - "_email_plain": "teacher@school1.com", "_first_name_enc": "ZmFrZV9lbmM6KHAoomsgzlenkIf3O3B1HkeoBYLCHnmBSzdbpebUfr0=", "_first_name_hash": "7554c6b4930e0ac2c4d7fb5e7e266797b0444953067a0d7a17e835b518229e84", - "_first_name_plain": "John", "_last_name_enc": "ZmFrZV9lbmM6UiD5C/IkRNEYEE7NgaGlAdMc+Le7fUhUCXNyEwEVYQ==", - "_last_name_plain": "Doe", "_username_enc": "ZmFrZV9lbmM6jV4xfKt2ER8uTjc56hdndNVFL5ZUpxwF8xmVreNQRpfz9PDHF226iw7U14MmLBk=", "_username_hash": "d23e2f365e919be1cf5e29a5472801d54bd75c6f360bce7c37674506bc63a9ce", - "_username_plain": "teacher@school1.com", "dek": "ZmFrZV9lbmM6jirqlzqds7vw9lM1WNsHUk7eGuT1X5EcZFvj77aL6V86oRw09V90ANLPNUUjOnNYvbb0BBh0EKT/j198", "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" } @@ -53,10 +48,8 @@ "fields": { "_access_code_enc": "ZmFrZV9lbmM60BDe75Jx8mLFNVE4Fj6A47qgtKnwj40qYBgA2/PdcJqJ", "_access_code_hash": "ca366870cf5e795a3c9ff340f119df2ff646128cf164d992b0167bbca8c247eb", - "_access_code_plain": "ZZ111", "_name_enc": "ZmFrZV9lbmM6GE5ywQd6ChB38qHTDtALv40nO1RQu1ty7Azups4SMdlBAfNUh93EdK65vyHrMg==", "_name_hash": "62e88a78e3a94e7b616d06399b55a3e667b85cf14386698df532d3c381244781", - "_name_plain": "Class 1 @ School 1", "accept_requests_until": "9999-02-09 20:26:08.298402+00:00", "teacher": 6 } @@ -67,10 +60,8 @@ "fields": { "_first_name_enc": "ZmFrZV9lbmM68LydJ3q0a8MRqB/aobe1wf3XPUOZZV/ibILf8bFjWeeTLVLY", "_first_name_hash": "341d77ca3f20f1a3ddf8d9a06d076940c6e24b252a08c5dae066b43c2b236ed5", - "_first_name_plain": "Student1", "_username_enc": "ZmFrZV9lbmM6fgGMjMvNBd0UtOYl4ZheNodBdcSjPsVmLYrIES+t5PbSSw66LNdmCAXLWefCbQu3Nlf4m3ESwd/3cw==", "_username_hash": "1a3a828a1d7b20c8c9bd4d3677dcd8e5df6352c950b2d99e04a77cdc291fbfbe", - "_username_plain": "111111111111111111111111111111", "dek": "ZmFrZV9lbmM6LLCQnwBkWTs27wN70fGx1S1a32s9SB4bd7w/qwEf38c4rhaXQaoBQr3yjcSD9KpBFUcngCtFpeKvwVYE", "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" } @@ -98,15 +89,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6oYdBxX01WxnO+2XvH4GcDYl8JVoGDYqCmB8u5hDlu33j84hMIU0c0rXh7X3EbVDqPaviQGY=", "_email_hash": "01c1e0721b31cb0636da3f5b3dc1a107bdd44fd0185fa12dd87f6aad06c91a08", - "_email_plain": "admin.teacher@school1.com", "_first_name_enc": "ZmFrZV9lbmM6ehSIW3xreQS4cUAXbBMnqq6OA7uS9iYEI9Y7iNOVDS4=", "_first_name_hash": "a5945caf6491a22426f870debae98c823043af86691d336f95248daff7bb989a", - "_first_name_plain": "Jane", "_last_name_enc": "ZmFrZV9lbmM6uf/W7NPtVAiFRyiZCwzvPOWrYa1KeBZT/HcZhoO1MQ==", - "_last_name_plain": "Doe", "_username_enc": "ZmFrZV9lbmM6PLx6EXzPQG/RgVjD718xICsjsz++Wir/jtJhP/5yqpPPNeLj7fjx3Zb7+VwO3Q5q8xDMOcY=", "_username_hash": "01c1e0721b31cb0636da3f5b3dc1a107bdd44fd0185fa12dd87f6aad06c91a08", - "_username_plain": "admin.teacher@school1.com", "dek": "ZmFrZV9lbmM6r5c/ViTs1yOJQrGRIkZcCR/7qyNWgWrWpbrj8lOhNUnsDhr31RvKy50cNcKauKJvpLWlVzXI2XLbQ2BJ", "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" } @@ -135,10 +122,8 @@ "fields": { "_access_code_enc": "ZmFrZV9lbmM6UjOO6ScOIUFHcbbcDkSzaWAQY3PaW7GNeRWTQ73zcpTl", "_access_code_hash": "e90c6683965279b92914a9e2b18716f0bc826c0dccb5976130562ff88cbd7f01", - "_access_code_plain": "ZZ222", "_name_enc": "ZmFrZV9lbmM6ghwPnoQhhjiDpIsfE2ufqAMOaXJAuiwUZ7nwuRLu208FZznOJkbXb3FKy9iE4Q==", "_name_hash": "b385f80b0a7d2cbdff653027b2f280e492fdd8f3c8c968de11d593b2a790f3af", - "_name_plain": "Class 2 @ School 1", "teacher": 7 } }, @@ -148,10 +133,8 @@ "fields": { "_first_name_enc": "ZmFrZV9lbmM6WTlhK3trdBVmqjtck1x0Jemd/5F1xq/hu5ghMYR+W+HRRsCj", "_first_name_hash": "f7ecf0b3ebd27342b1a6d30a6ef300b6dc23518fd5c83ca25cdc2b7135927cf8", - "_first_name_plain": "Student2", "_username_enc": "ZmFrZV9lbmM6r10EMezevJm9pQjvClWd/Rx06lfTdlIRYzsYEUcAq+V0yPYcafJ63+j+6jbHBcOR8qlXYoBw10mOHA==", "_username_hash": "69a7b577f2249a789e2a12ee80ee669c30ff20872c94153abeb3ebc05a61be08", - "_username_plain": "222222222222222222222222222222", "dek": "ZmFrZV9lbmM6cbR+gLNfBr+zos590Nlz8I2AbW1AS5A7Jo+iFzvK4bOn3OmQAw7QaTDGb7+n+Bquc74bWG9OwNz/eu70", "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" } @@ -179,10 +162,8 @@ "fields": { "_access_code_enc": "ZmFrZV9lbmM64FGWxjHz//y120LjO3pVfij0ZhFZixOLE8Z57vaYOPCW", "_access_code_hash": "3e3b96d4c7775a2c8d15999f6d662b164ccf3d66b8df93329bd5e426251d8cdb", - "_access_code_plain": "ZZ333", "_name_enc": "ZmFrZV9lbmM6d6hSPPotg2fBEgOhd6IRzrvlWYQxFaSv4v/FS7uXG7YxNB2XBq2k0Y08ZrhFtQ==", "_name_hash": "b6ff16f6d1e61c2f2845f3cde4cc5a67704b3d0b55e1d825c6813425fbbf5b8b", - "_name_plain": "Class 3 @ School 1", "accept_requests_until": "2023-02-09 20:26:08.298402+00:00", "teacher": 7 } diff --git a/codeforlife/user/fixtures/school_2.json b/codeforlife/user/fixtures/school_2.json index cfbedf50..379403d1 100644 --- a/codeforlife/user/fixtures/school_2.json +++ b/codeforlife/user/fixtures/school_2.json @@ -5,7 +5,6 @@ "fields": { "_name_enc": "ZmFrZV9lbmM6rDyP9kxRgaY6xKUohQdWebf8Gs94JU03UH5NCdROWyGOpUk5", "_name_hash": "de23670d9df1c898501128f02d31ed462ffef10d0d884cd1807ebfba96c6751c", - "_name_plain": "School 2", "country": "GB", "county": "Hertfordshire", "dek": "ZmFrZV9lbmM6bC7l4CSlvQMlUQ3LGmAgdXlinpUVIUxRCKl15mBnGovwpYqrwQhpqx47OXVPRQnzsIh5oX5/it+bnEw2" @@ -17,15 +16,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6aiPwZ1FRKtCtuUMMBINg0bJH+T0ADhxYrPmCORVX4iH+q73WJwIIXIp7I9hQ5Bs=", "_email_hash": "8cdd687351ee31833adcb72d37e7b8f1c1c17c655c3fb182ac0127320af64284", - "_email_plain": "teacher@school2.com", "_first_name_enc": "ZmFrZV9lbmM6SugTLn6zL+eLgD5bNHpO3WG1WS+ijY591LnSOvIxDic=", "_first_name_hash": "7554c6b4930e0ac2c4d7fb5e7e266797b0444953067a0d7a17e835b518229e84", - "_first_name_plain": "John", "_last_name_enc": "ZmFrZV9lbmM6JG27G2EreOMHHuSC6ydDGwMO9sE3g0VQBKNLJDomIw==", - "_last_name_plain": "Doe", "_username_enc": "ZmFrZV9lbmM6TEP3/O1x823sRLTr68TrLNGrCdYrQF8xhHtrqkhJJKHyg4OcCq+p3IV9chaGAlQ=", "_username_hash": "8cdd687351ee31833adcb72d37e7b8f1c1c17c655c3fb182ac0127320af64284", - "_username_plain": "teacher@school2.com", "dek": "ZmFrZV9lbmM631EYeb8mv6nnLRT6A9OG9FBDTQY+mzzJSZ9/poVoYpJwZas0QQ+o+S+T0N1qfP8XG2j4QAtybU8BYtVb", "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" } @@ -142,10 +137,8 @@ "fields": { "_access_code_enc": "ZmFrZV9lbmM6EK8Y5SIWGbV55+NvvCUsRgaw3QB+JEUGjCgJHgFJOj7Z", "_access_code_hash": "0157477281b2aee7660889207dee1a14756bddd428162de41bada7a02c593ff6", - "_access_code_plain": "XX111", "_name_enc": "ZmFrZV9lbmM6QS58FDOcIpwtt0KJ08wr+sKWQCVBucwv75ScfLpy53RQGS8dRIut/FEBoNUQ2g==", "_name_hash": "9d2590b59d26bcb0df89c9102403bb583445831149f8569feb4a75cb006e71a0", - "_name_plain": "Class 1 @ School 2", "teacher": 8 } }, @@ -155,15 +148,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6Cc97ynmcPEUq24u31O3ho+rE5m+La+iHmoc3tyz3VObKKWca658I30zgJM253BWZWax0WaY=", "_email_hash": "4841a8d3657918adffdab6cb4a0a6b64ad771aa56a53f80fed0ee8d074841ca3", - "_email_plain": "admin.teacher@school2.com", "_first_name_enc": "ZmFrZV9lbmM6XAAtQFeiEPJbJe9g1zErH67kShgjeyTihaxID8eHsG4=", "_first_name_hash": "a5945caf6491a22426f870debae98c823043af86691d336f95248daff7bb989a", - "_first_name_plain": "Jane", "_last_name_enc": "ZmFrZV9lbmM60OVDyC7hAJrEkLkpcJ7LvaWcl5ovVLPrafucEwvZsQ==", - "_last_name_plain": "Doe", "_username_enc": "ZmFrZV9lbmM6Duu4eRtUr7kJP/pxZea76U4q0m+/ouw1xt/jEhZ7bFyVVvc1n33HOLIKzvGO4RQKdE9Ze4s=", "_username_hash": "4841a8d3657918adffdab6cb4a0a6b64ad771aa56a53f80fed0ee8d074841ca3", - "_username_plain": "admin.teacher@school2.com", "dek": "ZmFrZV9lbmM6acw2bmFUUi2FVvY6NaRb393PcBFtnWsUdgZokzdWNxjrCnfI3qANds0dKuEEFzufBNQe4bPsmD5RmJdO", "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" } @@ -201,10 +190,8 @@ "fields": { "_access_code_enc": "ZmFrZV9lbmM6qqcLrPU7P3AyIdfDrFRA93ClW5sJEm/zyLRdbR5FsVUd", "_access_code_hash": "750df33e8a531e83fddf652344b680983a1555c6dcd8fda80f4af785357b444d", - "_access_code_plain": "XX222", "_name_enc": "ZmFrZV9lbmM65DRSk4ioNDC3lTdBdMzCx8izq6MA3hx1Lv7bVmIB0klrcE/ytKj/2YAOqQF5uA==", "_name_hash": "8e8c44adf55dce807796a63836973c83deca1c78995167d05a02bcac4e45c2e9", - "_name_plain": "Class 2 @ School 2", "teacher": 9 } } diff --git a/codeforlife/user/fixtures/school_3.json b/codeforlife/user/fixtures/school_3.json index 54494a0c..4196e4fa 100644 --- a/codeforlife/user/fixtures/school_3.json +++ b/codeforlife/user/fixtures/school_3.json @@ -5,7 +5,6 @@ "fields": { "_name_enc": "ZmFrZV9lbmM6LQq31+NKOlsLWDpsatuQth2lvPldBNrjK/oLXHFMGZbSkZFh", "_name_hash": "ccb652f4e608481591b5ab4d60348eed454b26aabf4ffed93a9748590e816a11", - "_name_plain": "School 3", "country": "GB", "county": "Hertfordshire", "dek": "ZmFrZV9lbmM6shO69sz7AjLAiIOt+QChcplwnM2SkTEydMeG2n/a8G39H7d320r9s9Q9D18poYRBOTW1ONMu4FDE1QAE" @@ -17,15 +16,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6IWL4ECecrQdG5hEcbqZjrua3qN4twKvoYnZ4Sxdsri4ER5WaDmBoRdlfpDxfBdt7mF4lGtw=", "_email_hash": "22a604d4828ae8429e8303d16c7d9967145b214b827a8ff1845b19e2a10b12c3", - "_email_plain": "admin.teacher@school3.com", "_first_name_enc": "ZmFrZV9lbmM6X/pzRYxC3wSQmFiiPbeGouhsrbGoQviFfkpXbF4nutGt", "_first_name_hash": "083ec2a4fd4004ed6f9bd61965b170a9b5db5d5873c7217f65bede117f004a79", - "_first_name_plain": "Peter", "_last_name_enc": "ZmFrZV9lbmM6W9e5RMeFGyGqvF5EXOFm66Sa6OeiQT/wbub//DRcXxzjUA==", - "_last_name_plain": "Parker", "_username_enc": "ZmFrZV9lbmM6CddPXEbQUUpaRaGDpvdxqRYehBobuVa0GuIgG+mE8t/Qys7HA9KvKTx3zNWF0rwobAz7ulk=", "_username_hash": "22a604d4828ae8429e8303d16c7d9967145b214b827a8ff1845b19e2a10b12c3", - "_username_plain": "admin.teacher@school3.com", "dek": "ZmFrZV9lbmM6whXTmVh8Ll7uD/RveSp8tPcfOtNoJb7XPPq2qa1ZXDSymoH85slcLwDSu/wl9gCKYhotmG1eQWyTbI5B", "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" } @@ -54,15 +49,11 @@ "fields": { "_email_enc": "ZmFrZV9lbmM6VhCbhP1x0NtRUqNK6RCZ4g3g6fFC8kjqD7vJDj+Jqrv3/vS6b5r6ui0BeIg2XcU=", "_email_hash": "c66f480d5458f514966dd1a540e652d1c57850a11122a26b1760eb95d7833fa1", - "_email_plain": "teacher@school3.com", "_first_name_enc": "ZmFrZV9lbmM6XZjkxcuqVijVuzm3HPh/C+wn9MbuGSI6FH+qbWghkHIRqg==", "_first_name_hash": "be367a9fe2b4bf78b7f7bd98e888fac606cfafa10fc91f25745d4a60e167ba72", - "_first_name_plain": "Doctor", "_last_name_enc": "ZmFrZV9lbmM6rZsTsYhs5sQaqfUSMArJQ7u7cb/ufIvxvUANzmjust+rIjw=", - "_last_name_plain": "Octopus", "_username_enc": "ZmFrZV9lbmM6UyHAKfn5fsxKpVQBKNOjvFXSWeKdjx+z51Id4rtPKYOQdLPuir213o1WgrbiR6A=", "_username_hash": "c66f480d5458f514966dd1a540e652d1c57850a11122a26b1760eb95d7833fa1", - "_username_plain": "teacher@school3.com", "dek": "ZmFrZV9lbmM68IHXVmIH5mmTb5T4H53Dcw2J1r4tZd0/GkPNUDJPk9d0/Gzm7UfKWIlLhDiNaSAlppjgSzf2lBd1w8K+", "password": "pbkdf2_sha256$720000$Jp50WPBA6WZImUIpj3UcVm$OJWB8+UoW5lLaUkHLYo0cKgMkyRI6qnqVOWxYEsi9T0=" } From 877aa9693655f7fb33fc595ac649772f444453f9 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Thu, 14 May 2026 16:02:53 +0000 Subject: [PATCH 6/7] fixes --- codeforlife/models/fields/base_encrypted.py | 24 ++++++++++++++------ codeforlife/models/fields/sha256_test.py | 2 +- codeforlife/user/models/other.py | 25 +++++++++++++++++---- codeforlife/user/models/user/contactable.py | 2 -- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/codeforlife/models/fields/base_encrypted.py b/codeforlife/models/fields/base_encrypted.py index 673a3fc3..5a2698fe 100644 --- a/codeforlife/models/fields/base_encrypted.py +++ b/codeforlife/models/fields/base_encrypted.py @@ -182,18 +182,28 @@ def full_associated_data(self): def _decrypt(self, instance: EncryptedModel, ciphertext: bytes): """Decrypts a single value using the DEK and associated data.""" - data = instance.dek_aead.decrypt( - ciphertext=ciphertext, - associated_data=self.full_associated_data, + data = ( + b"" + if ciphertext == b"" + else instance.dek_aead.decrypt( + ciphertext=ciphertext, + associated_data=self.full_associated_data, + ) ) return self.bytes_to_value(data) - def _encrypt(self, instance: EncryptedModel, plaintext: T): + def _encrypt(self, instance: EncryptedModel, value: T): """Encrypts a single value using the DEK and associated data.""" - return instance.dek_aead.encrypt( - plaintext=self.value_to_bytes(plaintext), - associated_data=self.full_associated_data, + plaintext = self.value_to_bytes(value) + + return ( + b"" + if plaintext == b"" + else instance.dek_aead.encrypt( + plaintext=plaintext, + associated_data=self.full_associated_data, + ) ) @staticmethod diff --git a/codeforlife/models/fields/sha256_test.py b/codeforlife/models/fields/sha256_test.py index 60dff338..2f92ba7e 100644 --- a/codeforlife/models/fields/sha256_test.py +++ b/codeforlife/models/fields/sha256_test.py @@ -24,7 +24,7 @@ def test_init__max_length_not_64(self): def test_set__none(self): """Setting field to None sets to None.""" - user = User(_email_hash=None) + user = User(_email_hash=None) # type: ignore[misc] assert user.__dict__["_email_hash"] is None def test_hash(self): diff --git a/codeforlife/user/models/other.py b/codeforlife/user/models/other.py index e57b214a..02396531 100644 --- a/codeforlife/user/models/other.py +++ b/codeforlife/user/models/other.py @@ -19,7 +19,6 @@ from ...models.fields import EncryptedTextField, Sha256Field from ...models.fields.decorators import validated_field_setter from ...types import Validators -from .user import User if t.TYPE_CHECKING: # pragma: no cover from datetime import datetime @@ -308,9 +307,15 @@ def invited_teacher_first_name(self): return EncryptedTextField.get(self, "_invited_teacher_first_name_enc") @invited_teacher_first_name.setter - @validated_field_setter(*User.first_name_validators) def invited_teacher_first_name(self, value: str): """Sets the invited teacher first name value.""" + # Importing locally to avoid circular import issues. + # pylint: disable-next=import-outside-toplevel + from .user import User + + for validator in User.first_name_validators: + validator(value) + EncryptedTextField.set(self, value, "_invited_teacher_first_name_enc") # -------------------------------------------------------------------------- @@ -329,9 +334,15 @@ def invited_teacher_last_name(self): return EncryptedTextField.get(self, "_invited_teacher_last_name_enc") @invited_teacher_last_name.setter - @validated_field_setter(*User.last_name_validators) def invited_teacher_last_name(self, value: str): """Sets the invited teacher last name value.""" + # Importing locally to avoid circular import issues. + # pylint: disable-next=import-outside-toplevel + from .user import User + + for validator in User.last_name_validators: + validator(value) + EncryptedTextField.set(self, value, "_invited_teacher_last_name_enc") # -------------------------------------------------------------------------- @@ -350,9 +361,15 @@ def invited_teacher_email(self): return EncryptedTextField.get(self, "_invited_teacher_email_enc") @invited_teacher_email.setter - @validated_field_setter(*User.email_validators) def invited_teacher_email(self, value: str): """Sets the invited teacher email value.""" + # Importing locally to avoid circular import issues. + # pylint: disable-next=import-outside-toplevel + from .user import User + + for validator in User.email_validators: + validator(value) + EncryptedTextField.set(self, value, "_invited_teacher_email_enc") # -------------------------------------------------------------------------- diff --git a/codeforlife/user/models/user/contactable.py b/codeforlife/user/models/user/contactable.py index 8669d48d..431d3dbd 100644 --- a/codeforlife/user/models/user/contactable.py +++ b/codeforlife/user/models/user/contactable.py @@ -7,8 +7,6 @@ import typing as t -from django.db.models.query import QuerySet - from .... import mail from .user import User, UserManager From 51ae33993c62f9a991f9ec1d8bdf3d58c7295362 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Mon, 18 May 2026 11:09:50 +0000 Subject: [PATCH 7/7] blank and null values --- codeforlife/models/fields/decorators.py | 15 +++++++++++---- codeforlife/user/models/user/user.py | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/codeforlife/models/fields/decorators.py b/codeforlife/models/fields/decorators.py index 5cf89d83..5c4646e4 100644 --- a/codeforlife/models/fields/decorators.py +++ b/codeforlife/models/fields/decorators.py @@ -8,14 +8,21 @@ from ...types import PropertySetter, Validator -def validated_field_setter(*validators: Validator): - """Decorator to apply validators to a property setter method.""" +def validated_field_setter(*validators: Validator, blank=False, null=False): + """Decorator to apply validators to a property setter method. + + Args: + *validators: Validator functions to apply to the value. + blank: If True, allows empty string values without validation. + null: If True, allows None values without validation. + """ def decorator(fset: PropertySetter) -> PropertySetter: @wraps(fset) def wrapped(instance, value): - for validator in validators: - validator(value) # should raise ValidationError if invalid + if (value != "" or not blank) and (value is not None or not null): + for validator in validators: + validator(value) # should raise ValidationError if invalid return fset(instance, value) diff --git a/codeforlife/user/models/user/user.py b/codeforlife/user/models/user/user.py index 3a39fa02..7ce0dae0 100644 --- a/codeforlife/user/models/user/user.py +++ b/codeforlife/user/models/user/user.py @@ -209,7 +209,7 @@ def last_name(self): return EncryptedTextField.get(self, "_last_name_enc") @last_name.setter - @validated_field_setter(*last_name_validators) + @validated_field_setter(*last_name_validators, blank=True) def last_name(self, value: str): """Set the user's last name.""" EncryptedTextField.set(self, value, "_last_name_enc") @@ -236,7 +236,7 @@ def email(self): return EncryptedTextField.get(self, "_email_enc") @email.setter - @validated_field_setter(*email_validators) + @validated_field_setter(*email_validators, blank=True) def email(self, value: str): """Set the user's email address.""" value = self.__class__.objects.normalize_email(value)