From ab9c56c74961d3105265b745edecb6b3f82853e0 Mon Sep 17 00:00:00 2001 From: Louis-Adam Date: Tue, 3 Dec 2024 16:18:03 +0100 Subject: [PATCH 1/7] add Nh3Char(models.CharField) - rename Nh3Field in Nh3Text and deprecate the old naming --- .gitignore | 1 + src/django_nh3/models.py | 78 +++++++++- ...est_models.py => test_models_charfield.py} | 12 +- tests/test_models_textfield.py | 147 ++++++++++++++++++ 4 files changed, 231 insertions(+), 7 deletions(-) rename tests/{test_models.py => test_models_charfield.py} (94%) create mode 100644 tests/test_models_textfield.py diff --git a/.gitignore b/.gitignore index b6e4761..224c7ac 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST +.idea/ # PyInstaller # Usually these files are written by a python script from a template diff --git a/src/django_nh3/models.py b/src/django_nh3/models.py index 7ca0b20..c648b0e 100644 --- a/src/django_nh3/models.py +++ b/src/django_nh3/models.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from collections.abc import Callable from typing import Any @@ -13,7 +14,82 @@ from . import forms -class Nh3Field(models.TextField): +class Nh3Text(models.TextField): + def __init__( + self, + attributes: dict[str, set[str]] = {}, + attribute_filter: Callable[[str, str, str], str] | None = None, + clean_content_tags: set[str] = set(), + link_rel: str = "", + strip_comments: bool = False, + tags: set[str] = set(), + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__(*args, **kwargs) + + self.nh3_options = { + "attributes": attributes, + "attribute_filter": attribute_filter, + "clean_content_tags": clean_content_tags, + "link_rel": link_rel, + "strip_comments": strip_comments, + "tags": tags, + } + + def formfield( + self, form_class: FormField = forms.Nh3Field, **kwargs: Any + ) -> FormField: + """Makes the field for a ModelForm""" + + # If field doesn't have any choices add kwargs expected by Nh3Field. + if not self.choices: + kwargs.update( + { + "max_length": self.max_length, + "attributes": self.nh3_options.get("attributes"), + "attribute_filter": self.nh3_options.get("attribute_filter"), + "clean_content_tags": self.nh3_options.get("clean_content_tags"), + "link_rel": self.nh3_options.get("link_rel"), + "strip_comments": self.nh3_options.get("strip_comments"), + "tags": self.nh3_options.get("tags"), + "required": not self.blank, + } + ) + + return super().formfield(form_class=form_class, **kwargs) + + def pre_save(self, model_instance: Model, add: bool) -> Any: + data = getattr(model_instance, self.attname) + if data is None: + return data + clean_value = nh3.clean(data, **self.nh3_options) if data else "" + setattr(model_instance, self.attname, mark_safe(clean_value)) + return clean_value + + def from_db_value( + self, + value: Any, + expression: Expression, + connection: BaseDatabaseWrapper, + ) -> Any: + if value is None: + return value + # Values are sanitised before saving, so any value returned from the DB + # is safe to render unescaped. + return mark_safe(value) + + +def Nh3Field(*args: Any, **kwargs: Any) -> Nh3Text: + warnings.warn( + "Nh3Field is deprecated, use Nh3Text instead", + DeprecationWarning, + stacklevel=2, + ) + return Nh3Text(*args, **kwargs) + + +class Nh3Char(models.CharField): def __init__( self, attributes: dict[str, set[str]] = {}, diff --git a/tests/test_models.py b/tests/test_models_charfield.py similarity index 94% rename from tests/test_models.py rename to tests/test_models_charfield.py index 885abe5..8e16af7 100644 --- a/tests/test_models.py +++ b/tests/test_models_charfield.py @@ -3,17 +3,17 @@ from django.test import TestCase from django.utils.safestring import SafeString -from django_nh3.models import Nh3Field +from django_nh3.models import Nh3Char class Nh3Content(models.Model): """NH3 test model""" - content = Nh3Field( + content = Nh3Char( strip_comments=True, ) - blank_field = Nh3Field(blank=True) - null_field = Nh3Field(blank=True, null=True) + blank_field = Nh3Char(blank=True) + null_field = Nh3Char(blank=True, null=True) class Nh3ContentModelForm(ModelForm): @@ -28,8 +28,8 @@ class Nh3NullableContent(models.Model): """NH3 test model""" CHOICES = (("f", "first choice"), ("s", "second choice")) - choice = Nh3Field(choices=CHOICES, blank=True) - content = Nh3Field(blank=True, null=True) + choice = Nh3Char(choices=CHOICES, blank=True) + content = Nh3Char(blank=True, null=True) class Nh3NullableContentModelForm(ModelForm): diff --git a/tests/test_models_textfield.py b/tests/test_models_textfield.py new file mode 100644 index 0000000..f474b50 --- /dev/null +++ b/tests/test_models_textfield.py @@ -0,0 +1,147 @@ +from django.db import models +from django.forms import ModelForm +from django.test import TestCase +from django.utils.safestring import SafeString + +from django_nh3.models import Nh3Text + + +class Nh3Content(models.Model): + """NH3 test model""" + + content = Nh3Text( + strip_comments=True, + ) + blank_field = Nh3Text(blank=True) + null_field = Nh3Text(blank=True, null=True) + + +class Nh3ContentModelForm(ModelForm): + """NH3 test model form""" + + class Meta: + model = Nh3Content + fields = ["content"] + + +class Nh3NullableContent(models.Model): + """NH3 test model""" + + CHOICES = (("f", "first choice"), ("s", "second choice")) + choice = Nh3Text(choices=CHOICES, blank=True) + content = Nh3Text(blank=True, null=True) + + +class Nh3NullableContentModelForm(ModelForm): + """NH3 test model form""" + + class Meta: + model = Nh3NullableContent + fields = ["choice"] + + +class TestNh3ModelField(TestCase): + """Test model field""" + + def test_cleaning(self): + """Test values are sanitized""" + test_data = { + "html_data": "

