Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions codeforlife/legacy/helpers/emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand All @@ -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"],
Expand Down
14 changes: 10 additions & 4 deletions codeforlife/legacy/helpers/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


Expand Down
10 changes: 6 additions & 4 deletions codeforlife/legacy/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion codeforlife/legacy/tests/utils/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions codeforlife/legacy/tests/utils/organisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions codeforlife/legacy/tests/utils/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ 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


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()
Expand Down
2 changes: 1 addition & 1 deletion codeforlife/legacy/tests/utils/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
31 changes: 31 additions & 0 deletions codeforlife/models/fields/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
© 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, 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):
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)

return wrapped

return decorator
7 changes: 6 additions & 1 deletion codeforlife/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 0 additions & 4 deletions codeforlife/user/fixtures/google_users.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "a7e4c63feb2b46212c35276010cfcc7a0a8a021f42aefab89765c211cc794870",
"_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="
}
Expand Down
8 changes: 0 additions & 8 deletions codeforlife/user/fixtures/independent.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "fe1fe542767696689c8767d1b1e86734ce210252c07acc349c3a9f6175994e20",
"_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="
}
Expand Down Expand Up @@ -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": "fe1fe542767696689c8767d1b1e86734ce210252c07acc349c3a9f6175994e20",
"_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="
}
Expand Down
Loading
Loading