[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:
committed by
Martin Geubelle
parent
b29d4da037
commit
b2de618f9f
@@ -3,4 +3,3 @@
|
||||
from . import models
|
||||
from . import wizard
|
||||
from . import controllers
|
||||
from . import validation
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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>
|
||||
@@ -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.
|
||||
|
||||
@@ -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-")'
|
||||
|
||||
Reference in New Issue
Block a user