marquage textes d'aide/d'erreur (#75681) #19

Merged
fpeters merged 3 commits from wip/75681-aria-describedby into main 2023-05-12 09:18:35 +02:00
13 changed files with 187 additions and 13 deletions

3
Jenkinsfile vendored
View File

@ -12,8 +12,11 @@ pipeline {
always {
script {
utils = new Utils()
utils.publish_coverage('coverage.xml')
utils.publish_coverage_native('index.html')
utils.publish_pylint('pylint.out')
}
mergeJunitResults()
}
}
}

View File

@ -1,4 +1,3 @@
import django
from django import forms
@ -23,15 +22,8 @@ class MultiSelectWidget(forms.MultiWidget):
self.widgets.append(forms.Select(attrs=self.attrs, choices=self.choices))
# all subwidgets must have the same name
if django.VERSION >= (3, 1):
self.widgets_names = [''] * len(self.widgets)
return super().get_context(name, value, attrs)
else:
context = super().get_context(name, value, attrs)
subwidgets = context['widget']['subwidgets']
for widget in subwidgets:
widget['name'] = widget['name'].rsplit('_', 1)[0]
return context
self.widgets_names = [''] * len(self.widgets)
return super().get_context(name, value, attrs)
def decompress(self, value):
return value or []

View File

@ -21,7 +21,7 @@
{% block widget-attrs %}{% endblock %}>
{% block widget-hint %}
{% if field.help_text %}
<div class="hint">{{ field.help_text|safe }}</div>
<div class="hint" id="help_text_{{field.id_for_label}}">{{ field.help_text|safe }}</div>
{% endif %}
{% endblock %}
{% block widget-control %}
@ -29,7 +29,7 @@
{% endblock %}
{% block widget-error %}
{% if field.errors %}
<div class="error"><p>
<div class="error" id="error_{{field.id_for_label}}"><p>
{% for error in field.errors %}
{{ error }}{% if not forloop.last %}<br>{% endif %}
{% endfor %}

View File

@ -108,6 +108,14 @@ def with_template(form):
templates = ['gadjo/widget.html']
if hasattr(widget, 'input_type'):
templates.insert(0, 'gadjo/%s-widget.html' % widget.input_type)
aria_described_by = []
if field.field.help_text:
aria_described_by.append(f'help_text_{field.id_for_label}')
if field.errors:
aria_described_by.append(f'error_{field.id_for_label}')
field.field.widget.attrs['aria-invalid'] = 'true'
if aria_described_by:
field.field.widget.attrs['aria-describedby'] = ' '.join(aria_described_by)
fields_with_templates.append(
(
field,

2
pytest.ini Normal file
View File

@ -0,0 +1,2 @@
[pytest]
DJANGO_SETTINGS_MODULE = tests.project.settings

0
tests/__init__.py Normal file
View File

10
tests/conftest.py Normal file
View File

@ -0,0 +1,10 @@
import pytest
@pytest.fixture
def nocache(settings):
settings.CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}

View File

37
tests/project/settings.py Normal file
View File

@ -0,0 +1,37 @@
import os
DATABASES = {
'default': {
'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.postgresql_psycopg2'),
'NAME': 'gadjo-test-%s' % os.environ.get("BRANCH_NAME", "").replace('/', '-')[:45],
}
}
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [],
'builtins': [
'gadjo.templatetags.gadjo',
],
},
},
]
DEBUG = True
USE_TZ = True
INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sites",
"gadjo",
]
STATIC_URL = "/static/"
SITE_ID = 1
MIDDLEWARE_CLASSES = ()
LOGGING = {}
SECRET_KEY = "yay"

6
tests/test_finder.py Normal file
View File

@ -0,0 +1,6 @@
import gadjo.finders
def test_finder():
finder = gadjo.finders.XStaticFinder()
assert len(finder.find(path='', all=True)) == 4

View File

