Skip to content
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

[7274] images: add validator which enforces alt texts on img tags #1520

Merged
merged 1 commit into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions adhocracy4/images/validators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import math

import magic
from bs4 import BeautifulSoup
from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _

image_max_mb = 5
Expand Down Expand Up @@ -74,3 +76,32 @@ def validate_image(
if errors:
raise ValidationError(errors)
return image


@deconstructible
class ImageAltTextValidator:
"""Validate that if the input contains html img tags that all have the alt
attribute set, otherwise raise ValidationError.
"""

message = _("Please add an alternative text for all images.")
code = "invalid"

def __init__(self):
pass

def __call__(self, value):
"""Parse value with BeautifulSoup and check
if img tags exist which don't have the alt attribute set"""

soup = BeautifulSoup(value, "html.parser")
img_tags = soup("img", alt=False)
if len(img_tags) > 0:
raise ValidationError(message=self.message, code=self.code)

def __eq__(self, other):
return (
isinstance(other, ImageAltTextValidator)
and self.message == other.message
and self.code == other.code
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 4.2 on 2024-01-29 13:29

import adhocracy4.images.validators
from django.db import migrations
import django_ckeditor_5.fields


class Migration(migrations.Migration):
dependencies = [
("a4projects", "0046_alter_project_information_alter_project_result"),
]

operations = [
migrations.AlterField(
model_name="project",
name="information",
field=django_ckeditor_5.fields.CKEditor5Field(
blank=True,
validators=[adhocracy4.images.validators.ImageAltTextValidator()],
verbose_name="Description of your project",
),
),
migrations.AlterField(
model_name="project",
name="result",
field=django_ckeditor_5.fields.CKEditor5Field(
blank=True,
validators=[adhocracy4.images.validators.ImageAltTextValidator()],
verbose_name="Results of your project",
),
),
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's squash this and the last migration once this PR is merged to main.

3 changes: 3 additions & 0 deletions adhocracy4/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from adhocracy4 import transforms as html_transforms
from adhocracy4.administrative_districts.models import AdministrativeDistrict
from adhocracy4.images import fields
from adhocracy4.images.validators import ImageAltTextValidator
from adhocracy4.maps.fields import PointField
from adhocracy4.models import base

Expand Down Expand Up @@ -218,11 +219,13 @@ class Project(
blank=True,
config_name="collapsible-image-editor",
verbose_name=_("Description of your project"),
validators=[ImageAltTextValidator()],
)
result = CKEditor5Field(
blank=True,
config_name="collapsible-image-editor",
verbose_name=_("Results of your project"),
validators=[ImageAltTextValidator()],
)
access = EnumField(
Access, default=Access.PUBLIC, verbose_name=_("Access to the project")
Expand Down
2 changes: 2 additions & 0 deletions changelog/7274.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
- custom migration for iframes to make them work with ckeditor5 (WARNING:
backing up your database before running is recommended).
- added dependency beautifulsoup4
- added an ImageAltTextValidator to enforce alt text on img tags. The validator
is used for project information and result.

### Changed

Expand Down
38 changes: 38 additions & 0 deletions tests/dashboard/test_form_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
from adhocracy4.dashboard.components.forms import ProjectDashboardForm
from adhocracy4.dashboard.components.forms import ProjectFormComponent
from adhocracy4.dashboard.forms import ProjectBasicForm
from adhocracy4.dashboard.forms import ProjectInformationForm
from adhocracy4.dashboard.forms import ProjectResultForm
from adhocracy4.images.validators import ImageAltTextValidator
from adhocracy4.modules import models as module_models
from adhocracy4.phases import models as phase_models
from adhocracy4.projects import models as project_models
Expand Down Expand Up @@ -270,3 +273,38 @@ class Component(ModuleFormSetComponent):
phase.start_date = now()
phase.save()
assert component.get_progress(module) == (1, 2)


@pytest.mark.django_db
@pytest.mark.parametrize(
"form_class, field_name",
[(ProjectInformationForm, "information"), (ProjectResultForm, "result")],
)
def test_project_form_image_missing_alt_text(form_class, field_name, project_factory):
project = project_factory(is_draft=False)
form = form_class(
instance=project,
data={
field_name: "my project description <img>",
},
)
assert field_name in form.errors
assert form.errors[field_name].data[0].messages[0] == ImageAltTextValidator.message
assert not form.is_valid()


@pytest.mark.django_db
@pytest.mark.parametrize(
"form_class, field_name",
[(ProjectInformationForm, "information"), (ProjectResultForm, "result")],
)
def test_project_form_image_has_alt_text(form_class, field_name, project_factory):
project = project_factory(is_draft=False)
form = form_class(
instance=project,
data={
field_name: "my project description <img alt='descriptive picture'>",
},
)
assert field_name not in form.errors
assert form.is_valid()
Loading