summaryrefslogtreecommitdiff
path: root/odoo
diff options
context:
space:
mode:
authorkujiu <kujiu@users.noreply.github.com>2018-06-22 21:22:21 +0200
committerGitHub <noreply@github.com>2018-06-22 21:22:21 +0200
commit9de1bc0eef6f5bfaa2a8d745431caa361ae91548 (patch)
tree342ddbf7fb2b7303606f864ff9f68cbf1bd8814e /odoo
parent864357203fc6222cc989b99a5e9f428cd53f413f (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')
-rw-r--r--odoo/addons/base/models/ir_ui_view.py7
-rw-r--r--odoo/addons/base/rng/common.rng3
-rw-r--r--odoo/addons/base/tests/test_view_validation.py75
-rw-r--r--odoo/addons/base/views/ir_module_views.xml2
-rw-r--r--odoo/addons/base/views/ir_qweb_widget_templates.xml14
-rw-r--r--odoo/addons/base/views/ir_rule_views.xml2
-rw-r--r--odoo/addons/base/views/ir_sequence_views.xml22
-rw-r--r--odoo/addons/base/views/res_company_views.xml4
-rw-r--r--odoo/addons/base/views/res_config_settings_views.xml2
-rw-r--r--odoo/addons/base/views/res_partner_views.xml22
-rw-r--r--odoo/addons/base/views/res_users_views.xml12
-rw-r--r--odoo/addons/base/wizard/base_language_install_views.xml4
-rw-r--r--odoo/addons/base/wizard/base_module_uninstall_views.xml2
-rw-r--r--odoo/addons/base/wizard/base_module_update_views.xml2
-rw-r--r--odoo/addons/base/wizard/base_module_upgrade_views.xml4
-rw-r--r--odoo/addons/test_exceptions/view.xml46
-rw-r--r--odoo/addons/test_main_flows/static/src/js/tour.js46
-rw-r--r--odoo/addons/test_new_api/static/tests/x2many.js32
-rw-r--r--odoo/api.py7
-rw-r--r--odoo/exceptions.py7
-rw-r--r--odoo/tools/translate.py5
-rw-r--r--odoo/tools/view_validation.py242
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 ('&lt;br/&gt;%s' % ('' if options.get('no_marker') else '&amp;nbsp; &amp;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 ('&lt;br/&gt;%s' % ('' if options.get('no_marker') else '&amp;nbsp; &amp;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' &amp;&amp; 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' &amp;&amp; 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