diff --git a/addons/mail/__init__.py b/addons/mail/__init__.py index 9226c7d74ad..f2cd96d8c83 100644 --- a/addons/mail/__init__.py +++ b/addons/mail/__init__.py @@ -3,4 +3,3 @@ from . import models from . import wizard from . import controllers -from . import validation diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py index 7df60f71277..9b82416fed2 100644 --- a/addons/mail/models/__init__.py +++ b/addons/mail/models/__init__.py @@ -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 diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index f5bfd7c11cf..a8f7cdbf6c2 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -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, } diff --git a/addons/mail/models/models.py b/addons/mail/models/models.py new file mode 100644 index 00000000000..4c85b08d2af --- /dev/null +++ b/addons/mail/models/models.py @@ -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) diff --git a/addons/mail/static/src/js/views/activity/activity_controller.js b/addons/mail/static/src/js/views/activity/activity_controller.js index 6af4c310c01..4ed543dd3ec 100644 --- a/addons/mail/static/src/js/views/activity/activity_controller.js +++ b/addons/mail/static/src/js/views/activity/activity_controller.js @@ -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; diff --git a/addons/mail/static/src/js/views/activity/activity_model.js b/addons/mail/static/src/js/views/activity/activity_model.js index f5a2feee988..f464cf6a92f 100644 --- a/addons/mail/static/src/js/views/activity/activity_model.js +++ b/addons/mail/static/src/js/views/activity/activity_model.js @@ -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; }); }, }); diff --git a/addons/mail/static/src/js/views/activity/activity_record.js b/addons/mail/static/src/js/views/activity/activity_record.js new file mode 100644 index 00000000000..98da9dcaf8f --- /dev/null +++ b/addons/mail/static/src/js/views/activity/activity_record.js @@ -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; +}); diff --git a/addons/mail/static/src/js/views/activity/activity_renderer.js b/addons/mail/static/src/js/views/activity/activity_renderer.js index 75eb25367e4..640a8859767 100644 --- a/addons/mail/static/src/js/views/activity/activity_renderer.js +++ b/addons/mail/static/src/js/views/activity/activity_renderer.js @@ -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 = $('') @@ -46,7 +68,7 @@ var ActivityRenderer = AbstractRenderer.extend({ * @returns {jQueryElement} a jquery element */ _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 $('').append($rows); }, /** @@ -56,7 +78,7 @@ var ActivityRenderer = AbstractRenderer.extend({ _renderHeader: function () { var $tr = $('') .append($('').append($tr); }, /** @@ -73,22 +95,23 @@ var ActivityRenderer = AbstractRenderer.extend({ }, /** * @private - * @param {Object} data + * @param {integer} resId * @returns {jQueryElement} a element */ - _renderRow: function (data) { + _renderRow: function (resId) { var self = this; - var res_id = data[0]; - var name = data[1]; - var $nameTD = $('
')) //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 $('
') - .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 = $('', { + 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 = $('').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; diff --git a/addons/mail/static/src/js/views/activity/activity_view.js b/addons/mail/static/src/js/views/activity/activity_view.js index 9f0cec6b761..b21a68215d1 100644 --- a/addons/mail/static/src/js/views/activity/activity_view.js +++ b/addons/mail/static/src/js/views/activity/activity_view.js @@ -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); diff --git a/addons/mail/static/src/scss/activity_view.scss b/addons/mail/static/src/scss/activity_view.scss index f1eda8ae86a..c24b2de3565 100644 --- a/addons/mail/static/src/scss/activity_view.scss +++ b/addons/mail/static/src/scss/activity_view.scss @@ -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; diff --git a/addons/mail/validation.py b/addons/mail/validation.py deleted file mode 100644 index 743c40f66dc..00000000000 --- a/addons/mail/validation.py +++ /dev/null @@ -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 diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml index ea742a29fda..cd64ca186ca 100644 --- a/addons/mail/views/mail_templates.xml +++ b/addons/mail/views/mail_templates.xml @@ -77,6 +77,7 @@ + diff --git a/doc/reference/views.rst b/doc/reference/views.rst index 767bb8bbe5b..bd81e623d63 100644 --- a/doc/reference/views.rst +++ b/doc/reference/views.rst @@ -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 - - - -The root element of the Activity view is , it accepts the following +The root element of the Activity view is ````, 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 + ` 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 diff --git a/addons/mail/views/activity.rng b/odoo/addons/base/rng/activity_view.rng similarity index 51% rename from addons/mail/views/activity.rng rename to odoo/addons/base/rng/activity_view.rng index 389fbd8f602..8109bb0e444 100644 --- a/addons/mail/views/activity.rng +++ b/odoo/addons/base/rng/activity_view.rng @@ -3,13 +3,26 @@ xmlns:a="http://relaxng.org/ns/annotation/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"> - - - + + + + + + + + + + + + + + + + diff --git a/odoo/models.py b/odoo/models.py index 8ebd58f4b1d..365e4d1a332 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -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. diff --git a/odoo/tools/view_validation.py b/odoo/tools/view_validation.py index bc8bcfc52be..bde57f96a66 100644 --- a/odoo/tools/view_validation.py +++ b/odoo/tools/view_validation.py @@ -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
,
and