diff --git a/.gitignore b/.gitignore index 5c33273..f54a743 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,7 @@ coverage.xml *.log local_settings.py db.sqlite3 +django-bleach.db # Flask stuff: instance/ diff --git a/django_bleach/models.py b/django_bleach/models.py index ed1590b..74ce071 100644 --- a/django_bleach/models.py +++ b/django_bleach/models.py @@ -2,6 +2,7 @@ from bleach import clean +from . import forms from .utils import get_bleach_default_options @@ -27,6 +28,26 @@ def __init__(self, allowed_tags=None, allowed_attributes=None, if strip_comments: self.bleach_kwargs["strip_comments"] = strip_comments + def formfield(self, **kwargs): + """ + Makes field for ModelForm + """ + + # If field doesn't have any choice return BleachField + if not self.choices: + return forms.BleachField( + label=self.verbose_name, + max_length=self.max_length, + allowed_tags=self.bleach_kwargs.get("tags"), + allowed_attributes=self.bleach_kwargs.get("attributes"), + allowed_styles=self.bleach_kwargs.get("styles"), + allowed_protocols=self.bleach_kwargs.get("protocols"), + strip_tags=self.bleach_kwargs.get("strip"), + strip_comments=self.bleach_kwargs.get("strip_comments"), + ) + + return super(BleachField, self).formfield(**kwargs) + def pre_save(self, model_instance, add): data = getattr(model_instance, self.attname) if data: diff --git a/django_bleach/tests/test_modelformfield.py b/django_bleach/tests/test_modelformfield.py new file mode 100644 index 0000000..af74ff1 --- /dev/null +++ b/django_bleach/tests/test_modelformfield.py @@ -0,0 +1,44 @@ +from django import forms +from django.test import TestCase, override_settings +from django_bleach import forms as bleach_forms +from .test_models import BleachContent + + +class BleachContentModelForm(forms.ModelForm): + class Meta: + model = BleachContent + fields = '__all__' + + +class TestModelFormField(TestCase): + @override_settings(BLEACH_DEFAULT_WIDGET='testproject.forms.CustomBleachWidget') + def setUp(self): + model_form = BleachContentModelForm() + self.form_field = model_form.fields['content'] + self.choice_form_field = model_form.fields['choice'] + self.model_field = BleachContent()._meta.get_field('content') + self.default_widget_class = bleach_forms.get_default_widget() + + def test_formfield_type(self): + """ Check content's form field is instance of BleachField + """ + self.assertIsInstance(self.form_field, bleach_forms.BleachField) + + def test_custom_widget(self): + """ Check content form field's widget is instance of default widget + """ + self.assertIsInstance(self.form_field.widget, self.default_widget_class) + + def test_same_allowed_args(self): + """ Check model and form's allowed arguments (tags, attributes, ...) are same + """ + form_allowed_args: dict = self.form_field.bleach_options + model_allowed_args: dict = self.model_field.bleach_kwargs + + self.assertEqual(model_allowed_args, form_allowed_args) + + def test_with_choices(self): + """ Check if choices specified, use TextField's default widget (Select). + """ + form_field_widget = self.choice_form_field.widget.__class__ + self.assertEqual(form_field_widget, forms.widgets.Select) diff --git a/django_bleach/tests/test_models.py b/django_bleach/tests/test_models.py index d426ee9..5a2fbf8 100644 --- a/django_bleach/tests/test_models.py +++ b/django_bleach/tests/test_models.py @@ -10,6 +10,10 @@ class BleachContent(models.Model): """ Bleach test model""" + CHOICES = ( + ('f', 'first choice'), + ('s', 'second choice') + ) content = BleachField( allowed_attributes=ALLOWED_ATTRIBUTES, allowed_protocols=ALLOWED_PROTOCOLS, @@ -18,6 +22,7 @@ class BleachContent(models.Model): strip_comments=True, strip_tags=True ) + choice = BleachField(choices=CHOICES) class TestBleachModelField(TestCase): diff --git a/django_bleach/tests/test_settings.py b/django_bleach/tests/test_settings.py index 55e623a..6a05bc5 100644 --- a/django_bleach/tests/test_settings.py +++ b/django_bleach/tests/test_settings.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from django.core.exceptions import ImproperlyConfigured from django.forms import Textarea -from django.test import TestCase +from django.test import TestCase, override_settings from mock import patch from django_bleach.forms import get_default_widget @@ -53,6 +53,7 @@ def test_strip_comments(self, settings): class TestDefaultWidget(TestCase): """ Test form field widgets """ + @override_settings(BLEACH_DEFAULT_WIDGET='django.forms.widgets.Textarea') def test_default_widget(self): self.assertEqual(get_default_widget(), Textarea) diff --git a/testproject/forms.py b/testproject/forms.py index 112c14a..da343da 100755 --- a/testproject/forms.py +++ b/testproject/forms.py @@ -1,6 +1,7 @@ from django import forms from django_bleach.forms import BleachField +from testproject.models import Person from testproject.constants import ( ALLOWED_ATTRIBUTES, ALLOWED_PROTOCOLS, @@ -10,7 +11,11 @@ class CustomBleachWidget(forms.Textarea): - pass + + def __init__(self, attrs=None): + default_attrs = {'rows': 15, 'cols': 60} + default_attrs.update(attrs or {}) + super().__init__(attrs=default_attrs) class BleachForm(forms.Form): @@ -47,3 +52,10 @@ class BleachForm(forms.Form): allowed_tags=ALLOWED_TAGS, allowed_styles=ALLOWED_STYLES ) + + +class PersonForm(forms.ModelForm): + + class Meta: + model = Person + fields = '__all__' diff --git a/testproject/migrations/0001_initial.py b/testproject/migrations/0001_initial.py new file mode 100644 index 0000000..8ab9bf8 --- /dev/null +++ b/testproject/migrations/0001_initial.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.4 on 2021-06-15 12:39 + +from django.db import migrations, models +import django_bleach.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Person', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=20)), + ('biography', django_bleach.models.BleachField(max_length=100, verbose_name='Person biography')), + ], + ), + ] diff --git a/testproject/migrations/__init__.py b/testproject/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testproject/models.py b/testproject/models.py new file mode 100644 index 0000000..a0a6f64 --- /dev/null +++ b/testproject/models.py @@ -0,0 +1,14 @@ +from django.db import models +from django_bleach.models import BleachField + + +class Person(models.Model): + name = models.CharField(max_length=20) + biography = BleachField( + max_length=100, + verbose_name='Person biography', + allowed_tags=['p', 'a', 'li', 'ul', 'strong'], + allowed_attributes=['class', 'href', 'style'], + allowed_protocols=['http', 'https'], + allowed_styles=['color', 'background-color'] + ) diff --git a/testproject/settings.py b/testproject/settings.py index c91866e..426a27b 100755 --- a/testproject/settings.py +++ b/testproject/settings.py @@ -15,7 +15,7 @@ 'NAME': os.path.join(PROJECT_PATH, 'django-bleach.db') } } - +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' DATABASE_SUPPORTS_TRANSACTIONS = True INSTALLED_APPS = [ @@ -68,3 +68,5 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware' ) + +BLEACH_DEFAULT_WIDGET = 'testproject.forms.CustomBleachWidget' diff --git a/testproject/templates/home.html b/testproject/templates/home.html index 4b10859..ec4dda2 100755 --- a/testproject/templates/home.html +++ b/testproject/templates/home.html @@ -1,4 +1,5 @@ +

