Skip to content

fix: char set validators #154

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Mar 25, 2025
6 changes: 4 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ jobs:
if: |
github.repository_owner_id == 2088731 &&
github.ref_name == github.event.repository.default_branch
env:
CONFIG: releaserc.toml
steps:
- name: 🛫 Checkout
uses: actions/checkout@v4
Expand Down Expand Up @@ -59,7 +61,7 @@ jobs:
run: |
PYTHON_SCRIPT="from codeforlife import __version__; print(__version__)"
echo OLD_VERSION=$(python -c "$PYTHON_SCRIPT") >> $GITHUB_ENV
semantic-release version
semantic-release --config=${{ env.CONFIG }} version
echo NEW_VERSION=$(python -c "$PYTHON_SCRIPT") >> $GITHUB_ENV

- name: 🏗️ Build Distributions
Expand All @@ -70,7 +72,7 @@ jobs:
if: env.OLD_VERSION != env.NEW_VERSION
env:
GH_TOKEN: ${{ secrets.CFL_BOT_GH_TOKEN }}
run: semantic-release publish
run: semantic-release --config=${{ env.CONFIG }} publish

- name: 🚀 Publish to PyPI
if: env.OLD_VERSION != env.NEW_VERSION
Expand Down
10 changes: 5 additions & 5 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"black-formatter.args": [
"--config",
"pyproject.toml"
"../configs/backend/pyproject.toml"
],
"black-formatter.path": [
".venv/bin/python",
Expand Down Expand Up @@ -37,23 +37,23 @@
"**/__pycache__": true
},
"isort.args": [
"--settings-file=pyproject.toml"
"--settings-file=../configs/backend/pyproject.toml"
],
"isort.path": [
".venv/bin/python",
"-m",
"isort"
],
"mypy-type-checker.args": [
"--config-file=pyproject.toml"
"--config-file=../configs/backend/pyproject.toml"
],
"mypy-type-checker.path": [
".venv/bin/python",
"-m",
"mypy"
],
"pylint.args": [
"--rcfile=pyproject.toml"
"--rcfile=../configs/backend/pyproject.toml"
],
"pylint.path": [
".venv/bin/python",
Expand All @@ -65,7 +65,7 @@
"-n=auto",
"--cov=.",
"--cov-report=html",
"-c=pyproject.toml",
"-c=../configs/backend/pyproject.toml",
"."
],
"python.testing.pytestEnabled": true,
Expand Down
2 changes: 2 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ django-storages = {version = "==1.14.4", extras = ["s3"]}
pyotp = "==2.9.0"
python-dotenv = "==1.0.1"
psycopg2-binary = "==2.9.9"
regex = "==2024.11.6"
requests = "==2.32.2"
gunicorn = "==23.0.0"
uvicorn-worker = "==0.2.0"
Expand Down Expand Up @@ -48,6 +49,7 @@ isort = "==5.13.2"
mypy = "==1.6.1"
django-stubs = {version = "==4.2.6", extras = ["compatible-mypy"]}
djangorestframework-stubs = {version = "==3.14.4", extras = ["compatible-mypy"]}
types-regex = "==2024.11.6.*"

[requires]
python_version = "3.12"
390 changes: 250 additions & 140 deletions Pipfile.lock

Large diffs are not rendered by default.

11 changes: 0 additions & 11 deletions codecov.yml

This file was deleted.

1 change: 0 additions & 1 deletion codeforlife/settings/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
SERVICE_PORT = int(os.getenv("SERVICE_PORT", "8000"))

# The base url of the current service.
# The root service does not need its name included in the base url.
SERVICE_BASE_URL = f"{SERVICE_PROTOCOL}://{SERVICE_DOMAIN}:{SERVICE_PORT}"

