Skip to content

Commit 65d30ce

Browse files
author
Julian Dehm
committed
images: add validator which enforces alt texts on img tags
1 parent 27e8804 commit 65d30ce

File tree

5 files changed

+103
-0
lines changed

5 files changed

+103
-0
lines changed

adhocracy4/images/validators.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import math
22

33
import magic
4+
from bs4 import BeautifulSoup
45
from django.core.exceptions import ValidationError
6+
from django.utils.deconstruct import deconstructible
57
from django.utils.translation import gettext_lazy as _
68

79
image_max_mb = 5
@@ -74,3 +76,21 @@ def validate_image(
7476
if errors:
7577
raise ValidationError(errors)
7678
return image
79+
80+
81+
@deconstructible
82+
class ImageAltTextValidator:
83+
"""Validate that if the input contains html img tags that all have the alt
84+
attribute set, otherwise raise ValidationError.
85+
"""
86+
87+
message = (_("Please add an alternative text for all images."),)
88+
89+
def __call__(self, value):
90+
soup = BeautifulSoup(value)
91+
images = soup("img", alt=False)
92+
if len(images) > 0:
93+
raise ValidationError(self.message)
94+
95+
def __eq__(self, other):
96+
return isinstance(other, self.__class__)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Generated by Django 4.2 on 2024-01-29 13:20
2+
3+
import adhocracy4.images.validators
4+
from django.db import migrations
5+
import django_ckeditor_5.fields
6+
7+
8+
class Migration(migrations.Migration):
9+
dependencies = [
10+
("a4projects", "0045_rename_field_m2mtopics_to_topics"),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name="project",
16+
name="information",
17+
field=django_ckeditor_5.fields.CKEditor5Field(
18+
blank=True,
19+
help_text="This description should tell participants what the goal of the project is, how the project’s participation will look like. It will be always visible in the „Info“ tab on your project’s page.",
20+
validators=[adhocracy4.images.validators.ImageAltTextValidator()],
21+
verbose_name="Description of your project",
22+
),
23+
),
24+
migrations.AlterField(
25+
model_name="project",
26+
name="result",
27+
field=django_ckeditor_5.fields.CKEditor5Field(
28+
blank=True,
29+
help_text="Here you should explain what the expected outcome of the project will be and how you are planning to use the results. If the project is finished you should add a summary of the results.",
30+
validators=[adhocracy4.images.validators.ImageAltTextValidator()],
31+
verbose_name="Results of your project",
32+
),
33+
),
34+
]

adhocracy4/projects/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from adhocracy4 import transforms as html_transforms
1717
from adhocracy4.administrative_districts.models import AdministrativeDistrict
1818
from adhocracy4.images import fields
19+
from adhocracy4.images.validators import ImageAltTextValidator
1920
from adhocracy4.maps.fields import PointField
2021
from adhocracy4.models import base
2122

@@ -224,6 +225,7 @@ class Project(
224225
"participation will look like. It will be always visible "
225226
"in the „Info“ tab on your project’s page."
226227
),
228+
validators=[ImageAltTextValidator()],
227229
)
228230
result = CKEditor5Field(
229231
blank=True,
@@ -235,6 +237,7 @@ class Project(
235237
"results. If the project is finished you should add a "
236238
"summary of the results."
237239
),
240+
validators=[ImageAltTextValidator()],
238241
)
239242
access = EnumField(
240243
Access, default=Access.PUBLIC, verbose_name=_("Access to the project")

changelog/7274.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
- custom migration for iframes to make them work with ckeditor5 (WARNING:
44
backing up your database before running is recommended).
55
- added dependency beautifulsoup4
6+
- added an ImageAltTextValidator to enforce alt text on img tags. The validator
7+
is used for project information and result.
68

79
### Changed
810

tests/dashboard/test_form_components.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
from adhocracy4.dashboard.components.forms import ProjectDashboardForm
1111
from adhocracy4.dashboard.components.forms import ProjectFormComponent
1212
from adhocracy4.dashboard.forms import ProjectBasicForm
13+
from adhocracy4.dashboard.forms import ProjectInformationForm
14+
from adhocracy4.dashboard.forms import ProjectResultForm
15+
from adhocracy4.images.validators import ImageAltTextValidator
1316
from adhocracy4.modules import models as module_models
1417
from adhocracy4.phases import models as phase_models
1518
from adhocracy4.projects import models as project_models
@@ -270,3 +273,44 @@ class Component(ModuleFormSetComponent):
270273
phase.start_date = now()
271274
phase.save()
272275
assert component.get_progress(module) == (1, 2)
276+
277+
278+
@pytest.mark.django_db
279+
@pytest.mark.parametrize(
280+
"form_class, field_name",
281+
[(ProjectInformationForm, "information"), (ProjectResultForm, "result")],
282+
)
283+
def test_project_information_form_image_missing_alt_text(
284+
form_class, field_name, project_factory
285+
):
286+
project = project_factory(is_draft=False)
287+
form = form_class(
288+
instance=project,
289+
data={
290+
field_name: "my project description <img></img>",
291+
},
292+
)
293+
assert field_name in form.errors
294+
assert form.errors[field_name].data[0].messages[0] == str(
295+
ImageAltTextValidator.message
296+
)
297+
assert not form.is_valid()
298+
299+
300+
@pytest.mark.django_db
301+
@pytest.mark.parametrize(
302+
"form_class, field_name",
303+
[(ProjectInformationForm, "information"), (ProjectResultForm, "result")],
304+
)
305+
def test_project_information_form_image_has_alt_text(
306+
form_class, field_name, project_factory
307+
):
308+
project = project_factory(is_draft=False)
309+
form = form_class(
310+
instance=project,
311+
data={
312+
field_name: "my project description <img alt='descriptive picture'></img>",
313+
},
314+
)
315+
assert field_name not in form.errors
316+
assert form.is_valid()

0 commit comments

Comments
 (0)