Form example

{% csrf_token %} @@ -9,4 +10,5 @@
+ Model form example(Note: you need to migrate database to use it) \ No newline at end of file diff --git a/testproject/templates/model_form.html b/testproject/templates/model_form.html new file mode 100644 index 0000000..e3a76e6 --- /dev/null +++ b/testproject/templates/model_form.html @@ -0,0 +1,48 @@ +{% load bleach_tags %} + + + + +

Model form example

+ Note: You must migrate project to add person! + +
+ {% csrf_token %} + + {{ form.errors }} + {{ form.as_p }} + +
+ +
+
+ +
+

People list

+ + + + + + + {% for person in people %} + + + + + {% endfor %} + +
NameBiography
{{person.name}}{{person.biography|safe}}
+
+ + Form example + diff --git a/testproject/urls.py b/testproject/urls.py index f18d68f..2c8e619 100755 --- a/testproject/urls.py +++ b/testproject/urls.py @@ -3,8 +3,9 @@ except ImportError: from django.conf.urls.defaults import url -from .views import home +from .views import (home, model_form) urlpatterns = [ - url(r'^$', home), + url('^$', home, name='home'), + url('^model_form$', model_form, name='model_form'), ] diff --git a/testproject/views.py b/testproject/views.py index aae2aa3..2fc6d07 100755 --- a/testproject/views.py +++ b/testproject/views.py @@ -2,8 +2,10 @@ from django.http import HttpResponseRedirect from django.shortcuts import render +from django.db import OperationalError -from .forms import BleachForm +from .models import Person +from .forms import BleachForm, PersonForm def home(request): @@ -15,3 +17,23 @@ def home(request): form = BleachForm() return render(request, 'home.html', {'form': form}) + + +def model_form(request): + + if request.POST: + form = PersonForm(request.POST) + if form.is_valid(): + form.save() + return HttpResponseRedirect('?ok') + else: + form = PersonForm() + try: + people = list(Person.objects.all()) + except OperationalError: + people = [] + + return render(request, 'model_form.html', { + 'form': form, + 'people': people, + })