diff options
| author | kujiu <kujiu@users.noreply.github.com> | 2018-06-22 21:22:21 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-06-22 21:22:21 +0200 |
| commit | 9de1bc0eef6f5bfaa2a8d745431caa361ae91548 (patch) | |
| tree | 342ddbf7fb2b7303606f864ff9f68cbf1bd8814e /odoo | |
| parent | 864357203fc6222cc989b99a5e9f428cd53f413f (diff) | |
[IMP] Improve compatibility with screen readers (accessibility) (#24574)
Today, Odoo is really tricky to use without seeing the screen, it must be improved to be usable.
This PR forbid to use labels without a "for" attribute, add some title, rule and aria attributes in HTML. With that, Odoo will be fully usable with a screen reader.
* [IMP] Labels must have a for attribute. Improve accessibility.
* [IMP] Better error message when trying to read a missing cached value
* [FIX] Add some aria-label and title attributes for screen readers.
* [FIX] Template name is not included in the error message in case of SyntaxError in QWeb
* [FIX] Improve the Tour failed at step error message to be more explicit.
* [IMP] Add aria-labels
* [FIX] Add missing aria-label on failing test
* [IMP] aria-hidden means hidden. Fix all bad aria-hidden and hide aria-hidden for all.
* [IMP] Color names on kanban views and many2many tags
* [IMP] Add some checks on views for accessibility.
* [IMP] Add `alt` attribute on `img` tags.
* [IMP] Add aria-label and title on non-described icons
* [IMP] Add button role to widgets with btn class
* [IMP] Translate aria and formatted attributes.
* [IMP] Remove wrong aria-labelledby
* [IMP] Add menu role on dropdowns
* [IMP] Buttons must be focusable
* [IMP] Add aria attributes on progress bars
* [IMP] Improve accessibility of basic widgets
* [IMP] Change main layout to more semantic tags
* [IMP] Add menuitem role when missing
* [IMP] Remove wrong role='presentation'
* [IMP] Improve accessibility of tab panels
* [IMP] Add aria-invalid on invalid fields
* [IMP] Add aria-sort on ordered columns
* [IMP] Add role on alerts
* [IMP] Use dialog role, header, main and footer tags for modals
* [IMP] Add labels on o_status
* [IMP] Improve accessibility of kanban view with feeds and articles
* [IMP] Add alerts in case of new messages
* [IMP] Add widget, navigation or img role to aria-labelled items
Diffstat (limited to 'odoo')
22 files changed, 440 insertions, 122 deletions
diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py index 02f48a00c86..d813af46efa 100644 --- a/odoo/addons/base/models/ir_ui_view.py +++ b/odoo/addons/base/models/ir_ui_view.py @@ -338,8 +338,11 @@ actual arch. # A <data> element is a wrapper for multiple root nodes view_docs = view_docs[0] for view_arch in view_docs: - if not valid_view(view_arch): - raise ValidationError(_('Invalid view definition')) + check = valid_view(view_arch) + if not check: + raise ValidationError(_('Invalid view %s definition in %s') % (view.name, view.arch_fs)) + if check == "Warning": + _logger.warning(_('Invalid view %s definition in %s \n%s'), view.name, view.arch_fs, view.arch) return True @api.constrains('type', 'groups_id') diff --git a/odoo/addons/base/rng/common.rng b/odoo/addons/base/rng/common.rng index 78002b08999..cd6bb78a073 100644 --- a/odoo/addons/base/rng/common.rng +++ b/odoo/addons/base/rng/common.rng @@ -335,6 +335,9 @@ <rng:optional><rng:attribute name="help"/></rng:optional> <rng:optional><rng:attribute name="class"/></rng:optional> <rng:optional><rng:attribute name="default_focus"/></rng:optional> + <rng:optional><rng:attribute name="title"/></rng:optional> + <rng:optional><rng:attribute name="aria-label"/></rng:optional> + <rng:optional><rng:attribute name="aria-pressed"/></rng:optional> <rng:zeroOrMore> <rng:choice> <rng:ref name="field" /> diff --git a/odoo/addons/base/tests/test_view_validation.py b/odoo/addons/base/tests/test_view_validation.py index 524a53609e4..c3ac8020c27 100644 --- a/odoo/addons/base/tests/test_view_validation.py +++ b/odoo/addons/base/tests/test_view_validation.py @@ -9,29 +9,45 @@ from odoo.tests.common import tagged from odoo.tools.view_validation import ( valid_page_in_book, valid_att_in_form, valid_type_in_colspan, valid_type_in_col, valid_att_in_field, valid_att_in_label, - valid_field_in_graph, valid_field_in_tree, + valid_field_in_graph, valid_field_in_tree, valid_alternative_image_text, + valid_alternative_icon_text, valid_title_icon, valid_simili_button, + valid_simili_progressbar, valid_dialog, valid_simili_dropdown, + valid_focusable_button, valid_prohibited_none_role, valid_simili_tabpanel, + valid_simili_tab, valid_simili_tablist, valid_alerts ) invalid_form = etree.parse(io.BytesIO(b'''\ <form> <label></label> + <ul class="dropdown-menu"><li/><li/></ul> + <div role="presentation"/> <group> <div> <page></page> - <label colspan="True"></label> + <label colspan="True" string=""></label> <field></field> </div> </group> <notebook> <page> <group col="Two"> - <div> - <label></label> - <field colspan="Five"> </field> + <div> + <div class="o_progressbar">100%</div> + <label string=""></label> + <img/> + <span class="fa fa-warning"/> + <field colspan="Five"> </field> </div> </group> + <a class="btn"/> + <div class="btn"/> + <div class="tab-pane"/> </page> </notebook> + <div class="modal"/> + <a data-toggle="tab"/> + <div class="nav-tabs"/> + <div class="alert alert-success"/> </form> ''')).getroot() @@ -39,19 +55,36 @@ valid_form = etree.parse(io.BytesIO(b'''\ <form string=""> <field name=""></field> <field name=""></field> + <ul class="dropdown-menu" role="menu"></ul> <notebook> <page> <field name=""></field> - <label string=""></label> + <label for="" string=""></label> <field name=""></field> </page> <page> <group colspan="5" col="2"> + <div class="o_progressbar" role="progressbar" aria-valuenow="14" aria-valuemin="0" aria-valuemax="100">14%</div> <label for=""></label> - <label string="" colspan="5"></label> + <label for="" string="" colspan="5"></label> + <img alt="Test image"/> + <span class="fa fa-success" aria-label="Test span" title="Test span"/> + <a class="fa fa-success"><span aria-label="test" title="test"/></a> + <a class="btn" role="button"/> </group> </page> </notebook> + <div role="dialog" class="modal"> + <header class="modal-header"/> + <main class="modal-body"/> + <footer class="modal-footer"/> + </div> + <div class="tab-pane" role="tabpanel"/> + <a data-toggle="tab" role="tab" aria-selected="true" aria-controls="test"/> + <div class="nav-tabs" role="tablist"/> + <div class="alert alert-success" role="alert"/> + <div class="alert alert-success" role="alertdialog"/> + <div class="alert alert-success" role="status"/> </form> ''')).getroot() @@ -129,3 +162,31 @@ class TestViewValidation(unittest.TestCase): def test_col_datatype_validation(self): assert not valid_type_in_col(invalid_form) assert valid_type_in_col(valid_form) + + def test_a11y_validation(self): + assert valid_alternative_image_text(invalid_form) == "Warning" + assert valid_alternative_image_text(valid_form) is True + assert valid_alternative_icon_text(invalid_form) == "Warning" + assert valid_alternative_icon_text(valid_form) is True + assert valid_title_icon(invalid_form) == "Warning" + assert valid_title_icon(valid_form) is True + assert valid_simili_button(invalid_form) == "Warning" + assert valid_simili_button(valid_form) is True + assert valid_dialog(invalid_form) == "Warning" + assert valid_dialog(valid_form) is True + assert valid_simili_dropdown(invalid_form) == "Warning" + assert valid_simili_dropdown(valid_form) is True + assert valid_simili_progressbar(invalid_form) == "Warning" + assert valid_simili_progressbar(valid_form) is True + assert valid_simili_tabpanel(invalid_form) == "Warning" + assert valid_simili_tabpanel(valid_form) is True + assert valid_simili_tablist(invalid_form) == "Warning" + assert valid_simili_tablist(valid_form) is True + assert valid_simili_tab(invalid_form) == "Warning" + assert valid_simili_tab(valid_form) is True + assert valid_focusable_button(invalid_form) == "Warning" + assert valid_focusable_button(valid_form) is True + assert valid_prohibited_none_role(invalid_form) == "Warning" + assert valid_prohibited_none_role(valid_form) is True + assert valid_alerts(invalid_form) == "Warning" + assert valid_alerts(valid_form) is True diff --git a/odoo/addons/base/views/ir_module_views.xml b/odoo/addons/base/views/ir_module_views.xml index 53768df481b..9ca6a61f441 100644 --- a/odoo/addons/base/views/ir_module_views.xml +++ b/odoo/addons/base/views/ir_module_views.xml @@ -157,7 +157,7 @@ <t t-name="kanban-box"> <div class="oe_module_vignette oe_kanban_global_click"> <t t-set="installed" t-value="record.state.raw_value == 'installed'"/> - <img t-attf-src="#{record.icon.value}" class="oe_module_icon"/> + <img t-attf-src="#{record.icon.value}" class="oe_module_icon" alt="Icon"/> <div class="oe_module_desc" t-att-title="record.shortdesc.value"> <h4 class="o_kanban_record_title"><field name="shortdesc"/></h4> <p class="oe_module_name"> diff --git a/odoo/addons/base/views/ir_qweb_widget_templates.xml b/odoo/addons/base/views/ir_qweb_widget_templates.xml index 1fc0e5d921a..afb3c08ec41 100644 --- a/odoo/addons/base/views/ir_qweb_widget_templates.xml +++ b/odoo/addons/base/views/ir_qweb_widget_templates.xml @@ -22,21 +22,21 @@ </t> <div itemprop="address" itemscope="itemscope" itemtype="http://schema.org/PostalAddress"> <div t-if="address and 'address' in fields"> - <i t-if="not options.get('no_marker')" class='fa fa-map-marker fa-fw'/> <span itemprop="streetAddress" t-raw="address.replace('\n', options.get('no_tag_br') and ', ' or ('<br/>%s' % ('' if options.get('no_marker') else '&nbsp; &nbsp; ')))"/> + <i t-if="not options.get('no_marker')" class='fa fa-map-marker fa-fw' role="img" aria-label="Address" title="Address"/> <span itemprop="streetAddress" t-raw="address.replace('\n', options.get('no_tag_br') and ', ' or ('<br/>%s' % ('' if options.get('no_marker') else '&nbsp; &nbsp; ')))"/> </div> <div t-if="city and 'city' in fields"> - <i t-if="not options.get('no_marker')" class='fa fa-map-marker fa-fw'/> + <i t-if="not options.get('no_marker')" class='fa fa-map-marker fa-fw' role="img" aria-label="Address" title="Address"/> <span itemprop="addressLocality" t-raw="city"/>, <span itemprop="addressCountry" t-raw="country_id"/> </div> - <div t-if="phone and 'phone' in fields"><i t-if="not options.get('no_marker') or options.get('phone_icons')" class='fa fa-phone fa-fw'/> <span itemprop="telephone" t-esc="phone"/></div> - <div t-if="mobile and 'mobile' in fields"><i t-if="not options.get('no_marker') or options.get('phone_icons')" class='fa fa-mobile fa-fw'/> <span itemprop="telephone" t-esc="mobile"/></div> - <div t-if="fax and 'fax' in fields"><i t-if="not options.get('no_marker') or options.get('phone_icons')" class='fa fa-fax fa-fw'/> <span itemprop="faxNumber" t-esc="fax"/></div> + <div t-if="phone and 'phone' in fields"><i t-if="not options.get('no_marker') or options.get('phone_icons')" class='fa fa-phone fa-fw' role="img" aria-label="Phone" title="Phone"/> <span itemprop="telephone" t-esc="phone"/></div> + <div t-if="mobile and 'mobile' in fields"><i t-if="not options.get('no_marker') or options.get('phone_icons')" class='fa fa-mobile fa-fw' role="img" aria-label="Mobile" title="Mobile"/> <span itemprop="telephone" t-esc="mobile"/></div> + <div t-if="fax and 'fax' in fields"><i t-if="not options.get('no_marker') or options.get('phone_icons')" class='fa fa-fax fa-fw' role="img" aria-label="Fax" title="Fax"/> <span itemprop="faxNumber" t-esc="fax"/></div> <div t-if="website and 'website' in fields"> - <i t-if="not options.get('no_marker')" class='fa fa-globe'/> + <i t-if="not options.get('no_marker')" class='fa fa-globe' role="img" aria-label="Website" title="Website"/> <a t-att-href="website and '%s%s' % ('http://' if '://' not in website else '',website)"><span itemprop="website" t-esc="website"/></a> </div> - <div t-if="email and 'email' in fields"><i t-if="not options.get('no_marker')" class='fa fa-envelope fa-fw'/> <span itemprop="email" t-esc="email"/></div> + <div t-if="email and 'email' in fields"><i t-if="not options.get('no_marker')" class='fa fa-envelope fa-fw' role="img" aria-label="Email" title="Email"/> <span itemprop="email" t-esc="email"/></div> </div> <t t-if="object.country_id and object.country_id.name_position == 'after'"> <t t-call="base.contact_name"/> diff --git a/odoo/addons/base/views/ir_rule_views.xml b/odoo/addons/base/views/ir_rule_views.xml index be8bd917785..54f91932975 100644 --- a/odoo/addons/base/views/ir_rule_views.xml +++ b/odoo/addons/base/views/ir_rule_views.xml @@ -29,7 +29,7 @@ <field name="global"/> <field name="groups" nolabel="1" colspan="4"/> </group> - <i class="fa fa-info fa-3x text-info pull-left"/> + <i class="fa fa-info fa-3x text-info pull-left" role="img" aria-label="Info" title="Info"/> <h3>Interaction between rules</h3> <div> <p> diff --git a/odoo/addons/base/views/ir_sequence_views.xml b/odoo/addons/base/views/ir_sequence_views.xml index d53570cc79a..d55e8d7d1d9 100644 --- a/odoo/addons/base/views/ir_sequence_views.xml +++ b/odoo/addons/base/views/ir_sequence_views.xml @@ -40,21 +40,21 @@ </field> <group col="3" string="Legend (for prefix, suffix)"> <group> - <label colspan="2" string="Current Year with Century: %%(year)s"/> - <label colspan="2" string="Current Year without Century: %%(y)s"/> - <label colspan="2" string="Month: %%(month)s"/> - <label colspan="2" string="Day: %%(day)s"/> + <span colspan="2">Current Year with Century: %%(year)s</span> + <span colspan="2">Current Year without Century: %%(y)s</span> + <span colspan="2">Month: %%(month)s</span> + <span colspan="2">Day: %%(day)s</span> </group> <group> - <label colspan="2" string="Day of the Year: %%(doy)s"/> - <label colspan="2" string="Week of the Year: %%(woy)s"/> - <label colspan="2" string="Day of the Week (0:Monday): %%(weekday)s"/> + <span colspan="2">Day of the Year: %%(doy)s</span> + <span colspan="2">Week of the Year: %%(woy)s</span> + <span colspan="2">Day of the Week (0:Monday): %%(weekday)s</span> </group> <group> - <label colspan="2" string="Hour 00->24: %%(h24)s"/> - <label colspan="2" string="Hour 00->12: %%(h12)s"/> - <label colspan="2" string="Minute: %%(min)s"/> - <label colspan="2" string="Second: %%(sec)s"/> + <span colspan="2">Hour 00->24: %%(h24)s</span> + <span colspan="2">Hour 00->12: %%(h12)s</span> + <span colspan="2">Minute: %%(min)s</span> + <span colspan="2">Second: %%(sec)s</span> </group> </group> <group attrs="{'invisible': [('use_date_range', '=', False)]}"> diff --git a/odoo/addons/base/views/res_company_views.xml b/odoo/addons/base/views/res_company_views.xml index 57b08a2eaef..90e4a0b2d97 100644 --- a/odoo/addons/base/views/res_company_views.xml +++ b/odoo/addons/base/views/res_company_views.xml @@ -73,7 +73,7 @@ <t t-name="kanban-box"> <div t-attf-class="oe_kanban_global_click"> <div class="text-center"> - <i class="fa fa-building" aria-hidden="true"></i> <strong><field name="name"/></strong> + <i class="fa fa-building" role="img" aria-label="Enterprise" title="Enterprise"></i> <strong><field name="name"/></strong> </div> <hr class="mt4 mb4"/> <div class="row"> @@ -129,7 +129,7 @@ <field name="arch" type="xml"> <field name="logo" position="before"> <div class="row"> - <div class="alert alert-warning ml8 mr8" role="alert" attrs="{'invisible': [('logo','!=',False)]}"> + <div class="alert alert-warning ml8 mr8" role="status" attrs="{'invisible': [('logo','!=',False)]}"> Update your company details and upload your logo to get a beautiful document. </div> </div> diff --git a/odoo/addons/base/views/res_config_settings_views.xml b/odoo/addons/base/views/res_config_settings_views.xml index 74ce361e96b..2d119ee5af6 100644 --- a/odoo/addons/base/views/res_config_settings_views.xml +++ b/odoo/addons/base/views/res_config_settings_views.xml @@ -11,7 +11,7 @@ <div class="title h2 mb0 mt4 text-muted hidden-xs">Settings</div> <div class="o_setting_search"> <input type="text" class="searchInput" placeholder="Search..."/> - <span class="searchIcon"><i class="fa fa-search"/></span> + <span class="searchIcon"><i class="fa fa-search" role="img" aria-label="Search" title="Search"/></span> </div> </div> <header> diff --git a/odoo/addons/base/views/res_partner_views.xml b/odoo/addons/base/views/res_partner_views.xml index 5ded79d3e67..f7657ccb475 100644 --- a/odoo/addons/base/views/res_partner_views.xml +++ b/odoo/addons/base/views/res_partner_views.xml @@ -292,15 +292,15 @@ <t t-name="kanban-box"> <t t-set="color" t-value="kanban_color(record.color.raw_value)"/> <div t-att-class="color + (record.title.raw_value == 1 ? ' oe_kanban_color_alert' : '') + ' oe_kanban_global_click'"> - <a t-if="!read_only_mode" type="delete" class="fa fa-times pull-right"/> + <a t-if="!read_only_mode" type="delete" class="fa fa-times pull-right" title="Delete" aria-label="Delete"/> <div class="o_kanban_image"> - <img t-if="record.image_small.raw_value" t-att-src="'data:image/png;base64,'+record.image_small.raw_value"/> + <img alt="" t-if="record.image_small.raw_value" t-att-src="'data:image/png;base64,'+record.image_small.raw_value"/> <t t-if="!record.image_small.raw_value"> - <img t-if="record.type.raw_value === 'delivery'" t-att-src='_s + "/base/static/img/truck.png"'/> - <img t-if="record.type.raw_value === 'invoice'" t-att-src='_s + "/base/static/img/money.png"'/> + <img alt="Delivery" t-if="record.type.raw_value === 'delivery'" t-att-src='_s + "/base/static/img/truck.png"'/> + <img alt="Invoice" t-if="record.type.raw_value === 'invoice'" t-att-src='_s + "/base/static/img/money.png"'/> <t t-if="record.type.raw_value !== 'invoice' && record.type.raw_value !== 'delivery'"> - <img t-if="record.is_company.raw_value === true" t-att-src='_s + "/base/static/img/company_image.png"'/> - <img t-if="record.is_company.raw_value === false" t-att-src='_s + "/base/static/img/avatar.png"'/> + <img alt="Logo" t-if="record.is_company.raw_value === true" t-att-src='_s + "/base/static/img/company_image.png"'/> + <img alt="Avatar" t-if="record.is_company.raw_value === false" t-att-src='_s + "/base/static/img/avatar.png"'/> </t> </t> </div> @@ -495,21 +495,21 @@ <div class="oe_kanban_global_click o_res_partner_kanban"> <div class="o_kanban_image"> <t t-if="record.image_small.raw_value"> - <img t-att-src="kanban_image('res.partner', 'image_small', record.id.raw_value)"/> + <img alt="Partner" t-att-src="kanban_image('res.partner', 'image_small', record.id.raw_value)"/> </t> <t t-if="!record.image_small.raw_value"> <t t-if="record.type.raw_value === 'delivery'"> - <img t-att-src='_s + "/base/static/img/truck.png"' class="o_kanban_image oe_kanban_avatar_smallbox"/> + <img alt="Delivery" t-att-src='_s + "/base/static/img/truck.png"' class="o_kanban_image oe_kanban_avatar_smallbox"/> </t> <t t-if="record.type.raw_value === 'invoice'"> - <img t-att-src='_s + "/base/static/img/money.png"' class="o_kanban_image oe_kanban_avatar_smallbox"/> + <img alt="Invoice" t-att-src='_s + "/base/static/img/money.png"' class="o_kanban_image oe_kanban_avatar_smallbox"/> </t> <t t-if="record.type.raw_value != 'invoice' && record.type.raw_value != 'delivery'"> <t t-if="record.is_company.raw_value === true"> - <img t-att-src='_s + "/base/static/img/company_image.png"'/> + <img alt="Logo" t-att-src='_s + "/base/static/img/company_image.png"'/> </t> <t t-if="record.is_company.raw_value === false"> - <img t-att-src='_s + "/base/static/img/avatar.png"'/> + <img alt="Avatar" t-att-src='_s + "/base/static/img/avatar.png"'/> </t> </t> </t> diff --git a/odoo/addons/base/views/res_users_views.xml b/odoo/addons/base/views/res_users_views.xml index ebf1f72479f..ddeb9ba91ee 100644 --- a/odoo/addons/base/views/res_users_views.xml +++ b/odoo/addons/base/views/res_users_views.xml @@ -64,7 +64,7 @@ <field name="users" context="{'search_default_no_share':1}"/> </page> <page string="Inherited"> - <label string="Users added to this group are automatically added in the following groups."/> + <label for="implied_ids" string="Users added to this group are automatically added in the following groups."/> <field name="implied_ids"/> </page> <page string="Menus"> @@ -162,7 +162,7 @@ <field name="active" widget="boolean_button" options='{"terminology": "active"}'/> </button> </div> - <div class="alert alert-info text-center o_form_header" attrs="{'invisible': [('active', '=', True)]}"> + <div class="alert alert-info text-center o_form_header" attrs="{'invisible': [('active', '=', True)]}" role="alert"> <a class="close" data-dismiss="alert" href="#" aria-hidden="true">x</a> <div> <strong>The contact linked to this user is still active</strong> @@ -283,17 +283,17 @@ <t t-name="kanban-box"> <div t-attf-class="oe_kanban_global_click"> <div class="o_kanban_image"> - <img t-att-src="kanban_image('res.users', 'image_medium', record.id.raw_value)"/> + <img alt="Avatar" t-att-src="kanban_image('res.users', 'image_medium', record.id.raw_value)"/> </div> <div class="oe_kanban_details"> <ul> - <li class="text-success pull-right mb4" t-if="record.active.raw_value"><i class="fa fa-circle" aria-hidden="true"></i></li> - <li class="text-danger pull-right mb4" t-if="!record.active.raw_value"><i class="fa fa-circle"></i></li> + <li class="text-success pull-right mb4" t-if="record.active.raw_value"><i class="fa fa-circle" role="img" aria-label="Ok" title="Ok"></i></li> + <li class="text-danger pull-right mb4" t-if="!record.active.raw_value"><i class="fa fa-circle" role="img" aria-label="Invalid" title="Invalid"></i></li> <li class="mb4"> <strong><field name="name"/></strong> </li> <li class="badge pull-right mb4" t-if="record.lang.raw_value"><field name="lang"/></li> - <li class="mb4" t-if="record.login.raw_value"><i class="fa fa-envelope"/> <field name="login"/></li> + <li class="mb4" t-if="record.login.raw_value" title="Login"><i class="fa fa-envelope" role="img" aria-label="Login"/> <field name="login"/></li> </ul> </div> </div> diff --git a/odoo/addons/base/wizard/base_language_install_views.xml b/odoo/addons/base/wizard/base_language_install_views.xml index 5a82cb5d7ec..936197d810e 100644 --- a/odoo/addons/base/wizard/base_language_install_views.xml +++ b/odoo/addons/base/wizard/base_language_install_views.xml @@ -13,8 +13,8 @@ <field name="overwrite"/> </group> <group states="done" colspan="4"> - <label string="The selected language has been successfully installed. -You must change the preferences of the user to view the changes."/> + <span class="o_form_label">The selected language has been successfully installed. +You must change the preferences of the user to view the changes.</span> </group> <footer states="init"> <button name="lang_install" string="Load" type="object" class="btn-primary"/> diff --git a/odoo/addons/base/wizard/base_module_uninstall_views.xml b/odoo/addons/base/wizard/base_module_uninstall_views.xml index aa945029b00..4f10bebc276 100644 --- a/odoo/addons/base/wizard/base_module_uninstall_views.xml +++ b/odoo/addons/base/wizard/base_module_uninstall_views.xml @@ -19,7 +19,7 @@ <templates> <t t-name="kanban-box"> <div class="oe_module_vignette"> - <img t-attf-src="#{record.icon.value}" + <img t-attf-src="#{record.icon.value}" alt="Icon" class="oe_module_icon" style="width: 60px; height: 60px"/> <div class="oe_module_desc" style="padding-left: 70px"> <h4><field name="shortdesc"/></h4> diff --git a/odoo/addons/base/wizard/base_module_update_views.xml b/odoo/addons/base/wizard/base_module_update_views.xml index c50d1a261ae..e465479e6ff 100644 --- a/odoo/addons/base/wizard/base_module_update_views.xml +++ b/odoo/addons/base/wizard/base_module_update_views.xml @@ -10,7 +10,7 @@ <field name="state" invisible="1"/> <separator string="Module Update Result"/> <group states="init"> - <label string="Click on Update below to start the process..." /> + <span class="o_form_label">Click on Update below to start the process...</span> </group> <group states="done" > <field name="updated"/> diff --git a/odoo/addons/base/wizard/base_module_upgrade_views.xml b/odoo/addons/base/wizard/base_module_upgrade_views.xml index 63c85fbe7aa..53bf47d409f 100644 --- a/odoo/addons/base/wizard/base_module_upgrade_views.xml +++ b/odoo/addons/base/wizard/base_module_upgrade_views.xml @@ -43,8 +43,8 @@ <field name="priority" eval="20"/> <field name="arch" type="xml"> <form string="Apply Schedule Upgrade"> - <div><label string="The selected modules have been updated / installed !"/></div> - <div><label string="We suggest to reload the menu tab to see the new menus (Ctrl+T then Ctrl+R)."/></div> + <div><span class="o_form_label">The selected modules have been updated / installed !</span></div> + <div><span class="o_form_label">We suggest to reload the menu tab to see the new menus (Ctrl+T then Ctrl+R)."</span></div> <footer> <button name="config" string="Start configuration" type="object" class="btn-primary"/> <button string="Cancel" class="btn-default" special="cancel"/> diff --git a/odoo/addons/test_exceptions/view.xml b/odoo/addons/test_exceptions/view.xml index dd0aa97b7f1..7979425befd 100644 --- a/odoo/addons/test_exceptions/view.xml +++ b/odoo/addons/test_exceptions/view.xml @@ -7,97 +7,97 @@ <field name="model">test.exceptions.model</field> <field name="arch" type="xml"> <form string="Test exceptions"> - <label string="Each button generates a specific exception on the server. The text on the right is the expected representation of the exception when displayed on the client. Button marked with a '*' use safe_eval()."/> + <p>Each button generates a specific exception on the server. The text on the right is the expected representation of the exception when displayed on the client. Button marked with a '*' use safe_eval().</p> <separator/> <group> <div> <button name="generate_except_osv" string="except_osv" type="object" icon="fa-refresh text-primary" /> - <label string="Warning-description"/> + <label for="generate_except_osv" string="Warning-description"/> </div> <div> <button name="generate_except_orm" string="except_orm" type="object" icon="fa-refresh text-primary"/> - <label string="Warning-description"/> + <label for="generate_except_orm" string="Warning-description"/> </div> <div> <button name="generate_warning" string="Warning" type="object" icon="fa-refresh text-primary"/> - <label string="Warning-description"/> + <label for="generate_warning" string="Warning-description"/> </div> <div> <button name="generate_redirect_warning" string="RedirectWarning" type="object" icon="fa-refresh text-primary"/> - <label string="Warning-description-redirection button"/> + <label for="generate_redirect_warning" string="Warning-description-redirection button"/> </div> <div> <button name="generate_access_denied" string="AccessDenied" type="object" icon="fa-refresh text-primary"/> - <label string="Access denied-traceback"/> + <label for="generate_access_denied" string="Access denied-traceback"/> </div> <div> <button name="generate_access_error" string="AccessError" type="object" icon="fa-refresh text-primary"/> - <label string="Access rights error-description" class="oe_inline"/> + <label for="generate_access_error" string="Access rights error-description" class="oe_inline"/> </div> <div> <button name="generate_exc_access_denied" string="Exc AccessDenied" type="object" icon="fa-refresh text-primary"/> - <label string="Access denied-traceback"/> + <label for="generate_exc_access_denied" string="Access denied-traceback"/> </div> <div> <button name="generate_undefined" string="Undefined" type="object" icon="fa-refresh text-primary"/> - <label string="Server error-traceback"/> + <label for="generate_undefined" string="Server error-traceback"/> </div> <div> <button name="generate_user_error" string="UserError" type="object" icon="fa-refresh text-primary"/> - <label string="Warning-description"/> + <label for="generate_user_error" string="Warning-description"/> </div> <div> <button name="generate_missing_error" string="MissingError" type="object" icon="fa-refresh text-primary"/> - <label string="Missing Error-description"/> + <label for="generate_missing_error" string="Missing Error-description"/> </div> <div> <button name="generate_validation_error" string="ValidationError" type="object" icon="fa-refresh text-primary"/> - <label string="Validation Error-description"/> + <label for="generate_validation_error" string="Validation Error-description"/> </div> <div> <button name="generate_except_osv_safe_eval" string="except_osv*" type="object" icon="fa-refresh text-primary"/> - <label string="Warning-description"/> + <label for="generate_except_osv_safe_eval" string="Warning-description"/> </div> <div> <button name="generate_except_orm_safe_eval" string="except_orm*" type="object" icon="fa-refresh text-primary"/> - <label string="Warning-description"/> + <label for="generate_except_orm_safe_eval" string="Warning-description"/> </div> <div> <button name="generate_warning_safe_eval" string="Warning*" type="object" icon="fa-refresh text-primary"/> - <label string="Warning-description"/> + <label for="generate_warning_safe_eval" string="Warning-description"/> </div> <div> <button name="generate_redirect_warning_safe_eval" string="RedirectWarning*" type="object" icon="fa-refresh text-primary"/> - <label string="Warning-description-redirection button" class="oe_inline"/> + <label for="generate_redirect_warning_safe_eval" string="Warning-description-redirection button" class="oe_inline"/> </div> <div> <button name="generate_access_denied_safe_eval" string="AccessDenied*" type="object" icon="fa-refresh text-primary"/> - <label string="Access denied-traceback"/> + <label for="generate_access_denied_safe_eval" string="Access denied-traceback"/> </div> <div> <button name="generate_access_error_safe_eval" string="AccessError*" type="object" icon="fa-refresh text-primary"/> - <label string="Access rights error-description"/> + <label for="generate_access_error_safe_eval" string="Access rights error-description"/> </div> <div> <button name="generate_exc_access_denied_safe_eval" string="Exc AccessDenied*" type="object" icon="fa-refresh text-primary"/> - <label string="Access denied-traceback"/> + <label for="generate_exc_access_denied_safe_eval" string="Access denied-traceback"/> </div> <div> <button name="generate_undefined_safe_eval" string="Undefined*" type="object" icon="fa-refresh text-primary"/> - <label string="Server error-traceback"/> + <label for="generate_undefined_safe_eval" string="Server error-traceback"/> </div> <div> <button name="generate_user_error_safe_eval" string="UserError*" type="object" icon="fa-refresh text-primary"/> - <label string="Warning-description"/> + <label for="generate_user_error_safe_eval" string="Warning-description"/> </div> <div> <button name="generate_missing_error_safe_eval" string="MissingError*" type="object" icon="fa-refresh text-primary"/> - <label string="Missing Error-description"/> + <label for="generate_missing_error_safe_eval" string="Missing Error-description"/> </div> <div> <button name="generate_validation_error_safe_eval" string="ValidationError*" type="object" icon="fa-refresh text-primary"/> - <label string="Validation Error-description"/> + <label for="generate_validation_error_safe_eval" string="Validation Error-description"/> </div> </group> </form> diff --git a/odoo/addons/test_main_flows/static/src/js/tour.js b/odoo/addons/test_main_flows/static/src/js/tour.js index 925e86c9bef..c74d473fcbc 100644 --- a/odoo/addons/test_main_flows/static/src/js/tour.js +++ b/odoo/addons/test_main_flows/static/src/js/tour.js @@ -122,7 +122,7 @@ tour.register('main_flow_tour', { position: 'right', }, { trigger: ".o_field_widget[name=name] input", - extra_trigger: ".modal-dialog", + extra_trigger: "[role=\"dialog\"]", content: _t('Select a seller'), position: 'top', run: "text the_flow.vendor", @@ -135,12 +135,12 @@ tour.register('main_flow_tour', { position: 'right', run: "text 1", }, { - trigger: ".modal-footer .btn-primary:first", + trigger: "footer.modal-footer .btn-primary:first", extra_trigger: ".o_field_widget[name=name] > .o_external_button", // Wait name_create content: _t('Save & Close'), position: 'bottom', }, { - trigger: ".modal-footer .btn-primary", + trigger: "footer.modal-footer .btn-primary", content: _t('Save'), position: 'bottom', }, @@ -176,7 +176,7 @@ tour.register('main_flow_tour', { position: 'right', }, { trigger: ".o_field_widget[name=name] input", - extra_trigger: ".modal-dialog", + extra_trigger: "[role=\"dialog\"]", content: _t('Select a seller'), position: 'top', run: "text the_flow.vendor", @@ -190,11 +190,11 @@ tour.register('main_flow_tour', { position: 'right', run: "text 1", }, { - trigger: ".modal-footer .btn-primary:first", + trigger: "footer.modal-footer .btn-primary:first", content: _t('Save & Close'), position: 'bottom', }, { - trigger: ".modal-footer .btn-primary", + trigger: "footer.modal-footer .btn-primary", // Wait Save & Close and check value extra_trigger: ".o_field_widget[name=seller_ids] .o_data_row td:nth-child(2):contains('the_flow.vendor')", content: _t('Save'), @@ -326,8 +326,8 @@ tour.register('main_flow_tour', { run: function (actions) { actions.auto(); // if the one2many isn't editable, we have to close the dialog - if ($(".modal-footer .btn-primary").length) { - actions.auto(".modal-footer .btn-primary"); + if ($("footer.modal-footer .btn-primary").length) { + actions.auto("footer.modal-footer .btn-primary"); } }, }, { @@ -338,7 +338,7 @@ tour.register('main_flow_tour', { trigger: ".o_field_widget[name=product_id] input", // the one2many may be editable or not according to the modules installed, so // we have to handle both cases - extra_trigger: '.o_field_widget[name=order_line] .o_data_row:nth(1).o_selected_row, .modal-dialog', + extra_trigger: '.o_field_widget[name=order_line] .o_data_row:nth(1).o_selected_row, [role="dialog"]', content: _t("Select a product"), position: "right", run: "text the_flow.service", @@ -349,8 +349,8 @@ tour.register('main_flow_tour', { run: function (actions) { actions.auto(); // if the one2many isn't editable, we have to close the dialog - if ($(".modal-footer .btn-primary").length) { - actions.auto(".modal-footer .btn-primary"); + if ($("footer.modal-footer .btn-primary").length) { + actions.auto("footer.modal-footer .btn-primary"); } }, }, { @@ -368,11 +368,11 @@ tour.register('main_flow_tour', { position: "right", run: "text test@the_flow.com", },{ - trigger: ".modal-footer .btn-primary", + trigger: "footer.modal-footer .btn-primary", content: _t("Save your changes"), position: "bottom", }, { - trigger: ".modal-footer .btn-primary span:contains('Send')", + trigger: "footer.modal-footer .btn-primary span:contains('Send')", content: _t("Try to send it to email"), position: "bottom", }, { @@ -440,8 +440,8 @@ tour.register('main_flow_tour', { content: _t("Click on schedulers"), position: "bottom" }, { - trigger: ".modal-footer .btn-primary", - extra_trigger: ".modal-dialog", + trigger: "footer.modal-footer .btn-primary", + extra_trigger: "[role=\"dialog\"]", content: _t("Run Schedulers"), position: "bottom", }, { @@ -471,8 +471,8 @@ tour.register('main_flow_tour', { content: _t("Validate"), position: "bottom", }, { - trigger: ".modal-footer .btn-primary", - extra_trigger: ".modal-dialog", + trigger: "footer.modal-footer .btn-primary", + extra_trigger: "[role=\"dialog\"]", content: _t("Apply"), position: "bottom", }, { @@ -497,12 +497,12 @@ tour.register('main_flow_tour', { position: "bottom", }, { trigger: "select.o_field_widget[name=journal_id]", - extra_trigger: ".modal-dialog", + extra_trigger: "[role=\"dialog\"]", content: _t("Select Journal"), position: "bottom", run: 'text(Bank (USD))', }, { - trigger: ".modal-footer .btn-primary", + trigger: "footer.modal-footer .btn-primary", extra_trigger: ".o_field_widget[name=payment_method_id]", // FIXME: Wait onchange content: _t("Validate"), position: "bottom", @@ -528,7 +528,7 @@ tour.register('main_flow_tour', { content: _t("Produce"), position: "bottom", }, { - trigger: ".modal-footer .btn-primary:first", + trigger: "footer.modal-footer .btn-primary:first", content: _t('Record Production'), position: 'bottom', }, { @@ -607,7 +607,7 @@ tour.register('main_flow_tour', { content: _t("Validate"), position: "bottom", }, { - trigger: ".modal-footer .btn-primary", + trigger: "footer.modal-footer .btn-primary", content: _t("Create and View Invoices"), position: "bottom", }, { @@ -620,12 +620,12 @@ tour.register('main_flow_tour', { position: "bottom", }, { trigger: "select.o_field_widget[name=journal_id]", - extra_trigger: ".modal-dialog", + extra_trigger: "[role=\"dialog\"]", content: _t("Select Journal"), position: "bottom", run: 'text(Bank (USD))', }, { - trigger: ".modal-footer .btn-primary", + trigger: "footer.modal-footer .btn-primary", content: _t("Validate"), position: "bottom", }, { diff --git a/odoo/addons/test_new_api/static/tests/x2many.js b/odoo/addons/test_new_api/static/tests/x2many.js index d256d026325..4a088576050 100644 --- a/odoo/addons/test_new_api/static/tests/x2many.js +++ b/odoo/addons/test_new_api/static/tests/x2many.js @@ -31,7 +31,7 @@ odoo.define('web.test.x2many', function (require) { }, { content: "insert a name into the modal form", trigger: 'input.o_field_widget.o_required_modifier:first', - extra_trigger: '.modal:visible', + extra_trigger: '[role="dialog"]:visible', run: function (action_helper) { action_helper.text('user_test_' + (inc = new Date().getTime())); } @@ -44,7 +44,7 @@ odoo.define('web.test.x2many', function (require) { } }, { content: "save the modal content and create the new moderator", - trigger: '.modal-footer button:contains(Save):first', + trigger: 'footer.modal-footer button:contains(Save):first', extra_trigger: 'input.o_field_widget.o_required_modifier:propValueContains(@test)', }, { content: "check if the modal is saved", @@ -82,12 +82,12 @@ odoo.define('web.test.x2many', function (require) { trigger: '.tab-pane:eq(0) .o_field_x2many_list_row_add a' }, { content: "insert body a", - trigger: '.modal-body textarea:first', + trigger: 'main.modal-body textarea:first', run: 'text a', }, { content: "save new message a", - trigger: '.modal-footer button:contains(Save):first', - extra_trigger: '.modal-body textarea:first:propValue(a)', + trigger: 'footer.modal-footer button:contains(Save):first', + extra_trigger: 'main.modal-body textarea:first:propValue(a)', }, { // add message b content: "create new message b", trigger: '.tab-pane:eq(0) .o_field_x2many_list_row_add a', @@ -98,7 +98,7 @@ odoo.define('web.test.x2many', function (require) { run: 'text b', }, { content: "save new message b", - trigger: '.modal-footer button:contains(Save):first', + trigger: 'footer.modal-footer button:contains(Save):first', extra_trigger: 'textarea[name="body"]:first:propValue(b)', }, { // change content to trigger on change content: "insert content", @@ -123,7 +123,7 @@ odoo.define('web.test.x2many', function (require) { run: 'text bbb', }, { content: "save changes", - trigger: '.modal-footer button:contains(Save):first', + trigger: 'footer.modal-footer button:contains(Save):first', extra_trigger: 'textarea[name="body"]:first:propValue(bbb)', }, { // add message c content: "create new message c", @@ -135,7 +135,7 @@ odoo.define('web.test.x2many', function (require) { run: 'text c', }, { content: "save new message c", - trigger: '.modal-footer button:contains(Save):first', + trigger: 'footer.modal-footer button:contains(Save):first', extra_trigger: 'textarea[name="body"]:first:propValue(c)', }, { // add participants content: "change tab to Participants", @@ -186,7 +186,7 @@ odoo.define('web.test.x2many', function (require) { run: 'text d', }, { content: "save new message d", - trigger: '.modal-footer button:contains(Save):first', + trigger: 'footer.modal-footer button:contains(Save):first', extra_trigger: 'textarea[name="body"]:first:propValue(d)', }, { // add message e content: "create new message e", @@ -198,7 +198,7 @@ odoo.define('web.test.x2many', function (require) { run: 'text e', }, { content: "save new message e", - trigger: '.modal-footer button:contains(Save):first', + trigger: 'footer.modal-footer button:contains(Save):first', extra_trigger: 'textarea[name="body"]:first:propValue(e)', }, { // change message a content: "edit message a", @@ -210,7 +210,7 @@ odoo.define('web.test.x2many', function (require) { run: 'text aaa', }, { content: "save changes", - trigger: '.modal-footer button:contains(Save):first', + trigger: 'footer.modal-footer button:contains(Save):first', extra_trigger: 'textarea[name="body"]:first:propValue(aaa)', }, { // change message e content: "edit message e", @@ -235,7 +235,7 @@ odoo.define('web.test.x2many', function (require) { run: function () {}, // don't change texarea content }, { content: "save changes", - trigger: '.modal-footer button:contains(Save):contains(Close)' + trigger: 'footer.modal-footer button:contains(Save):contains(Close)' }, { content: "test one2many triggered the onchange on save for the line", trigger: '.o_content:has(.tab-pane:eq(0) .o_field_widget tbody tr td.o_readonly_modifier:contains([test_trigger] Demo User))', @@ -283,7 +283,7 @@ odoo.define('web.test.x2many', function (require) { }, { content: "select another user", trigger: '.o_field_many2one .o_input_dropdown > input', - extra_trigger: 'body:has(.modal) .tab-pane:eq(0) .o_field_widget tbody tr:has(td:containsExact(d))', + extra_trigger: 'body:has([role="dialog"]) .tab-pane:eq(0) .o_field_widget tbody tr:has(td:containsExact(d))', run: 'text Demo', }, { content: "select demo user", @@ -299,7 +299,7 @@ odoo.define('web.test.x2many', function (require) { run: 'text ddd', }, { content: "save new message ddd", - trigger: '.modal-footer button:contains(Save):first', + trigger: 'footer.modal-footer button:contains(Save):first', extra_trigger: 'textarea[name="body"]:first:propValue(ddd)', }, { // trigger onchange content: "blur the one2many", @@ -321,7 +321,7 @@ odoo.define('web.test.x2many', function (require) { run: 'click', }, { content: "confirm cancel change", - trigger: '.modal-footer button:contains(Ok)', + trigger: 'footer.modal-footer button:contains(Ok)', }, ///////////////////////////////////////////////////////////////////////////////////////////// @@ -516,6 +516,6 @@ odoo.define('web.test.x2many', function (require) { run: 'click', }, { content: "confirm cancel change", - trigger: '.modal-footer button:contains(Ok)', + trigger: 'footer.modal-footer button:contains(Ok)', }]); }); diff --git a/odoo/api.py b/odoo/api.py index d1b1872e7f5..2d2bae74e1a 100644 --- a/odoo/api.py +++ b/odoo/api.py @@ -56,6 +56,7 @@ from decorator import decorator from werkzeug.local import Local, release_local from odoo.tools import frozendict, classproperty, StackMap +from odoo.exceptions import CacheMiss _logger = logging.getLogger(__name__) @@ -958,7 +959,11 @@ class Cache(object): def get(self, record, field): """ Return the value of ``field`` for ``record``. """ key = field.cache_key(record) - value = self._data[field][record.id][key] + try: + value = self._data[field][record.id][key] + except KeyError: + raise CacheMiss(record, field) + return value.get() if isinstance(value, SpecialValue) else value def set(self, record, field, value): diff --git a/odoo/exceptions.py b/odoo/exceptions.py index c318f9ecdac..e1689ffd27f 100644 --- a/odoo/exceptions.py +++ b/odoo/exceptions.py @@ -63,6 +63,13 @@ class AccessError(except_orm): super(AccessError, self).__init__(msg) +class CacheMiss(except_orm, KeyError): + """ Missing value(s) in cache. + Example: When you try to read a value in a flushed cache.""" + def __init__(self, record, field): + super(CacheMiss, self).__init__("%s.%s" % (str(record), field.name)) + + class MissingError(except_orm): """ Missing record(s). Example: When you try to write on a deleted record.""" diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py index f69d37214bd..6b785c0d47a 100644 --- a/odoo/tools/translate.py +++ b/odoo/tools/translate.py @@ -143,9 +143,12 @@ TRANSLATED_ELEMENTS = { # which attributes must be translated TRANSLATED_ATTRS = { - 'string', 'help', 'sum', 'avg', 'confirm', 'placeholder', 'alt', 'title', + 'string', 'help', 'sum', 'avg', 'confirm', 'placeholder', 'alt', 'title', 'aria-label', + 'aria-keyshortcuts', 'aria-placeholder', 'aria-roledescription', 'aria-valuetext', } +TRANSLATED_ATTRS = TRANSLATED_ATTRS | {'t-attf-' + attr for attr in TRANSLATED_ATTRS} + avoid_pattern = re.compile(r"\s*<!DOCTYPE", re.IGNORECASE | re.MULTILINE | re.UNICODE) node_pattern = re.compile(r"<[^>]*>(.*)</[^<]*>", re.DOTALL | re.MULTILINE | re.UNICODE) diff --git a/odoo/tools/view_validation.py b/odoo/tools/view_validation.py index 1ffdd43e58b..7c69906220d 100644 --- a/odoo/tools/view_validation.py +++ b/odoo/tools/view_validation.py @@ -15,9 +15,13 @@ _relaxng_cache = {} def valid_view(arch): for pred in _validators[arch.tag]: - if not pred(arch): + check = pred(arch) + if not check: _logger.error("Invalid XML: %s", pred.__doc__) return False + if check == "Warning": + _logger.warning("Invalid XML: %s", pred.__doc__) + return "Warning" return True @@ -88,8 +92,8 @@ def valid_att_in_field(arch): @validate('form') def valid_att_in_label(arch): - """ ``label`` nodes must have a ``@for`` or a ``@string`` """ - return not arch.xpath('//label[not(@for or @string)]') + """ ``label`` nodes must have a ``@for`` """ + return not arch.xpath('//label[not(@for) and not(descendant::input)]') @validate('form') @@ -113,3 +117,235 @@ def valid_type_in_col(arch): attrib.isdigit() for attrib in arch.xpath('//@col') ) + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_alternative_image_text(arch): + """An `img` tag must have an alt value.""" + if arch.xpath('//img[not(@alt or @t-att-alt or @t-attf-alt)]'): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_alternative_icon_text(arch): + """An icon with fa- class or in a button must have aria-label in its tag, parents, descendants or have text.""" + valid_aria_attrs = { + 'aria-label', 'aria-labelledby', 't-att-aria-label', 't-attf-aria-label', + 't-att-aria-labelledby', 't-attf-aria-labelledby'} + valid_t_attrs = {'t-value', 't-raw', 't-field', 't-esc'} + valid_attrs = valid_aria_attrs | valid_t_attrs + valid_aria_attrs_xpath = ' or '.join('@' + attr for attr in valid_aria_attrs) + valid_attrs_xpath = ' or '.join('@' + attr for attr in valid_attrs) + + # Select elements with class begining by 'fa-' + xpath = '(//*[contains(concat(" ", @class), " fa-")' + xpath += ' or contains(concat(" ", @t-att-class), " fa-")' + xpath += ' or contains(concat(" ", @t-attf-class), " fa-")]' + xpath += ' | //button[@icon])' + # Elements with accessibility or string attrs are good + xpath += '[not(' + valid_attrs_xpath + ')]' + # And we ignore all elements with describing in children + xpath += '[not(//*[' + valid_attrs_xpath + '])]' + # Aria label can be on ancestors + xpath += '[not(ancestor[' + valid_aria_attrs_xpath + '])]' + # Labels provide text by definition + xpath += '[not(descendant-or-self::label)]' + # Buttons can have a string attribute + xpath += '[not(descendant-or-self::button[@string])]' + # Fields have labels + xpath += '[not(descendant-or-self::field)]' + # And finally, if there is some text, it's good too + xpath += '[not(descendant-or-self::*[text()])]' + + if arch.xpath(xpath): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_title_icon(arch): + """An icon with fa- class or in a button must have title in its tag, parents, descendants or have text.""" + valid_title_attrs = {'title', 't-att-title', 't-attf-title'} + valid_t_attrs = {'t-value', 't-raw', 't-field', 't-esc'} + valid_attrs = valid_title_attrs | valid_t_attrs + valid_title_attrs_xpath = ' or '.join('@' + attr for attr in valid_title_attrs) + valid_attrs_xpath = ' or '.join('@' + attr for attr in valid_attrs) + + # Select elements with class begining by 'fa-' + xpath = '(//*[contains(concat(" ", @class), " fa-")' + xpath += ' or contains(concat(" ", @t-att-class), " fa-")' + xpath += ' or contains(concat(" ", @t-attf-class), " fa-")]' + xpath += ' | //button[@icon])' + # Elements with accessibility or string attrs are good + xpath += '[not(' + valid_attrs_xpath + ')]' + # And we ignore all elements with describing in children + xpath += '[not(//*[' + valid_attrs_xpath + '])]' + # Aria label can be on ancestors + xpath += '[not(ancestor[' + valid_title_attrs_xpath + '])]' + # Labels provide text by definition + xpath += '[not(descendant-or-self::label)]' + # Buttons can have a string attribute + xpath += '[not(descendant-or-self::button[@string])]' + # Fields have labels + xpath += '[not(descendant-or-self::field)]' + # And finally, if there is some text, it's good too + xpath += '[not(descendant-or-self::*[text()])]' + + if arch.xpath(xpath): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_simili_button(arch): + """A simili button must be tagged with "role='button'".""" + # Select elements with class 'btn' + xpath = '//a[contains(concat(" ", @class), " btn")' + xpath += ' or contains(concat(" ", @t-att-class), " btn")' + xpath += ' or contains(concat(" ", @t-attf-class), " btn")]' + xpath += '[not(@role="button")]' + if arch.xpath(xpath): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_simili_dropdown(arch): + """A simili dropdown must be tagged with "role='menu'".""" + xpath = '//*[contains(concat(" ", @class, " "), " dropdown-menu ")' + xpath += ' or contains(concat(" ", @t-att-class, " "), " dropdown-menu ")' + xpath += ' or contains(concat(" ", @t-attf-class, " "), " dropdown-menu ")]' + xpath += '[not(@role="menu")]' + if arch.xpath(xpath): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_simili_progressbar(arch): + """A simili progressbar must be tagged with "role='progressbar'" and have + aria-valuenow, aria-valuemin and aria-valuemax attributes.""" + # Select elements with class 'btn' + xpath = '//*[contains(concat(" ", @class, " "), " o_progressbar ")' + xpath += ' or contains(concat(" ", @t-att-class, " "), " o_progressbar ")' + xpath += ' or contains(concat(" ", @t-attf-class, " "), " o_progressbar ")]' + xpath += '[not(self::progress)]' + xpath += '[not(@role="progressbar")]' + xpath += '[not(@aria-valuenow or @t-att-aria-valuenow or @t-attf-aria-valuenow)]' + xpath += '[not(@aria-valuemin or @t-att-aria-valuemin or @t-attf-aria-valuemin)]' + xpath += '[not(@aria-valuemax or @t-att-aria-valuemax or @t-attf-aria-valuemax)]' + if arch.xpath(xpath): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_dialog(arch): + """A dialog must use role="dialog" and its header, body and footer contents must use <header/>, <main/> and <footer/>.""" + # Select elements with class 'btn' + xpath = '//*[contains(concat(" ", @class, " "), " modal ")' + xpath += ' or contains(concat(" ", @t-att-class, " "), " modal ")' + xpath += ' or contains(concat(" ", @t-attf-class, " "), " modal ")]' + xpath += '[not(@role="dialog")]' + if arch.xpath(xpath): + return "Warning" + + xpath = '//*[contains(concat(" ", @class, " "), " modal-header ")' + xpath += ' or contains(concat(" ", @t-att-class, " "), " modal-header ")' + xpath += ' or contains(concat(" ", @t-attf-class, " "), " modal-header ")]' + xpath += '[not(self::header)]' + if arch.xpath(xpath): + return "Warning" + + xpath = '//*[contains(concat(" ", @class, " "), " modal-body ")' + xpath += ' or contains(concat(" ", @t-att-class, " "), " modal-body ")' + xpath += ' or contains(concat(" ", @t-attf-class, " "), " modal-body ")]' + xpath += '[not(self::main)]' + if arch.xpath(xpath): + return "Warning" + + xpath = '//*[contains(concat(" ", @class, " "), " modal-footer ")' + xpath += ' or contains(concat(" ", @t-att-class, " "), " modal-footer ")' + xpath += ' or contains(concat(" ", @t-attf-class, " "), " modal-footer ")]' + xpath += '[not(self::footer)]' + if arch.xpath(xpath): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_simili_tabpanel(arch): + """A tab panel with tab-pane class must have role="tabpanel".""" + # Select elements with class 'btn' + xpath = '//*[contains(concat(" ", @class, " "), " tab-pane ")' + xpath += ' or contains(concat(" ", @t-att-class, " "), " tab-pane ")' + xpath += ' or contains(concat(" ", @t-attf-class, " "), " tab-pane ")]' + xpath += '[not(@role="tabpanel")]' + if arch.xpath(xpath): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_simili_tab(arch): + """A tab link must have role="tab", a link to an id (without #) by aria-controls.""" + # Select elements with class 'btn' + xpath = '//*[@data-toggle="tab"]' + xpath += '[not(@role="tab")' + xpath += 'or not(@aria-controls or @t-att-aria-controls or @t-attf-aria-controls)' + xpath += 'or contains(@aria-controls, "#") or contains(@t-att-aria-controls, "#")]' + if arch.xpath(xpath): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_simili_tablist(arch): + """A tab list with class nav-tabs must have role="tablist".""" + # Select elements with class 'btn' + xpath = '//*[contains(concat(" ", @class, " "), " nav-tabs ")' + xpath += ' or contains(concat(" ", @t-att-class, " "), " nav-tabs ")' + xpath += ' or contains(concat(" ", @t-attf-class, " "), " nav-tabs ")]' + xpath += '[not(@role="tablist")]' + if arch.xpath(xpath): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_focusable_button(arch): + """A simili button must be with a `button`, an `input` (with type `button`, `submit` or `reset`) or a `a` tag.""" + xpath = '//*[contains(concat(" ", @class), " btn")' + xpath += ' or contains(concat(" ", @t-att-class), " btn")' + xpath += ' or contains(concat(" ", @t-attf-class), " btn")]' + xpath += '[not(self::a)]' + xpath += '[not(self::button)]' + xpath += '[not(self::select)]' + xpath += '[not(self::input[@type="button"])]' + xpath += '[not(self::input[@type="submit"])]' + xpath += '[not(self::input[@type="reset"])]' + xpath += '[not(contains(@class, "btn-group"))]' + xpath += '[not(contains(@t-att-class, "btn-group"))]' + xpath += '[not(contains(@t-attf-class, "btn-group"))]' + xpath += '[not(contains(@class, "btn-toolbar"))]' + xpath += '[not(contains(@t-att-class, "btn-toolbar"))]' + xpath += '[not(contains(@t-attf-class, "btn-toolbar"))]' + xpath += '[not(contains(@class, "btn-ship"))]' + xpath += '[not(contains(@t-att-class, "btn-ship"))]' + xpath += '[not(contains(@t-attf-class, "btn-ship"))]' + if arch.xpath(xpath): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_prohibited_none_role(arch): + """A role can't be `none` or `presentation`. All your elements must be accessible with screen readers, describe it.""" + xpath = '//*[@role="none" or @role="presentation"]' + if arch.xpath(xpath): + return "Warning" + return True + +@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree') +def valid_alerts(arch): + """An alert (class alert-*) must have an alert, alertdialog or status role. Please use alert and alertdialog only for what expects to stop any activity to be read immediatly.""" + xpath = '//*[contains(concat(" ", @class), " alert-")' + xpath += ' or contains(concat(" ", @t-att-class), " alert-")' + xpath += ' or contains(concat(" ", @t-attf-class), " alert-")]' + xpath += '[not(contains(@class, "alert-link") or contains(@t-att-class, "alert-link")' + xpath += ' or contains(@t-attf-class, "alert-link"))]' + xpath += '[not(@role="alert")]' + xpath += '[not(@role="alertdialog")]' + xpath += '[not(@role="status")]' + if arch.xpath(xpath): + return "Warning" + return True |
