From 9c38ec39cd349a96c0544a4005bdd338c3b07274 Mon Sep 17 00:00:00 2001 From: Sidney Richards Date: Thu, 12 Dec 2024 16:08:02 +0100 Subject: [PATCH] Add slug field validation --- django_setup_configuration/fields.py | 6 +++- testapp/models.py | 1 + tests/test_django_model_ref_field.py | 54 ++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/django_setup_configuration/fields.py b/django_setup_configuration/fields.py index 4c40a52..15dc50b 100644 --- a/django_setup_configuration/fields.py +++ b/django_setup_configuration/fields.py @@ -7,6 +7,7 @@ from django.db import models from django.db.models.fields import NOT_PROVIDED, Field +from pydantic import constr from pydantic.fields import FieldInfo @@ -47,6 +48,9 @@ class UNMAPPED_DJANGO_FIELD: pass +_SLUG_RE = r"^[-a-zA-Z0-9_]+\z" + + class DjangoModelRefInfo(FieldInfo): """ A FieldInfo representing a reference to a field on a Django model. @@ -142,8 +146,8 @@ def _get_python_type( models.TextField: str, models.EmailField: str, models.URLField: str, - models.SlugField: str, models.UUIDField: str, + models.SlugField: constr(pattern=_SLUG_RE), # Integer-based fields models.AutoField: int, models.SmallAutoField: int, diff --git a/testapp/models.py b/testapp/models.py index dcecc3f..d0eb7cf 100644 --- a/testapp/models.py +++ b/testapp/models.py @@ -14,6 +14,7 @@ class TestModel(models.Model): nullable_str = models.CharField(null=True, blank=False, max_length=1) nullable_and_blank_str = models.CharField(null=True, blank=False, max_length=1) blank_str = models.CharField(null=False, blank=True, max_length=1) + slug = models.SlugField() field_with_help_text = models.IntegerField(help_text="This is the help text") field_with_verbose_name = models.IntegerField(verbose_name="The Verbose Name") diff --git a/tests/test_django_model_ref_field.py b/tests/test_django_model_ref_field.py index c64af66..322dc47 100644 --- a/tests/test_django_model_ref_field.py +++ b/tests/test_django_model_ref_field.py @@ -1,6 +1,10 @@ from typing import Literal +from django.core.exceptions import ValidationError as DjangoValidationError +from django.core.validators import validate_slug + import pytest +from pydantic import ValidationError from pydantic.fields import PydanticUndefined from django_setup_configuration.fields import DjangoModelRef @@ -51,6 +55,56 @@ class Config(ConfigurationModel): assert field.is_required() is True +@pytest.mark.parametrize( + "invalid_values", + ( + "", + "hello world", + "user@email.com", + "$price", + "my.variable", + "résumé", + "hello!", + "!", + "#", + "+", + ), +) +def test_slug_validation_fails_on_both_pydantic_and_django(invalid_values): + + class Config(ConfigurationModel): + slug = DjangoModelRef(TestModel, "slug") + + with pytest.raises(ValidationError): + Config(slug=invalid_values) + + with pytest.raises(DjangoValidationError): + validate_slug(invalid_values) + + +@pytest.mark.parametrize( + "valid_values", + ( + "a", + "foo-bar", + "foo_bar", + "foo_bar_baz", + "foo-bar-baz", + "fO0-B4r-Baz", + "foo-bar-baz", + "foobarbaz", + "FooBarBaz", + ), +) +def test_slug_validation_succeeds_on_both_pydantic_and_django(valid_values): + + class Config(ConfigurationModel): + slug = DjangoModelRef(TestModel, "slug") + + Config.model_validate(dict(slug=valid_values)) + validate_slug(valid_values) # does not raise + + def test_no_default_makes_field_required(): class Config(ConfigurationModel):