[IMP] base,mail: allow to customize the activity view

The activity view has been greatly improved to allow to customize it
more easily. It works quite similarly to the kanban view, defining
`<field>` tags at the top and using these fields in the `<template>`
section. The template name used to define the activity cards is
`activity-box`.

Note that these activity cards are rendered using `KanbanRecord` widget
(this has implied that ActivityView inherits from `BasicView`).

Also note that the view validation has been moved in base to include the
common grammar.

Task 1894990
This commit is contained in:
Hiral Bhavsar
2019-05-03 10:19:40 +00:00
committed by Martin Geubelle
parent b29d4da037
commit b2de618f9f
16 changed files with 310 additions and 163 deletions
-1
View File
@@ -3,4 +3,3 @@
from . import models
from . import wizard
from . import controllers
from . import validation
+1
View File
@@ -16,6 +16,7 @@ from . import mail_blacklist
from . import mail_channel
from . import mail_template
from . import mail_shortcode
from . import models
from . import res_partner
from . import res_users
from . import res_company
+1 -4
View File
@@ -524,8 +524,6 @@ class MailActivity(models.Model):
'state': state,
'o_closest_deadline': group['date_deadline'],
}
res_ids_sorted = sorted(res_id_to_deadline, key=lambda item: res_id_to_deadline[item])
res_id_to_name = dict(self.env[res_model].browse(res_ids_sorted).name_get())
activity_type_infos = []
for elem in sorted(activity_type_ids, key=lambda item: item.sequence):
mail_template_info = []
@@ -535,9 +533,8 @@ class MailActivity(models.Model):
return {
'activity_types': activity_type_infos,
'res_ids': [(rid, res_id_to_name[rid]) for rid in res_ids_sorted],
'activity_res_ids': sorted(res_id_to_deadline, key=lambda item: res_id_to_deadline[item]),
'grouped_activities': activity_data,
'model': res_model,
}
+19
View File
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from odoo import models, api
from lxml.builder import E
class BaseModel(models.AbstractModel):
_inherit = 'base'
@api.model
def _get_default_activity_view(self):
""" Generates an empty activity view.
:returns: a activity view as an lxml document
:rtype: etree._Element
"""
field = E.field(name=self._rec_name_fallback())
activity_box = E.div(field, {'t-name': "activity-box"})
templates = E.templates(activity_box)
return E.activity(templates, string=self._description)
@@ -1,34 +1,30 @@
odoo.define('mail.ActivityController', function (require) {
"use strict";
var AbstractController = require('web.AbstractController');
var BasicController = require('web.BasicController');
var ActivityController = AbstractController.extend({
custom_events: _.extend({}, AbstractController.prototype.custom_events, {
var ActivityController = BasicController.extend({
custom_events: _.extend({}, BasicController.prototype.custom_events, {
send_mail_template: '_onSendMailTemplate',
open_view_form: '_onOpenViewForm',
reload: '_onReload',
}),
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Overridden to remove the pager as it makes no sense in this view.
*
* @override
*/
renderPager: function () {
return Promise.resolve();
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {OdooEvent} event
* @param {string} event.name
* @param {Object} event.data
* @param {boolean} [event.data.activity]
* @param {boolean} [event.data.followers]
* @param {boolean} [event.data.thread]
*/
_onReload: function (event) {
event.stopPropagation();
var self = this;
this.model.reload().then(self.reload());
},
/**
* @private
* @param {OdooEvent} ev
@@ -36,8 +32,8 @@ var ActivityController = AbstractController.extend({
_onSendMailTemplate: function (ev) {
var templateID = ev.data.templateID;
var activityTypeID = ev.data.activityTypeID;
var state = this.model.get();
var groupedActivities = state.data.grouped_activities;
var state = this.model.get(this.handle);
var groupedActivities = state.grouped_activities;
var resIDS = [];
Object.keys(groupedActivities).forEach(function (resID) {
var activityByType = groupedActivities[resID];
@@ -52,21 +48,6 @@ var ActivityController = AbstractController.extend({
args: [resIDS, templateID],
});
},
/**
* @private
* @override
* @param {MouseEvent} ev
*/
_onOpenViewForm: function (ev) {
var resID = ev.data.resID;
this.do_action({
type: 'ir.actions.act_window',
res_model: this.model.modelName,
res_id: resID,
views: [[false, 'form']],
target: 'current'
});
},
});
return ActivityController;
@@ -1,43 +1,61 @@
odoo.define('mail.ActivityModel', function (require) {
'use strict';
var AbstractModel = require('web.AbstractModel');
var BasicModel = require('web.BasicModel');
var session = require('web.session');
var ActivityModel = AbstractModel.extend({
var ActivityModel = BasicModel.extend({
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
* Add the following (activity specific) keys when performing a `get` on the
* main list datapoint:
* - activity_types
* - activity_res_ids
* - grouped_activities
*
* @override
*/
get: function () {
return this.data;
var result = this._super.apply(this, arguments);
if (result && result.model === this.modelName && result.type === 'list') {
_.extend(result, this.additionalData);
}
return result;
},
/**
* @override
* @param {Object} params
* @param {Array[]} params.domain
* @returns {Promise}
*/
load: function (params) {
params.domain.push(['activity_ids', '!=', false]);
this.domain = params.domain;
this.modelName = params.modelName;
this.data = {};
return this._fetchData();
params.groupedBy = [];
var def = this._super.apply(this, arguments);
return Promise.all([def, this._fetchData()]).then(function (result) {
return result[0];
});
},
/**
* @param {any} handle
* @param {Object} params
* @param {Array[]} params.domain
* @returns {Promise}
* @override
* @param {Array[]} [params.domain]
*/
reload: function (handle, params) {
if (params && 'domain' in params) {
params.domain.push(['activity_ids', '!=', false]);
this.domain = params.domain;
}
return this._fetchData();
if (params && 'groupBy' in params) {
params.groupBy = [];
}
var def = this._super.apply(this, arguments);
return Promise.all([def, this._fetchData()]).then(function (result) {
return result[0];
});
},
//--------------------------------------------------------------------------
@@ -61,7 +79,7 @@ var ActivityModel = AbstractModel.extend({
context: session.user_context,
}
}).then(function (result) {
self.data.data = result;
self.additionalData = result;
});
},
});
@@ -0,0 +1,62 @@
odoo.define('mail.ActivityRecord', function (require) {
"use strict";
var KanbanRecord = require('web.KanbanRecord');
var ActivityRecord = KanbanRecord.extend({
/**
* @override
*/
init: function (parent, state) {
this._super.apply(this,arguments);
this.fieldsInfo = state.fieldsInfo.activity;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
* @private
*/
_render: function () {
this.defs = [];
this._replaceElement(this.qweb.render('activity-box', this.qweb_context));
this.$el.on('click', this._onGlobalClick.bind(this));
this.$el.addClass('o_activity_record');
this._processFields();
this._setupColor();
return Promise.all(this.defs);
},
/**
* @override
* @private
*/
_setFieldDisplay: function ($el, fieldName) {
this._super.apply(this, arguments);
// attribute muted
if (this.fieldsInfo[fieldName].muted) {
$el.addClass('text-muted');
}
},
/**
* @override
* @private
*/
_setState: function () {
this._super.apply(this, arguments);
// activity has a different qweb context
this.qweb_context = {
activity_image: this._getImageURL.bind(this),
record: this.record,
user_context: this.getSession().user_context,
widget: this,
};
},
});
return ActivityRecord;
});
@@ -2,23 +2,45 @@ odoo.define('mail.ActivityRenderer', function (require) {
"use strict";
var AbstractRenderer = require('web.AbstractRenderer');
var ActivityRecord = require('mail.ActivityRecord');
var core = require('web.core');
var field_registry = require('web.field_registry');
var qweb = require('web.QWeb');
var session = require('web.session');
var utils = require('web.utils');
var KanbanActivity = field_registry.get('kanban_activity');
var _t = core._t;
var QWeb = core.qweb;
var ActivityRenderer = AbstractRenderer.extend({
className: 'o_activity_view',
events: {
'click .o_res_name_cell': '_onResNameClicked',
'click .o_send_mail_template': '_onSenMailTemplateClicked',
},
/**
* @override
* @param {Object} params.templates
*/
init: function (parent, state, params) {
this._super.apply(this, arguments);
this.qweb = new qweb(session.debug, {_s: session.origin});
this.qweb.add_template(utils.json_node_to_xml(params.templates));
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
* @private
*/
_getRecord: function (recordId) {
return _.findWhere(this.state.data, { res_id: recordId });
},
/**
* @override
* @private
@@ -28,7 +50,7 @@ var ActivityRenderer = AbstractRenderer.extend({
.removeClass('table-responsive')
.empty();
if (this.state.data.activity_types.length === 0) {
if (this.state.activity_types.length === 0) {
this.$el.append(QWeb.render('ActivityView.nodata'));
} else {
var $table = $('<table>')
@@ -46,7 +68,7 @@ var ActivityRenderer = AbstractRenderer.extend({
* @returns {jQueryElement} a jquery element <tbody>
*/
_renderBody: function () {
var $rows = _.map(this.state.data.res_ids, this._renderRow.bind(this));
var $rows = _.map(this.state.activity_res_ids, this._renderRow.bind(this));
return $('<tbody>').append($rows);
},
/**
@@ -56,7 +78,7 @@ var ActivityRenderer = AbstractRenderer.extend({
_renderHeader: function () {
var $tr = $('<tr>')
.append($('<th>')) //empty cell for name
.append(_.map(this.state.data.activity_types, this._renderHeaderCell.bind(this)));
.append(_.map(this.state.activity_types, this._renderHeaderCell.bind(this)));
return $('<thead>').append($tr);
},
/**
@@ -73,22 +95,23 @@ var ActivityRenderer = AbstractRenderer.extend({
},
/**
* @private
* @param {Object} data
* @param {integer} resId
* @returns {jQueryElement} a <tr> element
*/
_renderRow: function (data) {
_renderRow: function (resId) {
var self = this;
var res_id = data[0];
var name = data[1];
var $nameTD = $('<td>')
.addClass("o_res_name_cell")
.html(name)
.data('res-id', res_id);
var $cells = _.map(this.state.data.activity_types, function (node) {
var record = this._getRecord(resId);
var $nameTD = $('<td>', {
class: _.contains(this.filteredResIDs, resId) ? 'o_activity_filter_' + this.activeFilter : '',
});
var activityRecord = new ActivityRecord(this, record, { qweb: this.qweb });
this.defs.push(activityRecord.appendTo($nameTD));
var $cells = _.map(this.state.activity_types, function (node) {
var $td = $('<td>').addClass("o_activity_summary_cell");
var activity_type_id = node[0];
var activity_group = self.state.data.grouped_activities[res_id][activity_type_id];
activity_group = activity_group || {count: 0, ids: [], state: false};
var activity_group = self.state.grouped_activities[resId];
activity_group = activity_group && activity_group[activity_type_id] || {count: 0, ids: [], state: false};
if (activity_group.state) {
$td.addClass(activity_group.state);
}
@@ -114,16 +137,15 @@ var ActivityRenderer = AbstractRenderer.extend({
},
},
fieldsInfo: {},
model: self.state.data.model,
ref: res_id, // not necessary, i think
model: self.state.model,
ref: resId, // not necessary, i think
type: 'record',
res_id: res_id,
res_id: resId,
getContext: function () {
return {}; // session.user_context
},
//todo intercept event or changes on record to update view
};
var KanbanActivity = field_registry.get('kanban_activity');
var widget = new KanbanActivity(self, "activity_ids", record, {});
widget.appendTo($td).then(function() {
// replace clock by closest deadline
@@ -166,15 +188,6 @@ var ActivityRenderer = AbstractRenderer.extend({
templateID: templateID,
});
},
/**
* @private
* @override
* @param {MouseEvent} ev
*/
_onResNameClicked: function (ev) {
var resID = $(ev.currentTarget).data('res-id');
this.trigger_up('open_view_form', {resID: resID});
},
});
return ActivityRenderer;
@@ -4,23 +4,36 @@ odoo.define('mail.ActivityView', function (require) {
var ActivityController = require('mail.ActivityController');
var ActivityModel = require('mail.ActivityModel');
var ActivityRenderer = require('mail.ActivityRenderer');
var AbstractView = require('web.AbstractView');
var BasicView = require('web.BasicView');
var core = require('web.core');
var view_registry = require('web.view_registry');
var _lt = core._lt;
var ActivityView = AbstractView.extend({
var ActivityView = BasicView.extend({
accesskey: "a",
display_name: _lt('Activity'),
icon: 'fa-clock-o',
config: _.extend({}, AbstractView.prototype.config, {
config: _.extend({}, BasicView.prototype.config, {
Controller: ActivityController,
Model: ActivityModel,
Renderer: ActivityRenderer,
}),
viewType: 'activity',
searchMenuTypes: ['filter', 'favorite'],
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
this.loadParams.type = 'list';
// limit makes no sense in this view as we display all records having activities
this.loadParams.limit = false;
this.rendererParams.templates = _.findWhere(this.arch.children, { 'tag': 'templates' });
},
});
view_registry.add('activity', ActivityView);
+51 -7
View File
@@ -27,15 +27,59 @@
line-height: 42px;
}
}
}
.o_res_name_cell {
padding: 0 10px;
color: #008784;
&:hover {
color: #666;
cursor: pointer;
> .o_activity_empty_cell {
display: none;
}
&:hover > .o_activity_empty_cell {
color: gray;
display: block;
font-style: italic;
}
.o_activity_btn > span {
bottom: 5px;
position: relative;
&.planned {
color: theme-color('success');
}
&.overdue {
color: theme-color('danger');
}
&.today {
color: theme-color('warning');
}
}
}
// it contains a kanban card representing the record
.o_activity_record {
display: flex;
flex: 1 1 auto;
align-items: center;
padding: 8px 8px;
cursor: pointer;
> img {
width: 32px;
max-height: 32px;
margin-right: 16px;
}
> div {
max-width: 200px;
.o_text_block {
@include o-text-overflow;
display: block;
}
}
.o_text_bold {
font-weight: bold;
}
.o_text_block {
display: block;
}
}
.o_activity_type_cell {
padding:10px;
-30
View File
@@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
import logging
import os
from lxml import etree
from odoo.loglevels import ustr
from odoo.tools import misc, view_validation
_logger = logging.getLogger(__name__)
_activity_validator = None
@view_validation.validate('activity')
def schema_activity(arch):
""" Check the activity view against its schema
:type arch: etree._Element
"""
global _activity_validator
if _activity_validator is None:
with misc.file_open(os.path.join('mail', 'views', 'activity.rng')) as f:
_activity_validator = etree.RelaxNG(etree.parse(f))
if _activity_validator.validate(arch):
return True
for error in _activity_validator.error_log:
_logger.error(ustr(error))
return False
+1
View File
@@ -77,6 +77,7 @@
<script type="text/javascript" src="/mail/static/src/js/views/activity/activity_model.js"></script>
<script type="text/javascript" src="/mail/static/src/js/views/activity/activity_controller.js"></script>
<script type="text/javascript" src="/mail/static/src/js/views/activity/activity_renderer.js"></script>
<script type="text/javascript" src="/mail/static/src/js/views/activity/activity_record.js"></script>
<!-- Attachment box -->
<script type="text/javascript" src="/mail/static/src/js/attachment_box.js"></script>
<!-- Others -->
+36 -11
View File
@@ -478,9 +478,9 @@ system. Available semantic components are:
``field``
renders (and allow edition of, possibly) a single field of the current
record. Using several times a field in a form view is supported and the fields
record. Using several times a field in a form view is supported and the fields
can receive different values for modifiers 'invisible' and 'readonly'. However,
the behavior is not guaranteed when several fields exist with different values
the behavior is not guaranteed when several fields exist with different values
for modifier 'required'. Possible attributes of the field node are:
``name`` (mandatory)
@@ -1697,26 +1697,51 @@ Activity
The Activity view is used to display the activities linked to the records. The
data are displayed in a chart with the records forming the rows and the activity
types the columns. When clicking on a cell, a detailed description of all
activities of the same type for the record is displayed.
types the columns. The first cell of each row displays a (customizable, see
``templates``, quite similarly to :ref:`reference/views/kanban`) card representing
the corresponding record. When clicking on others cells, a detailed description
of all activities of the same type for the record is displayed.
.. warning::
The Activity view is only available when the ``mail`` module is installed,
and for the models that inherit from the ``mail.activity.mixin``.
For example, here is a very simple Activity view:
.. code-block:: xml
<activity string="Activities"/>
The root element of the Activity view is <activity>, it accepts the following
The root element of the Activity view is ``<activity>``, it accepts the following
attributes:
- ``string`` (mandatory)
A title, which should describe the view
Possible children of the view element are:
``field``
declares fields to use in activity *logic*. If the field is simply displayed
in the activity view, it does not need to be pre-declared.
Possible attributes are:
``name`` (required)
the name of the field to fetch
``templates``
defines the :ref:`reference/qweb` templates. Cards definition may be
split into multiple templates for clarity, but activity views *must* define at
least one root template ``activity-box``, which will be rendered once for each
record.
The activity view uses mostly-standard :ref:`javascript qweb
<reference/qweb/javascript>` and provides the following context variables
(see :ref:`reference/views/kanban` for more details):
``widget``
the current :js:class:`ActivityRecord`, can be used to fetch some
meta-information. These methods are also available directly in the
template context and don't need to be accessed via ``widget``
``record``
an object with all the requested fields as its attributes. Each field has
two attributes ``value`` and ``raw_value``
.. _reference/views/search:
Search
@@ -3,13 +3,26 @@
xmlns:a="http://relaxng.org/ns/annotation/1.0"
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<start>
<ref name="activity"/>
</start>
<include href="common.rng"/>
<define name="activity">
<element name="activity">
<attribute name="string"/>
<interleave>
<zeroOrMore><ref name="field"/></zeroOrMore>
<optional>
<element name="templates">
<oneOrMore>
<ref name="any"/>
</oneOrMore>
</element>
</optional>
</interleave>
</element>
</define>
<start>
<ref name="activity"/>
</start>
</grammar>
-9
View File
@@ -1268,15 +1268,6 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
element = E.field(name=self._rec_name_fallback())
return E.tree(element, string=self._description)
@api.model
def _get_default_activity_view(self):
""" Generates an empty activity view.
:returns: a activity view as an lxml document
:rtype: etree._Element
"""
return E.activity(string=self._description)
@api.model
def _get_default_pivot_view(self):
""" Generates an empty pivot view.
+15 -15
View File
@@ -48,7 +48,7 @@ def relaxng(view_type):
return _relaxng_cache[view_type]
@validate('calendar', 'diagram', 'gantt', 'graph', 'pivot', 'search', 'tree')
@validate('calendar', 'diagram', 'gantt', 'graph', 'pivot', 'search', 'tree', 'activity')
def schema_valid(arch):
""" Get RNG validator and validate RNG file."""
validator = relaxng(arch.tag)
@@ -84,7 +84,7 @@ def valid_field_in_tree(arch):
)
@validate('form', 'graph', 'tree')
@validate('form', 'graph', 'tree', 'activity')
def valid_att_in_field(arch):
""" ``field`` nodes must all have a ``@name`` """
return not arch.xpath('//field[not(@name)]')
@@ -118,14 +118,14 @@ def valid_type_in_col(arch):
for attrib in arch.xpath('//@col')
)
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree')
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
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')
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
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 = {
@@ -166,7 +166,7 @@ def valid_alternative_icon_text(arch):
return "Warning"
return True
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree')
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
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'}
@@ -205,7 +205,7 @@ def valid_title_icon(arch):
return "Warning"
return True
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree')
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
def valid_simili_button(arch):
"""A simili button must be tagged with "role='button'"."""
# Select elements with class 'btn'
@@ -217,7 +217,7 @@ def valid_simili_button(arch):
return "Warning"
return True
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree')
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
def valid_simili_dropdown(arch):
"""A simili dropdown must be tagged with "role='menu'"."""
xpath = '//*[contains(concat(" ", @class, " "), " dropdown-menu ")'
@@ -228,7 +228,7 @@ def valid_simili_dropdown(arch):
return "Warning"
return True
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree')
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
def valid_simili_progressbar(arch):
"""A simili progressbar must be tagged with "role='progressbar'" and have
aria-valuenow, aria-valuemin and aria-valuemax attributes."""
@@ -245,7 +245,7 @@ def valid_simili_progressbar(arch):
return "Warning"
return True
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree')
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
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'
@@ -278,7 +278,7 @@ def valid_dialog(arch):
return "Warning"
return True
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree')
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
def valid_simili_tabpanel(arch):
"""A tab panel with tab-pane class must have role="tabpanel"."""
# Select elements with class 'btn'
@@ -290,7 +290,7 @@ def valid_simili_tabpanel(arch):
return "Warning"
return True
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree')
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
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'
@@ -302,7 +302,7 @@ def valid_simili_tab(arch):
return "Warning"
return True
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree')
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
def valid_simili_tablist(arch):
"""A tab list with class nav-tabs must have role="tablist"."""
# Select elements with class 'btn'
@@ -314,7 +314,7 @@ def valid_simili_tablist(arch):
return "Warning"
return True
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree')
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
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")'
@@ -339,7 +339,7 @@ def valid_focusable_button(arch):
return "Warning"
return True
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree')
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
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"]'
@@ -347,7 +347,7 @@ def valid_prohibited_none_role(arch):
return "Warning"
return True
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree')
@validate('calendar', 'diagram', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
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-")'