@ -0,0 +1,73 @@
import html
import urllib
from django import forms
from django.template import Context, Template
from django.test.client import RequestFactory
from pyquery import PyQuery
def test_start_timestamp():
t = Template('{% start_timestamp %}')
assert t.render(Context())
def test_querystring():
t = Template('{% querystring "name"="Ayers" "age"=20 %}')
ctx = Context({'request': RequestFactory().get('/')})
assert urllib.parse.parse_qs(urllib.parse.urlparse(html.unescape(t.render(ctx))).query) == {
'age': ['20'],
'name': ['Ayers'],
}
ctx = Context({'request': RequestFactory().get('/?age=10')})
assert urllib.parse.parse_qs(urllib.parse.urlparse(html.unescape(t.render(ctx))).query) == {
'age': ['20'],
'name': ['Ayers'],
}
t = Template('{% querystring "name"="Ayers" without "gender" %}')
ctx = Context({'request': RequestFactory().get('/')})
assert urllib.parse.parse_qs(urllib.parse.urlparse(html.unescape(t.render(ctx))).query) == {
'name': ['Ayers']
}
ctx = Context({'request': RequestFactory().get('/?gender=male')})
assert urllib.parse.parse_qs(urllib.parse.urlparse(html.unescape(t.render(ctx))).query) == {
'name': ['Ayers']
}
def test_with_template():
class ExampleForm(forms.Form):
text = forms.CharField(label='Text', max_length=50)
request = RequestFactory().get('/')
t = Template('{{ form|with_template }}')
ctx = Context({'request': request, 'form': ExampleForm()})
rendered = t.render(ctx)
assert PyQuery(rendered).find('input[type=text]')
assert not PyQuery(rendered).find('input[type=text]').attr['aria-invalid']
ctx = Context({'request': request, 'form': ExampleForm(data=request.GET)})
rendered = t.render(ctx)
assert (
PyQuery(rendered).find('input[type=text][aria-describedby]').attr['aria-describedby']
== 'error_id_text'
)
assert PyQuery(rendered).find('input[type=text]').attr['aria-invalid']
class ExampleForm(forms.Form):
text = forms.CharField(label='Text', max_length=50, help_text='Help text')
ctx = Context({'request': request, 'form': ExampleForm()})
rendered = t.render(ctx)
assert (
PyQuery(rendered).find('input[type=text][aria-describedby]').attr['aria-describedby']
== 'help_text_id_text'
)
ctx = Context({'request': request, 'form': ExampleForm(data=request.GET)})
rendered = t.render(ctx)
assert (
PyQuery(rendered).find('input[type=text][aria-describedby]').attr['aria-describedby']
== 'help_text_id_text error_id_text'
)

28
tests/test_widgets.py Normal file
View File

@ -0,0 +1,28 @@
from django import forms
from django.template import Context, Template
from django.test.client import RequestFactory
from pyquery import PyQuery
from gadjo.forms.widgets import MultiSelectWidget
def test_multiselect_widget():
class ExampleForm(forms.Form):
choices = forms.MultipleChoiceField(
label='choices', choices=[('a', 'Aa'), ('b', 'Bb'), ('c', 'Cc')], widget=MultiSelectWidget
)
request = RequestFactory().get('/')
t = Template('{{ form|with_template }}')
ctx = Context({'request': request, 'form': ExampleForm()})
rendered = t.render(ctx)
assert len(PyQuery(rendered).find('select')) == 1
assert PyQuery(rendered).find('.gadjo-multi-select-widget--button-add')
request = RequestFactory().get('/?choices=a&choices=b')
t = Template('{{ form|with_template }}')
ctx = Context({'request': request, 'form': ExampleForm(data=request.GET)})
rendered = t.render(ctx)
assert len(PyQuery(rendered).find('select')) == 2
assert PyQuery(rendered).find('option[selected]').text() == 'Aa Bb'
assert ctx['form'].cleaned_data == {'choices': ['a', 'b']}

17
tox.ini
View File

@ -1,13 +1,28 @@
[tox]
envlist = codestyle-pylint
envlist = codestyle-coverage-pylint
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/gadjo/{env:BRANCH_NAME:}
[testenv]
usedevelop =
coverage: True
nocoverage: False
setenv =
SETUPTOOLS_USE_DISTUTILS=stdlib
JUNIT=--junitxml=junit-{envname}.xml
coverage: COVERAGE=--cov-report xml --cov-report html --cov=gadjo/
deps =
pytest
pytest-django
WebTest
psycopg2-binary
psycopg2
pyquery
codestyle: pre-commit
coverage: pytest-cov
pylint: pylint
pylint: pylint-django
pylint: django>=3.2,<3.3
commands =
pytest {posargs: {env:JUNIT:} {env:COVERAGE:} tests/}
codestyle: pre-commit run --all-files --show-diff-on-failure
pylint: ./pylint.sh gadjo/