# The domain without the last level and a preceding dot.
Expand Down
1 change: 1 addition & 0 deletions codeforlife/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@
from .model_view_set import BaseModelViewSetTestCase, ModelViewSetTestCase
from .model_view_set_client import BaseModelViewSetClient, ModelViewSetClient
from .test import Client, TestCase
from .validator import RegexValidatorTestCase, ValidatorTestCase
30 changes: 30 additions & 0 deletions codeforlife/tests/validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
© Ocado Group
Created on 14/03/2025 at 10:04:16(+00:00).
"""

import typing as t

from django.core.validators import RegexValidator

from .test import TestCase

AnyValidator = t.TypeVar("AnyValidator", bound=t.Callable)
AnyRegexValidator = t.TypeVar("AnyRegexValidator", bound=RegexValidator)


class ValidatorTestCase(TestCase, t.Generic[AnyValidator]):
"""Test case with utilities for testing validators."""

validator_class: t.Type[AnyValidator]


class RegexValidatorTestCase(
ValidatorTestCase[AnyRegexValidator], t.Generic[AnyRegexValidator]
):
"""Test case with utilities for testing regex validators."""

def assert_raises_validation_error(self, *args, **kwargs):
return super().assert_raises_validation_error(
self.validator_class.code, *args, **kwargs
)
2 changes: 2 additions & 0 deletions codeforlife/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
DataDict = t.Dict[str, t.Any]
OrderedDataDict = t.OrderedDict[str, t.Any]

Validators = t.Sequence[t.Callable]


def get_arg(cls: t.Type[t.Any], index: int, orig_base: int = 0):
"""Get a type arg from a class.
Expand Down
6 changes: 4 additions & 2 deletions codeforlife/user/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"""

from .auth_factor import AuthFactor
from .klass import Class
from .klass import Class, class_name_validators
from .otp_bypass_token import OtpBypassToken
from .school import School
from .school import School, school_name_validators
from .session import Session
from .session_auth_factor import SessionAuthFactor
from .student import Independent, Student
Expand Down Expand Up @@ -35,4 +35,6 @@
TypedUser,
User,
UserProfile,
user_first_name_validators,
user_last_name_validators,
)
9 changes: 9 additions & 0 deletions codeforlife/user/models/auth_factor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@

import typing as t

from django.core.validators import MaxLengthValidator, MinLengthValidator
from django.db import models
from django.db.models.query import QuerySet
from django.utils.translation import gettext_lazy as _

from ...types import Validators
from ...validators import AsciiNumericCharSetValidator
from .user import User

if t.TYPE_CHECKING: # pragma: no cover
Expand All @@ -20,6 +23,12 @@ class AuthFactor(models.Model):

sessions: QuerySet["SessionAuthFactor"]

otp_validators: Validators = [
AsciiNumericCharSetValidator(),
MinLengthValidator(6),
MaxLengthValidator(6),
]

class Type(models.TextChoices):
"""The type of authentication factor."""

Expand Down
19 changes: 19 additions & 0 deletions codeforlife/user/models/klass.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,22 @@

# pylint: disable-next=unused-import
from common.models import Class # type: ignore[import-untyped]
from django.core.validators import MaxLengthValidator, MinLengthValidator

from ...validators import (
UnicodeAlphanumericCharSetValidator,
UppercaseAsciiAlphanumericCharSetValidator,
)

class_access_code_validators = [
MinLengthValidator(5),
MaxLengthValidator(5),
UppercaseAsciiAlphanumericCharSetValidator(),
]

class_name_validators = [
UnicodeAlphanumericCharSetValidator(
spaces=True,
special_chars="-_",
)
]
11 changes: 11 additions & 0 deletions codeforlife/user/models/otp_bypass_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@

from cryptography.fernet import Fernet
from django.conf import settings
from django.core.validators import MaxLengthValidator, MinLengthValidator
from django.db import models
from django.db.utils import IntegrityError
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _

from ...types import Validators
from ...validators import CharSetValidatorBuilder
from .user import User

if t.TYPE_CHECKING:
Expand All @@ -29,6 +32,14 @@ class OtpBypassToken(models.Model):
length = 8
allowed_chars = string.ascii_lowercase
max_count = 10
validators: Validators = [
MinLengthValidator(length),
MaxLengthValidator(length),
CharSetValidatorBuilder(
allowed_chars,
"lowercase alpha characters (a-z)",
),
]

# pylint: disable-next=missing-class-docstring,too-few-public-methods
class Manager(models.Manager["OtpBypassToken"]):
Expand Down
9 changes: 9 additions & 0 deletions codeforlife/user/models/school.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,12 @@

# pylint: disable-next=unused-import
from common.models import School # type: ignore[import-untyped]

from ...validators import UnicodeAlphanumericCharSetValidator

school_name_validators = [
UnicodeAlphanumericCharSetValidator(
spaces=True,
special_chars="'.",
)
]
16 changes: 16 additions & 0 deletions codeforlife/user/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from ... import mail
from ...models import AbstractBaseUser
from ...validators import UnicodeAlphanumericCharSetValidator
from .klass import Class
from .school import School

Expand All @@ -35,6 +36,21 @@
TypedModelMeta = object


# TODO: add to model validators in new schema.
user_first_name_validators = [
UnicodeAlphanumericCharSetValidator(
spaces=True,
special_chars="-'",
)
]
user_last_name_validators = [
UnicodeAlphanumericCharSetValidator(
spaces=True,
special_chars="-'",
)
]


# TODO: remove in new schema
class _AbstractBaseUser(AbstractBaseUser):
password: str = None # type: ignore[assignment]
Expand Down
8 changes: 8 additions & 0 deletions codeforlife/user/serializers/klass.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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
Expand All @@ -19,6 +20,13 @@ class ClassSerializer(ModelSerializer[RequestUser, Class]):
read_only=True,
)

# TODO: add to model validators in new schema.
name = serializers.CharField(
validators=class_name_validators,
max_length=200,
read_only=True,
)

read_classmates_data = serializers.BooleanField(
source="classmates_data_viewable",
read_only=True,
Expand Down
8 changes: 8 additions & 0 deletions codeforlife/user/serializers/school.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,20 @@
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


class SchoolSerializer(ModelSerializer[RequestUser, School]):
# TODO: add to model validators in new schema.
name = serializers.CharField(
validators=school_name_validators,
max_length=200,
read_only=True,
)

uk_county = serializers.CharField(source="county", read_only=True)

class Meta:
Expand Down
23 changes: 22 additions & 1 deletion codeforlife/user/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
from rest_framework import serializers

from ...serializers import ModelSerializer
from ..models import AnyUser, Student, Teacher, User
from ..models import (
AnyUser,
Student,
Teacher,
User,
user_first_name_validators,
user_last_name_validators,
)
from .student import StudentSerializer
from .teacher import TeacherSerializer

Expand All @@ -21,6 +28,20 @@
class BaseUserSerializer(
ModelSerializer[RequestUser, AnyUser], t.Generic[AnyUser]
):
# TODO: add to model validators in new schema.
first_name = serializers.CharField(
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,
max_length=150,
read_only=True,
)

requesting_to_join_class = serializers.CharField(
source="new_student.pending_class_request",
read_only=True,
Expand Down
1 change: 1 addition & 0 deletions codeforlife/user/views/school.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class SchoolViewSet(ModelViewSet[User, School]):
http_method_names = ["get"]
serializer_class = SchoolSerializer

# pylint: disable-next=missing-function-docstring
def get_permissions(self):
# No one is allowed to list schools.
if self.action == "list":
Expand Down
7 changes: 7 additions & 0 deletions codeforlife/validators/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
© Ocado Group
Created on 14/03/2025 at 08:33:12(+00:00).
"""

from .char_set import *
from .enhanced_regex import EnhancedRegexValidator
Loading