Skip to content

Commit b01a56d

Browse files
committed
Merge remote-tracking branch 'origin/pv-2023-11-react-maps' into pv-2023-11-react-maps
2 parents 38f880a + 48661d3 commit b01a56d

File tree

10 files changed

+181
-59
lines changed

10 files changed

+181
-59
lines changed

adhocracy4/comments_async/static/comments_async/comment.jsx

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ export default class Comment extends React.Component {
330330
</time>
331331
</div>
332332

333-
<div className="col-1 col-md-1 ms-auto a4-comments__dropdown-container">
333+
<div className="col-5 col-md-4 ms-auto a4-comments__dropdown-container">
334334
{!this.props.is_deleted && (this.props.has_changing_permission || this.props.has_deleting_permission) &&
335335
<CommentManageDropdown
336336
id={this.props.id}
@@ -377,39 +377,40 @@ export default class Comment extends React.Component {
377377
lastEdit={this.state.moderatorFeedback.last_edit}
378378
feedbackText={this.state.moderatorFeedback.feedback_text}
379379
/>}
380+
<div className="a4-comments__action">
381+
<div className="row">
382+
<div className="col-12 a4-comments__action-bar-container">
383+
{this.renderRatingBox()}
384+
385+
<div className="a4-comments__action-bar">
386+
{((this.allowForm() && !this.props.is_deleted) || (this.props.child_comments && this.props.child_comments.length > 0)) &&
387+
<button className="btn btn--no-border a4-comments__action-bar__btn" type="button" onClick={this.toggleShowComments.bind(this)}>
388+
<a href="#child-comment-form">
389+
<i className={this.state.showChildComments ? 'fa fa-minus' : 'far fa-comment'} aria-hidden="true" /> {getAnswerForm(this.state.showChildComments, this.props.child_comments.length)}
390+
</a>
391+
</button>}
392+
393+
{!this.props.is_deleted &&
394+
<a
395+
className="btn btn--no-border a4-comments__action-bar__btn" href={'?comment_' + this.props.id}
396+
data-bs-toggle="modal" data-bs-target={'#share_comment_' + this.props.id}
397+
><i className="fas fa-share" aria-hidden="true" /> {translated.share}
398+
</a>}
399+
400+
{!this.props.is_deleted && this.props.authenticated_user_pk && !this.props.is_users_own_comment &&
401+
<a
402+
className="btn btn--no-border a4-comments__action-bar__btn" href={'#report_comment_' + this.props.id}
403+
data-bs-toggle="modal"
404+
><i className="fas fa-exclamation-triangle" aria-hidden="true" />{translated.report}
405+
</a>}
380406

381-
<div className="row">
382-
<div className="col-12 a4-comments__action-bar-container">
383-
{this.renderRatingBox()}
384-
385-
<div className="a4-comments__action-bar">
386-
{((this.allowForm() && !this.props.is_deleted) || (this.props.child_comments && this.props.child_comments.length > 0)) &&
387-
<button className="btn btn--no-border a4-comments__action-bar__btn" type="button" onClick={this.toggleShowComments.bind(this)}>
388-
<a href="#child-comment-form">
389-
<i className={this.state.showChildComments ? 'fa fa-minus' : 'far fa-comment'} aria-hidden="true" /> {getAnswerForm(this.state.showChildComments, this.props.child_comments.length)}
390-
</a>
391-
</button>}
392-
393-
{!this.props.is_deleted &&
394-
<a
395-
className="btn btn--no-border a4-comments__action-bar__btn" href={'?comment_' + this.props.id}
396-
data-bs-toggle="modal" data-bs-target={'#share_comment_' + this.props.id}
397-
><i className="fas fa-share" aria-hidden="true" /> {translated.share}
398-
</a>}
399-
400-
{!this.props.is_deleted && this.props.authenticated_user_pk && !this.props.is_users_own_comment &&
401-
<a
402-
className="btn btn--no-border a4-comments__action-bar__btn" href={'#report_comment_' + this.props.id}
403-
data-bs-toggle="modal"
404-
><i className="fas fa-exclamation-triangle" aria-hidden="true" />{translated.report}
405-
</a>}
406-
407+
</div>
407408
</div>
408409
</div>
409410
</div>
410411
</div>
411412

412-
<div className="container">
413+
<>
413414
{this.state.showChildComments
414415
? (
415416
<div className="a4-comments__child--list">
@@ -456,7 +457,7 @@ export default class Comment extends React.Component {
456457
</div>
457458
</div>)
458459
: null}
459-
</div>
460+
</>
460461
<ReportModal
461462
name={'report_comment_' + this.props.id}
462463
description={translated.reportTitle}

adhocracy4/comments_async/static/comments_async/comment_box.jsx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ export const CommentBox = (props) => {
107107
function handleComments (result) {
108108
const data = result
109109

110-
translated.entries = django.ngettext('entry', 'entries', data.count)
111110
setComments(data.results)
112111
setNextComments(data.next)
113112
setCommentCount(data.comment_count)
@@ -409,6 +408,13 @@ export const CommentBox = (props) => {
409408
}
410409
}
411410

411+
function translatedEntries (entries) {
412+
return django.ngettext(
413+
'entry',
414+
'entries', entries
415+
)
416+
}
417+
412418
function translatedEntriesFound (entriesFound) {
413419
return django.ngettext(
414420
'entry found for ',
@@ -472,7 +478,7 @@ export const CommentBox = (props) => {
472478
<div
473479
className={search === '' ? 'a4-comments__filters__text' : 'd-none'}
474480
>
475-
{commentCount + ' ' + translated.entries}
481+
{commentCount + ' ' + translatedEntries(commentCount)}
476482
</div>
477483

478484
<div
@@ -519,19 +525,15 @@ export const CommentBox = (props) => {
519525
translated={translated}
520526
onSearch={handleSearch}
521527
/>
522-
{props.withCategories
523-
? (
524-
<FilterCategory
525-
translated={translated}
526-
filter={filter}
527-
filterDisplay={filterDisplay}
528-
onClickFilter={handleClickFilter}
529-
commentCategoryChoices={props.commentCategoryChoices}
530-
/>
531-
)
532-
: (
533-
<div className="col-lg-3" />
534-
)}
528+
{props.withCategories && (
529+
<FilterCategory
530+
translated={translated}
531+
filter={filter}
532+
filterDisplay={filterDisplay}
533+
onClickFilter={handleClickFilter}
534+
commentCategoryChoices={props.commentCategoryChoices}
535+
/>
536+
)}
535537
<FilterSort
536538
translated={translated}
537539
sort={sort}

adhocracy4/comments_async/static/comments_async/comment_form.jsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ const translated = {
1414
yourReply: django.gettext('Your reply'),
1515
characters: django.gettext(' characters'),
1616
post: django.gettext('post'),
17-
comment: django.gettext('Comment'),
1817
pleaseComment: django.gettext('Please login to comment'),
1918
onlyInvited: django.gettext('Only invited users can actively participate.'),
2019
notAllowedComment: django.gettext('The currently active phase doesn\'t allow to comment.'),
@@ -158,7 +157,7 @@ export default class CommentForm extends React.Component {
158157
style={textareaStyle}
159158
/>
160159
<div className="row">
161-
<div className="col-12 col-sm-9">
160+
<div className="col-12 col-sm-6">
162161
<p
163162
id={'id_char-count' + this.props.commentId}
164163
className="a4-comments__char-count"
@@ -173,7 +172,7 @@ export default class CommentForm extends React.Component {
173172
orgTermsUrl={this.props.orgTermsUrl}
174173
/>}
175174
</div>
176-
<div className="d-flex col-12 col-sm-3 justify-content-end">
175+
<div className="d-flex col-12 col-sm-6 justify-content-end">
177176
{cancelButton}
178177
{actionButton}
179178
</div>

adhocracy4/dashboard/forms.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ class Meta:
109109
"contact_url",
110110
]
111111
required_for_project_publish = ["information"]
112+
help_texts = {
113+
"information": _(
114+
"The project description will be shown in the info-tab. "
115+
"If you add an image, please provide an alternate text. "
116+
"It serves as a textual description of the image content and is "
117+
"read out by screen readers. Describe the image in approx. 80 "
118+
"characters. Example: A busy square with people in summer."
119+
)
120+
}
112121

113122
def __init__(self, *args, **kwargs):
114123
super().__init__(*args, **kwargs)
@@ -120,6 +129,18 @@ class Meta:
120129
model = project_models.Project
121130
fields = ["result"]
122131
required_for_project_publish = []
132+
help_texts = {
133+
"result": _(
134+
"The results description will be shown in the result-tab. "
135+
"Please describe a participation promise beforehand "
136+
"(what will happen with the outcome?) and inform afterwards "
137+
"about the outcome. If you add an image, please provide "
138+
"an alternate text. It serves as a textual description "
139+
"of the image content and is read out by screen readers. "
140+
"Describe the image in approx. 80 characters. "
141+
"Example: A busy square with people in summer."
142+
)
143+
}
123144

124145

125146
class ModuleBasicForm(ModuleDashboardForm):

adhocracy4/images/validators.py

Lines changed: 31 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,32 @@ 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+
code = "invalid"
89+
90+
def __init__(self):
91+
pass
92+
93+
def __call__(self, value):
94+
"""Parse value with BeautifulSoup and check
95+
if img tags exist which don't have the alt attribute set"""
96+
97+
soup = BeautifulSoup(value, "html.parser")
98+
img_tags = soup("img", alt=False)
99+
if len(img_tags) > 0:
100+
raise ValidationError(message=self.message, code=self.code)
101+
102+
def __eq__(self, other):
103+
return (
104+
isinstance(other, ImageAltTextValidator)
105+
and self.message == other.message
106+
and self.code == other.code
107+
)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Generated by Django 4.2 on 2024-01-30 10:03
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+
validators=[adhocracy4.images.validators.ImageAltTextValidator()],
20+
verbose_name="Description of your project",
21+
),
22+
),
23+
migrations.AlterField(
24+
model_name="project",
25+
name="result",
26+
field=django_ckeditor_5.fields.CKEditor5Field(
27+
blank=True,
28+
validators=[adhocracy4.images.validators.ImageAltTextValidator()],
29+
verbose_name="Results of your project",
30+
),
31+
),
32+
]

adhocracy4/projects/models.py

Lines changed: 3 additions & 12 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

@@ -218,23 +219,13 @@ class Project(
218219
blank=True,
219220
config_name="collapsible-image-editor",
220221
verbose_name=_("Description of your project"),
221-
help_text=_(
222-
"This description should tell participants "
223-
"what the goal of the project is, how the project’s "
224-
"participation will look like. It will be always visible "
225-
"in the „Info“ tab on your project’s page."
226-
),
222+
validators=[ImageAltTextValidator()],
227223
)
228224
result = CKEditor5Field(
229225
blank=True,
230226
config_name="collapsible-image-editor",
231227
verbose_name=_("Results of your project"),
232-
help_text=_(
233-
"Here you should explain what the expected outcome of the "
234-
"project will be and how you are planning to use the "
235-
"results. If the project is finished you should add a "
236-
"summary of the results."
237-
),
228+
validators=[ImageAltTextValidator()],
238229
)
239230
access = EnumField(
240231
Access, default=Access.PUBLIC, verbose_name=_("Access to the project")

changelog/7274.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
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

911
- replace django-ckeditor with django-ckeditor-5
12+
- update helptext for project information and result with info about image alt
13+
text and move them from the model to the form
1014

1115
### Removed
1216

changelog/_4444.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Fixed
2+
3+
- fix comment count plurals not updating properly

tests/dashboard/test_form_components.py

Lines changed: 38 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,38 @@ 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_form_image_missing_alt_text(form_class, field_name, project_factory):
284+
project = project_factory(is_draft=False)
285+
form = form_class(
286+
instance=project,
287+
data={
288+
field_name: "my project description <img>",
289+
},
290+
)
291+
assert field_name in form.errors
292+
assert form.errors[field_name].data[0].messages[0] == ImageAltTextValidator.message
293+
assert not form.is_valid()
294+
295+
296+
@pytest.mark.django_db
297+
@pytest.mark.parametrize(
298+
"form_class, field_name",
299+
[(ProjectInformationForm, "information"), (ProjectResultForm, "result")],
300+
)
301+
def test_project_form_image_has_alt_text(form_class, field_name, project_factory):
302+
project = project_factory(is_draft=False)
303+
form = form_class(
304+
instance=project,
305+
data={
306+
field_name: "my project description <img alt='descriptive picture'>",
307+
},
308+
)
309+
assert field_name not in form.errors
310+
assert form.is_valid()

0 commit comments

Comments
 (0)