summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiral Bhavsar <hbh@odoo.com>2019-02-04 06:18:08 +0000
committerMartin Geubelle <mge@odoo.com>2019-05-03 10:19:40 +0000
commitb2de618f9ff9969ff0644f3be10752f23d9738dc (patch)
treea7f54bb16273f3b626a96d363e0f54bf19bb4ba3
parentb29d4da037fbf73717cccdfcdf76d96e5b1b009b (diff)
[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
-rw-r--r--addons/mail/__init__.py1
-rw-r--r--addons/mail/models/__init__.py1
-rw-r--r--addons/mail/models/mail_activity.py5
-rw-r--r--addons/mail/models/models.py19
-rw-r--r--addons/mail/static/src/js/views/activity/activity_controller.js49
-rw-r--r--addons/mail/static/src/js/views/activity/activity_model.js48
-rw-r--r--addons/mail/static/src/js/views/activity/activity_record.js62
-rw-r--r--addons/mail/static/src/js/views/activity/activity_renderer.js69
-rw-r--r--addons/mail/static/src/js/views/activity/activity_view.js19
-rw-r--r--addons/mail/static/src/scss/activity_view.scss58
-rw-r--r--addons/mail/validation.py30
-rw-r--r--addons/mail/views/mail_templates.xml1
-rw-r--r--doc/reference/views.rst47
-rw-r--r--odoo/addons/base/rng/activity_view.rng (renamed from addons/mail/views/activity.rng)19
-rw-r--r--odoo/models.py9
-rw-r--r--odoo/tools/view_validation.py30
16 files changed, 307 insertions, 160 deletions
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',
}),
//--------------------------------------------------------------------------
- // Handlers
+ // Public
//--------------------------------------------------------------------------
/**
- * @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]
+ * Overridden to remove the pager as it makes no sense in this view.
+ *
+ * @override
*/
- _onReload: function (event) {
- event.stopPropagation();
- var self = this;
- this.model.reload().then(self.reload());
+ renderPager: function () {
+ return Promise.resolve();
},
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
/**
* @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,19 +2,34 @@ 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
//--------------------------------------------------------------------------
@@ -23,12 +38,19 @@ var ActivityRenderer = AbstractRenderer.extend({
* @override
* @private
*/
+ _getRecord: function (recordId) {
+ return _.findWhere(this.state.data, { res_id: recordId });
+ },
+ /**
+ * @override
+ * @private
+ */
_render: function () {
this.$el
.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;
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 @@
<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 -->
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
-
- <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
diff --git a/addons/mail/views/activity.rng b/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">
- <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>
diff --git a/odoo/models.py b/odoo/models.py
index 8ebd58f4b1d..365e4d1a332 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -1269,15 +1269,6 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
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 <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-")'