Heading

", + "no_html": "Heading", + "html_comment": "", + } + expected_values = { + "html_data": "Heading", + "no_html": "Heading", + "html_comment": "", + } + + for key, value in test_data.items(): + obj = Nh3Content.objects.create(content=value) + self.assertEqual(obj.content, expected_values[key]) + + def test_retrieved_values_are_template_safe(self): + obj = Nh3Content.objects.create(content="some content") + obj.refresh_from_db() + self.assertIsInstance(obj.content, SafeString) + obj = Nh3Content.objects.create(content="") + obj.refresh_from_db() + self.assertIsInstance(obj.content, SafeString) + + def test_saved_values_are_template_safe(self): + obj = Nh3Content(content="some content") + obj.save() + self.assertIsInstance(obj.content, SafeString) + obj = Nh3Content(content="") + obj.save() + self.assertIsInstance(obj.content, SafeString) + + def test_saved_none_values_are_none(self): + obj = Nh3Content(null_field=None) + obj.save() + self.assertIsNone(obj.null_field) + + +class TestNh3NullableModelField(TestCase): + """Test model field""" + + def test_cleaning(self): + """Test values are sanitized""" + test_data = { + "none": None, + "empty": "", + "whitespaces": " ", + "linebreak": "\n", + } + expected_values = { + "none": None, + "empty": "", + "whitespaces": " ", + "linebreak": "\n", + } + + for key, value in test_data.items(): + obj = Nh3NullableContent.objects.create(content=value) + self.assertEqual(obj.content, expected_values[key]) + + +class TestNh3ModelFormField(TestCase): + """Test model form field""" + + def test_cleaning(self): + """Test values are sanitized""" + test_data = { + "html_data": "

Heading

", + "no_html": "Heading", + "spacing": " Heading ", + } + expected_values = { + "html_data": "Heading", + "no_html": "Heading", + "spacing": "Heading", + } + + for key, value in test_data.items(): + form = Nh3ContentModelForm(data={"content": value}) + self.assertTrue(form.is_valid()) + obj = form.save() + self.assertEqual(obj.content, expected_values[key]) + + def test_stripped_comments(self): + """Content field strips comments so ensure they aren't allowed""" + + self.assertFalse( + Nh3ContentModelForm( + data={"content": ""} + ).is_valid() + ) + + def test_field_choices(self): + """Content field strips comments so ensure they aren't allowed""" + test_data = dict(Nh3NullableContent.CHOICES) + + for key, value in test_data.items(): + form = Nh3NullableContentModelForm(data={"choice": key}) + self.assertTrue(form.is_valid()) + obj = form.save() + self.assertEqual(obj.get_choice_display(), value) From b8c973cbca705fbc430f8d49a15b42ece99a62e6 Mon Sep 17 00:00:00 2001 From: Louis-Adam Date: Wed, 11 Dec 2024 16:33:32 +0100 Subject: [PATCH 2/7] change DeprecatedWarning to FutureWarning --- src/django_nh3/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/django_nh3/models.py b/src/django_nh3/models.py index c648b0e..e101f68 100644 --- a/src/django_nh3/models.py +++ b/src/django_nh3/models.py @@ -81,10 +81,10 @@ def from_db_value( def Nh3Field(*args: Any, **kwargs: Any) -> Nh3Text: - warnings.warn( - "Nh3Field is deprecated, use Nh3Text instead", - DeprecationWarning, - stacklevel=2, + warnings.filterwarnings( + action="default", + message="Nh3Field is deprecated, use Nh3Text instead", + category=DeprecationWarning, ) return Nh3Text(*args, **kwargs) From 0b3c721970c18c8f6ba00819d21c4ffc178004a2 Mon Sep 17 00:00:00 2001 From: Louis-Adam Date: Wed, 11 Dec 2024 16:34:13 +0100 Subject: [PATCH 3/7] change DeprecatedWarning to FutureWarning --- src/django_nh3/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/django_nh3/models.py b/src/django_nh3/models.py index e101f68..5523f17 100644 --- a/src/django_nh3/models.py +++ b/src/django_nh3/models.py @@ -84,7 +84,7 @@ def Nh3Field(*args: Any, **kwargs: Any) -> Nh3Text: warnings.filterwarnings( action="default", message="Nh3Field is deprecated, use Nh3Text instead", - category=DeprecationWarning, + category=FutureWarning, ) return Nh3Text(*args, **kwargs) From 23768a99869bd7cab1a57b3be0a616b28f8d9e89 Mon Sep 17 00:00:00 2001 From: blag Date: Mon, 19 May 2025 15:00:12 -0600 Subject: [PATCH 4/7] Keep .gitignore clean --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 224c7ac..b6e4761 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,6 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST -.idea/ # PyInstaller # Usually these files are written by a python script from a template From d1d5ab2465d79190588475f48b4ec65839056244 Mon Sep 17 00:00:00 2001 From: blag Date: Mon, 19 May 2025 14:59:57 -0600 Subject: [PATCH 5/7] Split Nh3CharField and Nh3TextField --- pyproject.toml | 1 + src/django_nh3/models.py | 91 +++++++------------------------- tests/forms.py | 41 ++++++++++++++ tests/migrations/0001_initial.py | 61 ++++++++++++++++++--- tests/models.py | 41 ++++++++++++++ tests/test_models_charfield.py | 67 ++++++----------------- tests/test_models_textfield.py | 65 ++++++----------------- 7 files changed, 187 insertions(+), 180 deletions(-) create mode 100644 tests/forms.py create mode 100644 tests/models.py diff --git a/pyproject.toml b/pyproject.toml index 485c05b..4107cf8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ classifiers = [ dependencies = [ "django>=3.2", "nh3", + "typing-extensions", ] urls.Changelog = "https://github.com/marksweb/django-nh3/blob/main/CHANGELOG.rst" diff --git a/src/django_nh3/models.py b/src/django_nh3/models.py index 5523f17..ebb30ee 100644 --- a/src/django_nh3/models.py +++ b/src/django_nh3/models.py @@ -1,8 +1,7 @@ -from __future__ import annotations - import warnings from collections.abc import Callable from typing import Any +from typing_extensions import deprecated import nh3 from django.db import models @@ -14,7 +13,7 @@ from . import forms -class Nh3Text(models.TextField): +class Nh3FieldMixin: def __init__( self, attributes: dict[str, set[str]] = {}, @@ -80,76 +79,24 @@ def from_db_value( return mark_safe(value) -def Nh3Field(*args: Any, **kwargs: Any) -> Nh3Text: - warnings.filterwarnings( - action="default", - message="Nh3Field is deprecated, use Nh3Text instead", - category=FutureWarning, - ) - return Nh3Text(*args, **kwargs) - +class Nh3TextField(Nh3FieldMixin, models.TextField): + pass -class Nh3Char(models.CharField): - def __init__( - self, - attributes: dict[str, set[str]] = {}, - attribute_filter: Callable[[str, str, str], str] | None = None, - clean_content_tags: set[str] = set(), - link_rel: str = "", - strip_comments: bool = False, - tags: set[str] = set(), - *args: Any, - **kwargs: Any, - ) -> None: - super().__init__(*args, **kwargs) - self.nh3_options = { - "attributes": attributes, - "attribute_filter": attribute_filter, - "clean_content_tags": clean_content_tags, - "link_rel": link_rel, - "strip_comments": strip_comments, - "tags": tags, - } +class Nh3CharField(Nh3FieldMixin, models.CharField): + pass - def formfield( - self, form_class: FormField = forms.Nh3Field, **kwargs: Any - ) -> FormField: - """Makes the field for a ModelForm""" - # If field doesn't have any choices add kwargs expected by Nh3Field. - if not self.choices: - kwargs.update( - { - "max_length": self.max_length, - "attributes": self.nh3_options.get("attributes"), - "attribute_filter": self.nh3_options.get("attribute_filter"), - "clean_content_tags": self.nh3_options.get("clean_content_tags"), - "link_rel": self.nh3_options.get("link_rel"), - "strip_comments": self.nh3_options.get("strip_comments"), - "tags": self.nh3_options.get("tags"), - "required": not self.blank, - } - ) - - return super().formfield(form_class=form_class, **kwargs) - - def pre_save(self, model_instance: Model, add: bool) -> Any: - data = getattr(model_instance, self.attname) - if data is None: - return data - clean_value = nh3.clean(data, **self.nh3_options) if data else "" - setattr(model_instance, self.attname, mark_safe(clean_value)) - return clean_value - - def from_db_value( - self, - value: Any, - expression: Expression, - connection: BaseDatabaseWrapper, - ) -> Any: - if value is None: - return value - # Values are sanitised before saving, so any value returned from the DB - # is safe to render unescaped. - return mark_safe(value) +@deprecated("Use Nh3TextField instead") +class Nh3Field(Nh3FieldMixin, models.TextField): + """ + .. deprecated:: 0.2.0 + Use :class:`Nh3TextField` instead. + """ + def __init__(self, *args, **kwargs): + warnings.warn( + "Nh3Field is deprecated and will be removed in a future version. Use Nh3TextField instead.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) diff --git a/tests/forms.py b/tests/forms.py new file mode 100644 index 0000000..0788a5b --- /dev/null +++ b/tests/forms.py @@ -0,0 +1,41 @@ +from django.forms import ModelForm + +from .models import ( + Nh3CharFieldContent, + Nh3CharFieldNullableContent, + Nh3TextFieldContent, + Nh3TextFieldNullableContent, +) + + + +class Nh3CharFieldContentModelForm(ModelForm): + """NH3 test model form""" + + class Meta: + model = Nh3CharFieldContent + fields = ["content"] + + +class Nh3CharFieldNullableContentModelForm(ModelForm): + """NH3 test model form""" + + class Meta: + model = Nh3CharFieldNullableContent + fields = ["choice"] + + +class Nh3TextFieldContentModelForm(ModelForm): + """NH3 test model form""" + + class Meta: + model = Nh3TextFieldContent + fields = ["content"] + + +class Nh3TextFieldNullableContentModelForm(ModelForm): + """NH3 test model form""" + + class Meta: + model = Nh3TextFieldNullableContent + fields = ["choice"] diff --git a/tests/migrations/0001_initial.py b/tests/migrations/0001_initial.py index f6686b2..f3112c3 100644 --- a/tests/migrations/0001_initial.py +++ b/tests/migrations/0001_initial.py @@ -11,7 +11,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name="Nh3Content", + name="Nh3CharFieldContent", fields=[ ( "id", @@ -22,16 +22,59 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("content", django_nh3.models.Nh3Field()), - ("blank_field", django_nh3.models.Nh3Field(blank=True)), + ("content", django_nh3.models.Nh3CharField(max_length=1000)), + ("blank_field", django_nh3.models.Nh3CharField(blank=True, max_length=1000)), ( "null_field", - django_nh3.models.Nh3Field(blank=True, null=True), + django_nh3.models.Nh3CharField(blank=True, null=True, max_length=1000), ), ], ), migrations.CreateModel( - name="Nh3NullableContent", + name="Nh3TextFieldContent", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("content", django_nh3.models.Nh3TextField(max_length=1000)), + ("blank_field", django_nh3.models.Nh3TextField(blank=True, max_length=1000)), + ( + "null_field", + django_nh3.models.Nh3TextField(blank=True, null=True, max_length=1000), + ), + ], + ), + migrations.CreateModel( + name="Nh3CharFieldNullableContent", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "choice", + django_nh3.models.Nh3CharField( + blank=True, + choices=[("f", "first choice"), ("s", "second choice")], + max_length=1000, + ), + ), + ("content", django_nh3.models.Nh3CharField(blank=True, null=True, max_length=1000)), + ], + ), + migrations.CreateModel( + name="Nh3TextFieldNullableContent", fields=[ ( "id", @@ -44,11 +87,13 @@ class Migration(migrations.Migration): ), ( "choice", - django_nh3.models.Nh3Field( - choices=[("f", "first choice"), ("s", "second choice")] + django_nh3.models.Nh3TextField( + blank=True, + choices=[("f", "first choice"), ("s", "second choice")], + max_length=1000, ), ), - ("content", django_nh3.models.Nh3Field(blank=True, null=True)), + ("content", django_nh3.models.Nh3TextField(blank=True, null=True, max_length=1000)), ], ), ] diff --git a/tests/models.py b/tests/models.py new file mode 100644 index 0000000..0b44b22 --- /dev/null +++ b/tests/models.py @@ -0,0 +1,41 @@ +from django.db import models + +from django_nh3.models import Nh3CharField, Nh3TextField + + +class Nh3CharFieldContent(models.Model): + """NH3 test model""" + + content = Nh3CharField( + strip_comments=True, + max_length=1000, + ) + blank_field = Nh3CharField(blank=True, max_length=1000) + null_field = Nh3CharField(blank=True, null=True, max_length=1000) + + +class Nh3CharFieldNullableContent(models.Model): + """NH3 test model""" + + CHOICES = (("f", "first choice"), ("s", "second choice")) + choice = Nh3CharField(choices=CHOICES, blank=True, max_length=1000) + content = Nh3CharField(blank=True, null=True, max_length=1000) + + +class Nh3TextFieldContent(models.Model): + """NH3 test model""" + + content = Nh3TextField( + strip_comments=True, + max_length=1000, + ) + blank_field = Nh3TextField(blank=True, max_length=1000) + null_field = Nh3TextField(blank=True, null=True, max_length=1000) + + +class Nh3TextFieldNullableContent(models.Model): + """NH3 test model""" + + CHOICES = (("f", "first choice"), ("s", "second choice")) + choice = Nh3TextField(choices=CHOICES, blank=True, max_length=1000) + content = Nh3TextField(blank=True, null=True, max_length=1000) diff --git a/tests/test_models_charfield.py b/tests/test_models_charfield.py index 8e16af7..e6b4e81 100644 --- a/tests/test_models_charfield.py +++ b/tests/test_models_charfield.py @@ -1,46 +1,11 @@ -from django.db import models -from django.forms import ModelForm from django.test import TestCase from django.utils.safestring import SafeString -from django_nh3.models import Nh3Char +from .forms import Nh3CharFieldContentModelForm, Nh3CharFieldNullableContentModelForm +from .models import Nh3CharFieldContent, Nh3CharFieldNullableContent -class Nh3Content(models.Model): - """NH3 test model""" - - content = Nh3Char( - strip_comments=True, - ) - blank_field = Nh3Char(blank=True) - null_field = Nh3Char(blank=True, null=True) - - -class Nh3ContentModelForm(ModelForm): - """NH3 test model form""" - - class Meta: - model = Nh3Content - fields = ["content"] - - -class Nh3NullableContent(models.Model): - """NH3 test model""" - - CHOICES = (("f", "first choice"), ("s", "second choice")) - choice = Nh3Char(choices=CHOICES, blank=True) - content = Nh3Char(blank=True, null=True) - - -class Nh3NullableContentModelForm(ModelForm): - """NH3 test model form""" - - class Meta: - model = Nh3NullableContent - fields = ["choice"] - - -class TestNh3ModelField(TestCase): +class TestNh3ModelCharField(TestCase): """Test model field""" def test_cleaning(self): @@ -57,32 +22,32 @@ def test_cleaning(self): } for key, value in test_data.items(): - obj = Nh3Content.objects.create(content=value) + obj = Nh3CharFieldContent.objects.create(content=value) self.assertEqual(obj.content, expected_values[key]) def test_retrieved_values_are_template_safe(self): - obj = Nh3Content.objects.create(content="some content") + obj = Nh3CharFieldContent.objects.create(content="some content") obj.refresh_from_db() self.assertIsInstance(obj.content, SafeString) - obj = Nh3Content.objects.create(content="") + obj = Nh3CharFieldContent.objects.create(content="") obj.refresh_from_db() self.assertIsInstance(obj.content, SafeString) def test_saved_values_are_template_safe(self): - obj = Nh3Content(content="some content") + obj = Nh3CharFieldContent(content="some content") obj.save() self.assertIsInstance(obj.content, SafeString) - obj = Nh3Content(content="") + obj = Nh3CharFieldContent(content="") obj.save() self.assertIsInstance(obj.content, SafeString) def test_saved_none_values_are_none(self): - obj = Nh3Content(null_field=None) + obj = Nh3CharFieldContent(null_field=None) obj.save() self.assertIsNone(obj.null_field) -class TestNh3NullableModelField(TestCase): +class TestNh3CharFieldNullableModelField(TestCase): """Test model field""" def test_cleaning(self): @@ -101,11 +66,11 @@ def test_cleaning(self): } for key, value in test_data.items(): - obj = Nh3NullableContent.objects.create(content=value) + obj = Nh3CharFieldNullableContent.objects.create(content=value) self.assertEqual(obj.content, expected_values[key]) -class TestNh3ModelFormField(TestCase): +class TestNh3CharFieldModelFormField(TestCase): """Test model form field""" def test_cleaning(self): @@ -122,7 +87,7 @@ def test_cleaning(self): } for key, value in test_data.items(): - form = Nh3ContentModelForm(data={"content": value}) + form = Nh3CharFieldContentModelForm(data={"content": value}) self.assertTrue(form.is_valid()) obj = form.save() self.assertEqual(obj.content, expected_values[key]) @@ -131,17 +96,17 @@ def test_stripped_comments(self): """Content field strips comments so ensure they aren't allowed""" self.assertFalse( - Nh3ContentModelForm( + Nh3CharFieldContentModelForm( data={"content": ""} ).is_valid() ) def test_field_choices(self): """Content field strips comments so ensure they aren't allowed""" - test_data = dict(Nh3NullableContent.CHOICES) + test_data = dict(Nh3CharFieldNullableContent.CHOICES) for key, value in test_data.items(): - form = Nh3NullableContentModelForm(data={"choice": key}) + form = Nh3CharFieldNullableContentModelForm(data={"choice": key}) self.assertTrue(form.is_valid()) obj = form.save() self.assertEqual(obj.get_choice_display(), value) diff --git a/tests/test_models_textfield.py b/tests/test_models_textfield.py index f474b50..3ba11c0 100644 --- a/tests/test_models_textfield.py +++ b/tests/test_models_textfield.py @@ -3,44 +3,11 @@ from django.test import TestCase from django.utils.safestring import SafeString -from django_nh3.models import Nh3Text +from .forms import Nh3TextFieldContentModelForm, Nh3TextFieldNullableContentModelForm +from .models import Nh3TextFieldContent, Nh3TextFieldNullableContent -class Nh3Content(models.Model): - """NH3 test model""" - - content = Nh3Text( - strip_comments=True, - ) - blank_field = Nh3Text(blank=True) - null_field = Nh3Text(blank=True, null=True) - - -class Nh3ContentModelForm(ModelForm): - """NH3 test model form""" - - class Meta: - model = Nh3Content - fields = ["content"] - - -class Nh3NullableContent(models.Model): - """NH3 test model""" - - CHOICES = (("f", "first choice"), ("s", "second choice")) - choice = Nh3Text(choices=CHOICES, blank=True) - content = Nh3Text(blank=True, null=True) - - -class Nh3NullableContentModelForm(ModelForm): - """NH3 test model form""" - - class Meta: - model = Nh3NullableContent - fields = ["choice"] - - -class TestNh3ModelField(TestCase): +class TestNh3TextFieldModelField(TestCase): """Test model field""" def test_cleaning(self): @@ -57,32 +24,32 @@ def test_cleaning(self): } for key, value in test_data.items(): - obj = Nh3Content.objects.create(content=value) + obj = Nh3TextFieldContent.objects.create(content=value) self.assertEqual(obj.content, expected_values[key]) def test_retrieved_values_are_template_safe(self): - obj = Nh3Content.objects.create(content="some content") + obj = Nh3TextFieldContent.objects.create(content="some content") obj.refresh_from_db() self.assertIsInstance(obj.content, SafeString) - obj = Nh3Content.objects.create(content="") + obj = Nh3TextFieldContent.objects.create(content="") obj.refresh_from_db() self.assertIsInstance(obj.content, SafeString) def test_saved_values_are_template_safe(self): - obj = Nh3Content(content="some content") + obj = Nh3TextFieldContent(content="some content") obj.save() self.assertIsInstance(obj.content, SafeString) - obj = Nh3Content(content="") + obj = Nh3TextFieldContent(content="") obj.save() self.assertIsInstance(obj.content, SafeString) def test_saved_none_values_are_none(self): - obj = Nh3Content(null_field=None) + obj = Nh3TextFieldContent(null_field=None) obj.save() self.assertIsNone(obj.null_field) -class TestNh3NullableModelField(TestCase): +class TestNh3TextFieldNullableModelField(TestCase): """Test model field""" def test_cleaning(self): @@ -101,11 +68,11 @@ def test_cleaning(self): } for key, value in test_data.items(): - obj = Nh3NullableContent.objects.create(content=value) + obj = Nh3TextFieldNullableContent.objects.create(content=value) self.assertEqual(obj.content, expected_values[key]) -class TestNh3ModelFormField(TestCase): +class TestNh3TextFieldModelFormField(TestCase): """Test model form field""" def test_cleaning(self): @@ -122,7 +89,7 @@ def test_cleaning(self): } for key, value in test_data.items(): - form = Nh3ContentModelForm(data={"content": value}) + form = Nh3TextFieldContentModelForm(data={"content": value}) self.assertTrue(form.is_valid()) obj = form.save() self.assertEqual(obj.content, expected_values[key]) @@ -131,17 +98,17 @@ def test_stripped_comments(self): """Content field strips comments so ensure they aren't allowed""" self.assertFalse( - Nh3ContentModelForm( + Nh3TextFieldContentModelForm( data={"content": ""} ).is_valid() ) def test_field_choices(self): """Content field strips comments so ensure they aren't allowed""" - test_data = dict(Nh3NullableContent.CHOICES) + test_data = dict(Nh3TextFieldNullableContent.CHOICES) for key, value in test_data.items(): - form = Nh3NullableContentModelForm(data={"choice": key}) + form = Nh3TextFieldNullableContentModelForm(data={"choice": key}) self.assertTrue(form.is_valid()) obj = form.save() self.assertEqual(obj.get_choice_display(), value) From 8439a934bd2da471b876bd7d1e3e414c185a275a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 21:08:51 +0000 Subject: [PATCH 6/7] ci: auto fixes from pre-commit hooks for more information, see https://pre-commit.ci --- src/django_nh3/models.py | 3 ++- tests/forms.py | 1 - tests/migrations/0001_initial.py | 32 ++++++++++++++++++++++++++------ tests/test_models_textfield.py | 2 -- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/django_nh3/models.py b/src/django_nh3/models.py index ebb30ee..75d8cdd 100644 --- a/src/django_nh3/models.py +++ b/src/django_nh3/models.py @@ -1,7 +1,6 @@ import warnings from collections.abc import Callable from typing import Any -from typing_extensions import deprecated import nh3 from django.db import models @@ -9,6 +8,7 @@ from django.db.models import Expression, Model from django.forms import Field as FormField from django.utils.safestring import mark_safe +from typing_extensions import deprecated from . import forms @@ -93,6 +93,7 @@ class Nh3Field(Nh3FieldMixin, models.TextField): .. deprecated:: 0.2.0 Use :class:`Nh3TextField` instead. """ + def __init__(self, *args, **kwargs): warnings.warn( "Nh3Field is deprecated and will be removed in a future version. Use Nh3TextField instead.", diff --git a/tests/forms.py b/tests/forms.py index 0788a5b..72c36ce 100644 --- a/tests/forms.py +++ b/tests/forms.py @@ -8,7 +8,6 @@ ) - class Nh3CharFieldContentModelForm(ModelForm): """NH3 test model form""" diff --git a/tests/migrations/0001_initial.py b/tests/migrations/0001_initial.py index f3112c3..b06e700 100644 --- a/tests/migrations/0001_initial.py +++ b/tests/migrations/0001_initial.py @@ -23,10 +23,15 @@ class Migration(migrations.Migration): ), ), ("content", django_nh3.models.Nh3CharField(max_length=1000)), - ("blank_field", django_nh3.models.Nh3CharField(blank=True, max_length=1000)), + ( + "blank_field", + django_nh3.models.Nh3CharField(blank=True, max_length=1000), + ), ( "null_field", - django_nh3.models.Nh3CharField(blank=True, null=True, max_length=1000), + django_nh3.models.Nh3CharField( + blank=True, null=True, max_length=1000 + ), ), ], ), @@ -43,10 +48,15 @@ class Migration(migrations.Migration): ), ), ("content", django_nh3.models.Nh3TextField(max_length=1000)), - ("blank_field", django_nh3.models.Nh3TextField(blank=True, max_length=1000)), + ( + "blank_field", + django_nh3.models.Nh3TextField(blank=True, max_length=1000), + ), ( "null_field", - django_nh3.models.Nh3TextField(blank=True, null=True, max_length=1000), + django_nh3.models.Nh3TextField( + blank=True, null=True, max_length=1000 + ), ), ], ), @@ -70,7 +80,12 @@ class Migration(migrations.Migration): max_length=1000, ), ), - ("content", django_nh3.models.Nh3CharField(blank=True, null=True, max_length=1000)), + ( + "content", + django_nh3.models.Nh3CharField( + blank=True, null=True, max_length=1000 + ), + ), ], ), migrations.CreateModel( @@ -93,7 +108,12 @@ class Migration(migrations.Migration): max_length=1000, ), ), - ("content", django_nh3.models.Nh3TextField(blank=True, null=True, max_length=1000)), + ( + "content", + django_nh3.models.Nh3TextField( + blank=True, null=True, max_length=1000 + ), + ), ], ), ] diff --git a/tests/test_models_textfield.py b/tests/test_models_textfield.py index 3ba11c0..fc525c8 100644 --- a/tests/test_models_textfield.py +++ b/tests/test_models_textfield.py @@ -1,5 +1,3 @@ -from django.db import models -from django.forms import ModelForm from django.test import TestCase from django.utils.safestring import SafeString From c62591f84c791c32e2d5f07a3a3250df53b5a46d Mon Sep 17 00:00:00 2001 From: blag Date: Mon, 19 May 2025 17:17:54 -0600 Subject: [PATCH 7/7] Make pre-commit happy --- src/django_nh3/models.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/django_nh3/models.py b/src/django_nh3/models.py index 75d8cdd..3674eea 100644 --- a/src/django_nh3/models.py +++ b/src/django_nh3/models.py @@ -42,28 +42,28 @@ def formfield( """Makes the field for a ModelForm""" # If field doesn't have any choices add kwargs expected by Nh3Field. - if not self.choices: + if not self.choices: # type: ignore[attr-defined] kwargs.update( { - "max_length": self.max_length, + "max_length": self.max_length, # type: ignore[attr-defined] "attributes": self.nh3_options.get("attributes"), "attribute_filter": self.nh3_options.get("attribute_filter"), "clean_content_tags": self.nh3_options.get("clean_content_tags"), "link_rel": self.nh3_options.get("link_rel"), "strip_comments": self.nh3_options.get("strip_comments"), "tags": self.nh3_options.get("tags"), - "required": not self.blank, + "required": not self.blank, # type: ignore[attr-defined] } ) - return super().formfield(form_class=form_class, **kwargs) + return super().formfield(form_class=form_class, **kwargs) # type: ignore[misc] def pre_save(self, model_instance: Model, add: bool) -> Any: - data = getattr(model_instance, self.attname) + data = getattr(model_instance, self.attname) # type: ignore[attr-defined] if data is None: return data clean_value = nh3.clean(data, **self.nh3_options) if data else "" - setattr(model_instance, self.attname, mark_safe(clean_value)) + setattr(model_instance, self.attname, mark_safe(clean_value)) # type: ignore[attr-defined] return clean_value def from_db_value( @@ -94,9 +94,10 @@ class Nh3Field(Nh3FieldMixin, models.TextField): Use :class:`Nh3TextField` instead. """ - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] warnings.warn( - "Nh3Field is deprecated and will be removed in a future version. Use Nh3TextField instead.", + "Nh3Field is deprecated and will be removed in a future version. " + "Use Nh3TextField instead.", DeprecationWarning, stacklevel=2, )