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/models/fields/decorators.py b/codeforlife/models/fields/decorators.py new file mode 100644 index 00000000..5c4646e4 --- /dev/null +++ b/codeforlife/models/fields/decorators.py @@ -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 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/fixtures/google_users.json b/codeforlife/user/fixtures/google_users.json index 561d8247..0644355c 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": "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=" } diff --git a/codeforlife/user/fixtures/independent.json b/codeforlife/user/fixtures/independent.json index fc1adccd..3f4ae104 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": "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=" } @@ -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=" } diff --git a/codeforlife/user/fixtures/legacy.json b/codeforlife/user/fixtures/legacy.json index 3f2f6f4d..50c94ea3 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": "6cb001965f12442bfcf1a8d9ecf5e31c6b2687bd5d05190cf56ecf648d9684f6", - "_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": "d8857e685fc7cd512c000209b05e67d4c2341f7770e93671adafdcdc332b21e7", - "_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": "20a84b82906c60b52b71aebab89320cefd1dc47e0f84861664f9e44d0f70d594", - "_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": "39710c33747315c8c6de8a7d5af97872303081fb0093cdb363aa4180223ac4a2", - "_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": "6a09152d3d184ca20ad7e3e2981761e8dd3747c93744591bd632843ce96f5e76", - "_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": "d502bc6b827c3f1fe7f8f43cc431f672c3b4044ff17883f1e081e5ace66d39ee", - "_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": "b2d8ed60f78679e18127a64fcb5df09083d9499b15a61cdea646b7cf8847f9c0", - "_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": "a28518a55f49b7810427f62b8cef8dcffde82aff3f8e16a17769f185caf8e11f", - "_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": "dc9a9f8e2d48f4db615efe61ad4f04906e28c93e37d4cf0ab13f692bcb86dfb1", - "_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": "e46e1b8306b6d1c91eb05ee6dd95241517067d4a166b02e98f3148197340b542", - "_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": "858907074712f2743f5e3408525017a064aa8e717c4d47d913ff81c29ddde3c6", - "_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": "3cf019416da72602544d0127b388fdd9c36e40bb648aab0e5f267b707d85b4ae", - "_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": "81bdaa26501a8cb2ee69aec002df098c5025b75ae0494b4b9ff6ef003131a792", - "_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": "4997281c5f1ee586af59704d9790c82ed1260e035073cbf05ba9ef24fea9f4d4", - "_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": "19e59a84a987c54abfd434eba28d49fa2c68b2be8a5649509562bd63b886e6d5", - "_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": "d8c5aba072c365b5ad92809ffa80dd21df8a77b7b28c2bb52d8e59492dc93f30", - "_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": "e63a00d61aa9887fd6e43a9a00545094b1dd9f9bfa902ee4e66bc1fe9e889bf7", - "_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": "010d83330e905dd4464175fc41558206ba825a7428519c50b16b20607211ac21", - "_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": "00ef09d6ae7627251b0673c48d9f00234537da9d3f38941b06726e6d902a5a66", - "_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": "5a012346d27f934d7196025377f84ef1c458929dfdd87419396780a4a2f5299d", - "_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": "ab51446cbc56dc4eeddc680b90f3e81311601bdcff926bd9d1fce3e0b50da6d5", - "_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": "6b152865ddf094484e72b8277cc134df7c3a49947dc90a5438fdcf59a8e9c668", - "_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": "0e9530fe4c47d64bb62a31e31e730665ac69d3e8ba9f311523c20d09ea53ec70", - "_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": "496b23a98c8f4f069a4ee4ed4ec498ad6b41e2ca921686339b87cdcceb9ee210", - "_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": "32b600fd12085215293ef30e867521520740cad567e21dc4c138c354d9189a1f", - "_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": "1dbd289e95b4925fc85eff99d7e879b2033f60182e05872f8665ff271d0d337c", - "_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": "a10cf8b1ffba3acb6bb68f941fde5b5226a0b0ab8e76390628114d73384792a7", - "_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 0e4582e8..50584f22 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": "d0118d7d585c39fb03017af6692eb181443402f0cb216e1541ed2c4a219955e0", - "_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": "7b5f659dd23dbcb84e56645e523c2dc962e4a99120c99bc3e99d361f85028605", - "_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 74b12e4b..b4df61b8 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": "18e858c37975d85d482cfca4547d9a8abad8d1474b19cad80c703e51a37a51dd", - "_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": "d0118d7d585c39fb03017af6692eb181443402f0cb216e1541ed2c4a219955e0", - "_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": "bdd4dcac0ca92a55010f608038920319cc8a12441fe47e502cd4b78414cc65b5", - "_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": "8b8e193489d0dee3b05730e8a0ed7dc86f8517dca0bd3a72935f427612476037", - "_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": "549014545102d32b3fd8280d7b5c1146c555b3ca09e02d71498a0d4d9c01283c", - "_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": "f102f85bd156b35d38322042e7a9a9f0335b3eb39db766aa79d9ff9b4e43d968", - "_name_plain": "Class 2 @ School 1", "teacher": 7 } }, @@ -148,10 +133,8 @@ "fields": { "_first_name_enc": "ZmFrZV9lbmM6WTlhK3trdBVmqjtck1x0Jemd/5F1xq/hu5ghMYR+W+HRRsCj", "_first_name_hash": "0d082766ef1a725b9da1a94ea7436ba0cc58feb4682d708b973ae8bd183abf54", - "_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": "a234c021eb820b72f6fb23a730d5d1b751767efdc374363826a99d220de21b34", - "_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 9a46be9c..46d762f9 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": "b65117fdd735fc7d03e5ffcba2e616a2298b2042c23ffadb2e554c23ae488488", - "_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": "d0118d7d585c39fb03017af6692eb181443402f0cb216e1541ed2c4a219955e0", - "_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": "19de16ed89970e886445d7e01bae0cfe2bc38d8a296993f238d783f39f936cea", - "_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": "549014545102d32b3fd8280d7b5c1146c555b3ca09e02d71498a0d4d9c01283c", - "_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": "88dd12eec5057abd68c74f5c32197e6110968cf135c00059132e2c5427c09f8a", - "_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 81f123b1..f4a4bd42 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": "54e5ecd0160f95541ac3975e488f242ab928383d1b974e49025ad9d4b87ec6de", - "_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": "cf728fccc7b72ca0648afcb4821ff233814704282ea069b5ac16d2afd5f41217", - "_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": "0f17a087a4b3fa11dbe711f99d1278341c5603776dd1e58578f3b4279fb725f8", - "_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=" } diff --git a/codeforlife/user/migrations/0006_client_side_encryption_part_4.py b/codeforlife/user/migrations/0006_client_side_encryption_part_4.py new file mode 100644 index 00000000..6fbb1f37 --- /dev/null +++ b/codeforlife/user/migrations/0006_client_side_encryption_part_4.py @@ -0,0 +1,237 @@ +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( + unique=True, + 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", "0005_client_side_encryption_part_3"), + ] + + 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 b47989aa..f2b81186 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.""" @@ -92,12 +79,8 @@ class Class(EncryptedModel): associated_data = "class" field_aliases = { - "name": {"_name_plain", "_name_enc", "_name_hash"}, - "access_code": { - "_access_code_plain", - "_access_code_enc", - "_access_code_hash", - }, + "name": {"_name_hash", "_name_enc"}, + "access_code": {"_access_code_enc", "_access_code_hash"}, } # -------------------------------------------------------------------------- @@ -111,8 +94,6 @@ class Class(EncryptedModel): name, lower=True ), ) - _name_plain: str - _name_plain = models.CharField(max_length=200) # type: ignore[assignment] _name_enc = EncryptedTextField( associated_data="name", db_column="name_enc", @@ -122,15 +103,20 @@ class Class(EncryptedModel): ), ) + name_validators: Validators = [ + MaxLengthValidator(200), + UnicodeAlphanumericCharSetValidator(spaces=True, special_chars="-_"), + ] + @property def name(self): """Get the name of the class.""" 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 = ClassModelManager.normalize_name(value, lower=False) EncryptedTextField.set(self, value, "_name_enc") Sha256Field.set(self, value, "_name_hash") @@ -152,10 +138,6 @@ def name(self, value: str): db_column="access_code_hash", normalize=ClassModelManager.normalize_access_code, ) - _access_code_plain: str - _access_code_plain = models.CharField( # type: ignore[assignment] - max_length=5, - ) _access_code_enc = EncryptedTextField( associated_data="access_code", verbose_name=_("access code"), @@ -163,15 +145,21 @@ def name(self, value: str): normalize=ClassModelManager.normalize_access_code, ) + access_code_validators: Validators = [ + MinLengthValidator(5), + MaxLengthValidator(5), + UppercaseAsciiAlphanumericCharSetValidator(), + ] + @property def access_code(self): """Get the access code for the class.""" return EncryptedTextField.get(self, "_access_code_enc") @access_code.setter - def access_code(self, value: str): + @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 = ClassModelManager.normalize_access_code(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 53f5c5e9..8fa3ad7d 100644 --- a/codeforlife/user/models/other.py +++ b/codeforlife/user/models/other.py @@ -10,12 +10,15 @@ 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 if t.TYPE_CHECKING: # pragma: no cover from datetime import datetime @@ -272,19 +275,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"}, } # -------------------------------------------------------------------------- @@ -294,24 +288,25 @@ class SchoolTeacherInvitation(EncryptedModel): _token_hash = Sha256Field( verbose_name=_("token hash"), db_column="token_hash", + unique=True, ) - _token_plain: str - _token_plain = models.CharField(max_length=88) # type: ignore[assignment] _token_enc = EncryptedTextField( associated_data="token", verbose_name=_("token"), db_column="token_enc", ) + token_validators: Validators = [MaxLengthValidator(88)] + @property def token(self): """Get the decrypted token value.""" 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") @@ -337,11 +332,6 @@ 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", verbose_name=_("invited teacher first name"), @@ -361,22 +351,19 @@ def invited_teacher_first_name(self): @invited_teacher_first_name.setter def invited_teacher_first_name(self, value: str): """Sets the invited teacher first name value.""" - self._invited_teacher_first_name_plain = ( - SchoolTeacherInvitationModelManager.normalize_first_name( - value, lower=False - ) - ) + # 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") # -------------------------------------------------------------------------- # 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", verbose_name=_("invited teacher last name"), @@ -391,18 +378,19 @@ def invited_teacher_last_name(self): @invited_teacher_last_name.setter def invited_teacher_last_name(self, value: str): """Sets the invited teacher last name value.""" - self._invited_teacher_last_name_plain = 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") # -------------------------------------------------------------------------- # 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", verbose_name=_("invited teacher email"), @@ -418,9 +406,13 @@ def invited_teacher_email(self): @invited_teacher_email.setter def invited_teacher_email(self, value: str): """Sets the invited teacher email value.""" - self._invited_teacher_email_plain = ( - SchoolTeacherInvitationModelManager.normalize_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/school.py b/codeforlife/user/models/school.py index 4be1f781..94a559d1 100644 --- a/codeforlife/user/models/school.py +++ b/codeforlife/user/models/school.py @@ -5,6 +5,7 @@ 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 _ @@ -12,6 +13,7 @@ from ...models import DataEncryptionKeyModel from ...models.fields import EncryptedTextField, Sha256Field +from ...models.fields.decorators import validated_field_setter from ...types import Validators from ...validators import UnicodeAlphanumericCharSetValidator @@ -23,15 +25,6 @@ TypedModelMeta = object -# 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.""" @@ -65,7 +58,7 @@ class School(DataEncryptionKeyModel): associated_data = "school" field_aliases = { - "name": {"_name_plain", "_name_enc", "_name_hash"}, + "name": {"_name_hash", "_name_enc"}, } # -------------------------------------------------------------------------- @@ -76,15 +69,11 @@ class School(DataEncryptionKeyModel): _name_hash = Sha256Field( verbose_name=_("name hash"), db_column="name_hash", + unique=True, normalize=lambda name: SchoolModelManager.normalize_name( name, lower=True ), ) - _name_plain: str - _name_plain = models.CharField( # type: ignore[assignment] - max_length=200, - unique=True, - ) _name_enc = EncryptedTextField( associated_data="name", verbose_name=_("name"), @@ -94,15 +83,23 @@ class School(DataEncryptionKeyModel): ), ) + name_validators: Validators = [ + MaxLengthValidator(200), + UnicodeAlphanumericCharSetValidator( + spaces=True, + special_chars="'.", + ), + ] + @property def name(self): """Get the school's name.""" 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 = SchoolModelManager.normalize_name(value, lower=False) EncryptedTextField.set(self, value, "_name_enc") Sha256Field.set(self, value, "_name_hash") 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/__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/contactable.py b/codeforlife/user/models/user/contactable.py index 5b5c52f4..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 @@ -22,10 +20,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 diff --git a/codeforlife/user/models/user/user.py b/codeforlife/user/models/user/user.py index baa65127..9567bc24 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 @@ -35,21 +37,6 @@ TypedModelMeta = object -# 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") @@ -107,11 +94,11 @@ def normalize_first_name(cls, first_name: str, lower=True): # pylint: disable=missing-function-docstring - # 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 @@ -142,19 +129,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] @@ -173,19 +156,7 @@ class User(AbstractBaseUser, PermissionsMixin, DataEncryptionKeyModel): _username_hash = Sha256Field( verbose_name=_("username hash"), db_column="username_hash", - ) - _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", @@ -193,15 +164,20 @@ class User(AbstractBaseUser, PermissionsMixin, DataEncryptionKeyModel): verbose_name=_("username"), ) + username_validators: Validators = [ + MaxLengthValidator(150), + UnicodeUsernameValidator(), + ] + @property def username(self): """The user's username.""" 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") @@ -216,9 +192,6 @@ def username(self, value: str): first_name, lower=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", @@ -228,17 +201,23 @@ def username(self, value: str): ), ) + first_name_validators: Validators = [ + MaxLengthValidator(150), + UnicodeAlphanumericCharSetValidator( + spaces=True, + special_chars="-'", + ), + ] + @property def first_name(self): """The user's first name.""" 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 = UserManager.normalize_first_name( - value, lower=False - ) EncryptedTextField.set(self, value, "_first_name_enc") Sha256Field.set(self, value, "_first_name_hash") @@ -246,24 +225,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", 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.""" return EncryptedTextField.get(self, "_last_name_enc") @last_name.setter + @validated_field_setter(*last_name_validators, blank=True) def last_name(self, value: str): """Set the user's last name.""" - self._last_name_plain = value EncryptedTextField.set(self, value, "_last_name_enc") # -------------------------------------------------------------------------- @@ -275,7 +259,6 @@ def last_name(self, value: str): db_column="email_hash", normalize=UserManager.normalize_email, ) - _email_plain = models.EmailField(_("email address"), blank=True) _email_enc = EncryptedTextField( associated_data="email", db_column="email_enc", @@ -283,15 +266,17 @@ def last_name(self, value: str): normalize=UserManager.normalize_email, ) + email_validators: Validators = [validate_email, MaxLengthValidator(254)] + @property def email(self): """The user's email address.""" return EncryptedTextField.get(self, "_email_enc") @email.setter + @validated_field_setter(*email_validators, blank=True) def email(self, value: str): """Set the user's email address.""" - self._email_plain = self.__class__.objects.normalize_email(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, )