[MERGE] *: batch and cache views RPCs + refactoring

- RPCs to load fields_views, fields and filters are batched for optimization
  purposes.
- Those data are now stored in a JS cache.
- Views API has been refactored.
This commit is contained in:
Aaron Bohy
2016-03-18 10:45:19 +01:00
35 changed files with 1199 additions and 1174 deletions
@@ -493,21 +493,20 @@ var abstractReconciliationLine = Widget.extend({
// field_manager
var dataset = new data.DataSet(this, "account.account", self.context);
dataset.ids = [];
dataset.arch = {
var fields_view = {};
fields_view.arch = {
attrs: { string: "Stéphanie de Monaco", version: "7.0", class: "oe_form_container o_form_container" },
children: [],
tag: "form"
};
self.field_manager = new FormView (
this, dataset, false, {
this, dataset, fields_view, {
initial_mode: 'edit',
disable_autofocus: false,
$buttons: $(),
$pager: $()
});
self.field_manager.load_form(dataset);
self.field_manager.appendTo(document.createDocumentFragment()); // starts the FormView
// fields default properties
var Default_field = function() {
+2 -1
View File
@@ -9,7 +9,8 @@ var _t = core._t;
Sidebar.include({
init : function(){
this._super.apply(this, arguments);
if (this.getParent().view_type == "form"){
var view = this.getParent();
if (view.fields_view && view.fields_view.type === "form") {
this.sections.splice(1, 0, { 'name' : 'files', 'label' : _t('Attachment(s)'), });
this.items.files = [];
}
@@ -27,8 +27,8 @@ FormView.include({
});
FavoriteMenu.include({
prepare_dropdown_menu: function (filters) {
this._super(filters);
start: function () {
this._super();
var am = this.findAncestor(function(a) {
return a instanceof ActionManager;
});
+11 -4
View File
@@ -10,6 +10,7 @@ var config = require('web.config');
var ControlPanelMixin = require('web.ControlPanelMixin');
var core = require('web.core');
var data = require('web.data');
var data_manager = require('web.data_manager');
var Dialog = require('web.Dialog');
var framework = require('web.framework');
var Model = require('web.Model');
@@ -160,6 +161,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
init: function(parent, action, options) {
this._super.apply(this, arguments);
this.action_manager = parent;
this.dataset = new data.DataSetSearch(this, 'mail.message');
this.domain = [];
this.action = action;
this.options = options || {};
@@ -170,7 +172,14 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
},
willStart: function () {
return chat_manager.is_ready;
var self = this;
var view_id = this.action && this.action.search_view_id && this.action.search_view_id[0];
var def = data_manager
.load_fields_view(this.dataset, view_id, 'search', false)
.then(function (fields_view) {
self.fields_view = fields_view;
});
return $.when(this._super(), chat_manager.is_ready, def);
},
start: function() {
@@ -182,8 +191,6 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
action: this.action,
disable_groupby: true,
};
var dataset = new data.DataSetSearch(this, 'mail.message');
var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false;
var default_channel_id = this.options.active_id ||
this.action.context.active_id ||
this.action.params.default_active_id ||
@@ -191,7 +198,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
var default_channel = chat_manager.get_channel(default_channel_id) ||
chat_manager.get_channel('channel_inbox');
this.searchview = new SearchView(this, dataset, view_id, {}, options);
this.searchview = new SearchView(this, this.dataset, this.fields_view, options);
this.searchview.on('search_data', this, this.on_search);
this.basic_composer = new composer.BasicComposer(this, {mention_partners_restricted: true});
@@ -15,7 +15,6 @@ var _lt = core._lt;
var SalesTeamDashboardView = KanbanView.extend({
display_name: _lt('Dashboard'),
icon: 'fa-dashboard',
view_type: "sales_team_dashboard",
searchview_hidden: true,
events: {
'click .o_dashboard_action': 'on_dashboard_action_clicked',
+1 -1
View File
@@ -1103,7 +1103,7 @@ class Binary(http.Controller):
class Action(http.Controller):
@http.route('/web/action/load', type='json', auth="user")
def load(self, action_id, do_not_eval=False, additional_context=None):
def load(self, action_id, additional_context=None):
Actions = request.session.model('ir.actions.actions')
value = False
try:
+2 -1
View File
@@ -5,6 +5,7 @@ var ControlPanel = require('web.ControlPanel');
var core = require('web.core');
var crash_manager = require('web.crash_manager');
var data = require('web.data');
var data_manager = require('web.data_manager');
var Dialog = require('web.Dialog');
var framework = require('web.framework');
var pyeval = require('web.pyeval');
@@ -564,7 +565,7 @@ var ActionManager = Widget.extend({
active_ids : options.additional_context.active_ids,
active_model : options.additional_context.active_model
};
return self.rpc("/web/action/load", { action_id: action, additional_context : additional_context }).then(function(result) {
return data_manager.load_action(action, additional_context).then(function(result) {
return self.do_action(result, options);
});
}
+3 -2
View File
@@ -186,8 +186,7 @@ function start_modules (modules) {
// Monkey-patching of the ListView for backward compatibiliy of the colors and
// fonts row's attributes, as they are deprecated in 9.0.
ListView.include({
load_list: function(data) {
this._super(data);
willStart: function() {
if (this.fields_view.arch.attrs.colors) {
this.colors = _(this.fields_view.arch.attrs.colors.split(';')).chain()
.compact()
@@ -208,6 +207,8 @@ ListView.include({
return [font, py.parse(py.tokenize(expr)), expr];
}).value();
}
return this._super(data);
},
/**
* Returns the style for the provided record in the current view (from the
@@ -0,0 +1,226 @@
odoo.define('web.DataManager', function (require) {
"use strict";
var core = require('web.core');
var Model = require('web.DataModel');
var session = require('web.session');
var utils = require('web.utils');
return core.Class.extend({
init: function () {
this.Filters = new Model('ir.filters');
this._cache = {
actions: {},
fields_views: {},
fields: {},
filters: {},
views: {},
};
},
/**
* Loads an action from its id or xmlid.
*
* @param {int|string} [action_id] the action id or xmlid
* @param {Object} [additional_context] used to load the action
* @return {Deferred} resolved with the action whose id or xmlid is action_id
*/
load_action: function (action_id, additional_context) {
var self = this;
var key = this._gen_key(action_id, additional_context || {});
if (!this._cache.actions[key]) {
this._cache.actions[key] = session.rpc("/web/action/load", {
action_id: action_id,
additional_context : additional_context,
}).then(function (action) {
self._cache.actions[key] = action.no_cache ? null : self._cache.actions[key];
return action;
});
}
return this._cache.actions[key];
},
/**
* Loads various information concerning views: fields_view for each view,
* and optionally filters and fields.
*
* @param {Object} [dataset] the dataset for which the views are loaded
* @param {Array} [views_descr] array of [view_id, view_type]
* @param {Object} [options] dictionnary of various options:
* - options.load_filters: whether or not to load the filters,
* - options.action_id: the action_id (required to load filters),
* - options.load_fields: whether or not to load the fields,
* - options.toolbar: whether or not a toolbar will be displayed,
* @return {Deferred} resolved with the requested views information
*/
load_views: function (dataset, views_descr, options) {
var self = this;
var model = dataset.model;
var context = dataset.get_context();
var key = this._gen_key(model, views_descr, options || {}, context);
if (!this._cache.views[key]) {
// Don't load fields or filters if already in cache
options.load_fields = options.load_fields && !this._cache.fields[model];
var filters_key;
if (options.load_filters) {
filters_key = this._gen_key(model, options.action_id);
options.load_filters = !this._cache.filters[filters_key];
}
this._cache.views[key] = dataset.call('load_views', {
views: views_descr,
options: options,
context: context,
}).then(function (result) {
// Postprocess fields_views and insert them into the fields_views cache
result.fields_views = _.mapObject(result.fields_views, self._postprocess_fvg.bind(self)); // FIXME: lazy postprocess?
_.each(views_descr, function (view_descr) {
var toolbar = options.toolbar && view_descr[1] !== 'search';
var fv_key = self._gen_key(model, view_descr[0], view_descr[1], toolbar, context);
self._cache.fields_views[fv_key] = $.when(result.fields_views[view_descr[1]]);
});
// Insert filters, if any, into the filters cache
if (result.filters) {
self._cache.filters[filters_key] = $.when(result.filters);
}
// Insert fields, if any, into the fields cache
if (result.fields) {
self._cache.fields[model] = $.when(result.fields);
}
return result.fields_views;
});
}
return this._cache.views[key];
},
/**
* Loads the fields_view of a given model, view_type and optional view_id.
*
* @param {Object} [dataset] the dataset for which the fields_view is loaded
* @param {int} [view_id] the id of the view (optional)
* @param {string} [view_type] the type of the view
* @param {Boolean} [toolbar] whether or not a toolbar will be displayed
* @return {Deferred} resolved with the requested fields_view
*/
load_fields_view: function (dataset, view_id, view_type, toolbar) {
var key = this._gen_key(dataset.model, view_id, view_type, toolbar, dataset.get_context());
if (!this._cache.fields_views[key]) {
this._cache.fields_views[key] = dataset.call('fields_view_get', {
view_id: view_id,
view_type: view_type,
toolbar: toolbar,
context: dataset.get_context(),
}).then(this._postprocess_fvg.bind(this));
}
return this._cache.fields_views[key];
},
/**
* Loads the fields of a given model.
*
* @param {Object} [dataset] the dataset for which the fields are loaded
* @return {Deferred} resolved with the requested fields
*/
load_fields: function (dataset) {
if (!this._cache.fields[dataset.model]) {
this._cache.fields[dataset.model] = dataset.call('fields_get', {
context: dataset.get_context(),
});
}
return this._cache.fields[dataset.model];
},
/**
* Loads the filters of a given model and optional action id.
*
* @param {Object} [dataset] the dataset for which the filters are loaded
* @param {int} [action_id] the id of the action (optional)
* @return {Deferred} resolved with the requested filters
*/
load_filters: function (dataset, action_id) {
var key = this._gen_key(dataset.model, action_id);
if (!this._cache.filters[key]) {
this._cache.filters[key] = this.Filters.call('get_filters', [dataset.model, action_id], {
context: dataset.get_context(),
});
}
return this._cache.filters[key];
},
/**
* Calls 'create_or_replace' on 'ir_filters'.
*
* @param {Object} [filter] the filter description
* @return {Deferred} resolved with the id of the created or replaced filter
*/
create_filter: function (filter) {
var self = this;
return this.Filters
.call('create_or_replace', [filter])
.then(function (filter_id) {
var key = [
filter.model_id,
filter.action_id || false,
].join(',');
self._cache.filters[key] = null; // invalidate cache
return filter_id;
});
},
/**
* Calls 'unlink' on 'ir_filters'.
*
* @param {Object} [filter] the description of the filter to remove
* @return {Deferred}
*/
delete_filter: function (filter) {
var self = this;
return this.Filters
.call('unlink', [filter.id])
.then(function () {
self._cache.filters = {}; // invalidate cache
});
},
/**
* Private function that postprocesses fields_view (mainly parses the arch attribute)
*/
_postprocess_fvg: function (fields_view) {
var self = this;
var doc = $.parseXML(fields_view.arch).documentElement;
fields_view.arch = utils.xml_to_json(doc, (doc.nodeName.toLowerCase() !== 'kanban'));
if ('id' in fields_view.fields) {
// Special case for id's
var id_field = fields_view.fields.id;
id_field.original_type = id_field.type;
id_field.type = 'id';
}
_.each(fields_view.fields, function(field) {
_.each(field.views || {}, function(view) {
self._postprocess_fvg(view);
});
});
return fields_view;
},
/**
* Private function that generates a cache key from its arguments
*/
_gen_key: function () {
return _.map(Array.prototype.slice.call(arguments), function (arg) {
if (!arg) {
return false;
}
return _.isObject(arg) ? JSON.stringify(arg) : arg;
}).join(',');
},
});
});
@@ -3,7 +3,6 @@ odoo.define('web.Model', function (require) {
var Class = require('web.Class');
var session = require('web.session');
var utils = require('web.utils');
var Model = Class.extend({
/**
@@ -70,41 +69,6 @@ var Model = Class.extend({
signal: signal
});
},
/**
* Performs a fields_view_get and apply postprocessing.
* return a {$.Deferred} resolved with the fvg
*
* @param {Object} args
* @param {String|Object} args.model instance.web.Model instance or string repr of the model
* @param {Object} [args.context] context if args.model is a string
* @param {Number} [args.view_id] id of the view to be loaded, default view if null
* @param {String} [args.view_type] type of view to be loaded if view_id is null
* @param {Boolean} [args.toolbar=false] get the toolbar definition
*/
fields_view_get: function (options) {
return this.call('fields_view_get', {
view_id: options.view_id,
view_type: options.view_type,
context: options.context,
toolbar: options.toolbar || false
}).then(function postprocess(fvg) {
var doc = $.parseXML(fvg.arch).documentElement;
fvg.arch = utils.xml_to_json(doc, (doc.nodeName.toLowerCase() !== 'kanban'));
if ('id' in fvg.fields) {
// Special case for id's
var id_field = fvg.fields.id;
id_field.original_type = id_field.type;
id_field.type = 'id';
}
_.each(fvg.fields, function(field) {
_.each(field.views || {}, function(view) {
postprocess(view);
});
});
return fvg;
});
}
});
return Model;
+28 -86
View File
@@ -3,6 +3,7 @@ odoo.define('web.View', function (require) {
var core = require('web.core');
var data = require('web.data');
var data_manager = require('web.data_manager');
var pyeval = require('web.pyeval');
var Widget = require('web.Widget');
@@ -14,10 +15,11 @@ var View = Widget.extend({
this.do_action(action_data);
}
},
defaults: {
action: {},
},
// name displayed in view switchers
display_name: '',
// define a view type for each view to allow automatic call to fields_view_get.
view_type: undefined,
// used by views that need a searchview.
searchable: true,
// used by views that need a searchview but don't want it to be displayed.
@@ -27,57 +29,28 @@ var View = Widget.extend({
multi_record: true,
// icon is the font-awesome icon to display in the view switcher
icon: 'fa-question',
init: function(parent, dataset, view_id, options) {
// whether or not the view requires the model's fields to render itself
require_fields: false,
init: function(parent, dataset, fields_view, options) {
this._super(parent);
this.ViewManager = parent;
this.dataset = dataset;
this.view_id = view_id;
this.set_default_options(options);
this.model = dataset.model;
this.fields_view = fields_view;
this.options = _.defaults({}, options, this.defaults);
},
start: function () {
return this.load_view();
},
load_view: function() {
var self = this;
var view_loaded_def;
if (this.embedded_view) {
view_loaded_def = $.Deferred();
$.async_when().done(function() {
view_loaded_def.resolve(self.embedded_view);
});
} else {
if (! this.view_type)
console.warn("view_type is not defined", this);
view_loaded_def = this.dataset._model.fields_view_get({
"view_id": this.view_id,
"view_type": this.view_type,
"toolbar": !!this.options.sidebar,
"context": this.dataset.get_context(),
});
}
return this.alive(view_loaded_def).then(function(r) {
self.fields_view = r;
// add css classes that reflect the (absence of) access rights
self.$el.addClass('oe_view')
.toggleClass('oe_cannot_create', !self.is_action_enabled('create'))
.toggleClass('oe_cannot_edit', !self.is_action_enabled('edit'))
.toggleClass('oe_cannot_delete', !self.is_action_enabled('delete'));
return $.when(self.view_loading(r)).then(function() {
self.trigger('view_loaded', r);
});
});
},
view_loading: function(r) {
},
set_default_options: function(options) {
this.options = options || {};
_.defaults(this.options, {
// All possible views options should be defaulted here
$sidebar: null,
sidebar_id: null,
action: null,
action_views_ids: {}
});
/**
* Triggers event 'view_loaded'.
* Views extending start() must call this.super() once they are ready.
* @return {Deferred}
*/
start: function() {
// add css classes that reflect the (absence of) access rights
this.$el.addClass('oe_view')
.toggleClass('oe_cannot_create', !this.is_action_enabled('create'))
.toggleClass('oe_cannot_edit', !this.is_action_enabled('edit'))
.toggleClass('oe_cannot_delete', !this.is_action_enabled('delete'));
return this._super().then(this.trigger.bind(this, 'view_loaded'));
},
/**
* Fetches and executes the action identified by ``action_data``.
@@ -87,7 +60,7 @@ var View = Widget.extend({
* @param {String} [action_data.special=null] special action handlers (currently: only ``'cancel'``)
* @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'``
* @param {Object} [action_data.context=null] additional action context, to add to the current context
* @param {instance.web.DataSet} dataset a dataset object used to communicate with the server
* @param {DataSet} dataset a dataset object used to communicate with the server
* @param {Object} [record_id] the identifier of the object on which the action is to be applied
* @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action)
*/
@@ -150,56 +123,25 @@ var View = Widget.extend({
core.bus.trigger('do_reload_needaction');
});
} else if (action_data.type=="action") {
return this.rpc('/web/action/load', {
action_id: action_data.name,
context: _.extend(pyeval.eval('context', context), {'active_model': dataset.model, 'active_ids': dataset.ids, 'active_id': record_id}),
do_not_eval: true
}).then(handler);
return data_manager.load_action(action_data.name, _.extend(pyeval.eval('context', context), {
active_model: dataset.model,
active_ids: dataset.ids,
active_id: record_id
})).then(handler);
} else {
return dataset.exec_workflow(record_id, action_data.name).then(handler);
}
},
/**
* Directly set a view to use instead of calling fields_view_get. This method must
* be called before start(). When an embedded view is set, underlying implementations
* of instance.web.View must use the provided view instead of any other one.
*
* @param embedded_view A view.
*/
set_embedded_view: function(embedded_view) {
this.embedded_view = embedded_view;
},
do_show: function () {
this._super();
core.bus.trigger('view_shown', this);
},
is_active: function () {
return this.ViewManager.active_view.controller === this;
},
/**
* Wraps fn to only call it if the current view is the active one. If the
* current view is not active, doesn't call fn.
*
* fn can not return anything, as a non-call to fn can't return anything
* either
*
* @param {Function} fn function to wrap in the active guard
*/
guard_active: function (fn) {
var self = this;
return function () {
if (self.is_active()) {
fn.apply(self, arguments);
}
};
},
do_push_state: function(state) {
if (this.getParent() && this.getParent().do_push_state) {
this.getParent().do_push_state(state);
}
},
do_load_state: function (state, warm) {
},
/**
* This function should render the buttons of the view, set this.$buttons to
@@ -0,0 +1,10 @@
odoo.define('web.data_manager', function (require) {
"use strict";
var DataManager = require('web.DataManager');
var data_manager = new DataManager();
return data_manager;
});
+124 -100
View File
@@ -3,7 +3,7 @@ odoo.define('web.ViewManager', function (require) {
var ControlPanelMixin = require('web.ControlPanelMixin');
var core = require('web.core');
var data = require('web.data');
var data_manager = require('web.data_manager');
var framework = require('web.framework');
var Model = require('web.DataModel');
var pyeval = require('web.pyeval');
@@ -17,14 +17,14 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
template: "ViewManager",
/**
* @param {Object} [dataset]
* @param {Array} [views] List of [view_id, view_type]
* @param {Array} [views] List of [view_id, view_type[, fields_view]]
* @param {Object} [flags] various boolean describing UI state
*/
init: function(parent, dataset, views, flags, options) {
var self = this;
this._super(parent);
this.action = options && options.action;
this.action = options && options.action || {};
this.action_manager = options && options.action_manager;
this.flags = flags || {};
this.dataset = dataset;
@@ -33,7 +33,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
this.view_stack = []; // used for breadcrumbs
this.active_view = null;
this.registry = core.view_registry;
this.title = this.action && this.action.name;
this.title = this.action.name;
this.is_in_DOM = false; // used to know if the view manager is attached in the DOM
_.each(views, function (view) {
var view_type = view[1] || view.view_type;
@@ -44,45 +44,42 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
}
var view_label = View.prototype.display_name;
var view_descr = {
controller: null,
options: view.options || {},
view_id: view[0] || view.view_id,
type: view_type,
label: view_label,
embedded_view: view.embedded_view,
title: self.title,
button_label: _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}),
multi_record: View.prototype.multi_record,
accesskey: View.prototype.accesskey,
icon: View.prototype.icon,
};
accesskey: View.prototype.accesskey,
button_label: _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}),
controller: null,
fields_view: view[2] || view.fields_view,
embedded_view: view.embedded_view,
icon: View.prototype.icon,
label: view_label,
multi_record: View.prototype.multi_record,
options: view.options || {},
require_fields: View.prototype.require_fields,
title: self.title,
type: view_type,
view_id: view[0] || view.view_id,
};
self.view_order.push(view_descr);
self.views[view_type] = view_descr;
});
this.first_view = this.views[options && options.view_type]; // view to open first
// Listen to event 'switch_view' indicating that the VM must now display view wiew_type
this.on('switch_view', this, function(view_type) {
if (view_type === 'form' && this.active_view && this.active_view.type === 'form') {
this._display_view(view_type);
} else {
this.switch_mode(view_type);
}
});
this.default_view = this.get_default_view();
},
willStart: function () {
var views_def;
var first_view_to_display = this.first_view || this.default_view;
if (!first_view_to_display.fields_view || (this.flags.search_view && !this.search_fields_view)) {
views_def = this.load_views(first_view_to_display.require_fields);
}
return $.when(this._super(), views_def);
},
/**
* @returns {jQuery.Deferred} initial view loading promise
* @return {Deferred} initial view and search view (if any) loading promise
*/
start: function() {
var self = this;
this._super();
var views_ids = {};
_.each(this.views, function (view) {
views_ids[view.type] = view.view_id;
view.options = _.extend({
action : self.action,
action_views_ids : views_ids,
action: self.action,
}, self.flags, self.flags[view.type], view.options);
view.$container = self.$(".oe-view-manager-view-" + view.type);
});
@@ -99,16 +96,49 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
// If a non multi-record first_view is given, switch to it but first push the default_view
// to the view_stack to complete the breadcrumbs
var default_view = this.get_default_view();
if (this.first_view && !this.first_view.multi_record && default_view.multi_record) {
default_view.controller = this.create_view(default_view);
this.view_stack.push(default_view);
if (this.first_view && !this.first_view.multi_record && this.default_view.multi_record) {
this.default_view.controller = this.create_view(this.default_view);
this.view_stack.push(this.default_view);
}
var view_to_load = this.first_view || default_view;
var view_to_load = this.first_view || this.default_view;
var options = this.flags[view_to_load] && this.flags[view_to_load].options;
var main_view_loaded = this.switch_mode(view_to_load.type, options);
return $.when(main_view_loaded, this.search_view_loaded);
return $.when(this._super(), main_view_loaded, this.search_view_loaded);
},
/**
* Loads all missing field_views of views in this.views and the search view.
*
* @param {Boolean} [load_fields] whether or not to load the fields as well
* @return {Deferred}
*/
load_views: function (load_fields) {
var self = this;
var views = [];
_.each(this.views, function (view) {
if (!view.fields_view) {
views.push([view.view_id, view.type]);
}
});
var options = {
action_id: this.action.id,
load_fields: load_fields,
toolbar: this.flags.sidebar,
};
if (this.flags.search_view && !this.search_fields_view) {
options.load_filters = true;
var searchview_id = this.action.search_view_id && this.action.search_view_id[0];
views.push([searchview_id || false, 'search']);
}
return data_manager.load_views(this.dataset, views, options).then(function (fields_views) {
_.each(fields_views, function (fields_view, view_type) {
if (view_type === 'search') {
self.search_fields_view = fields_view;
} else {
self.views[view_type].fields_view = fields_view;
}
});
});
},
/**
* Returns the default view with the following fallbacks:
@@ -131,51 +161,60 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
this.currently_switching = true; // prevent overlapping switches
}
if (view.multi_record) {
this.view_stack = [];
} else if (this.view_stack.length > 0 && !(_.last(this.view_stack).multi_record)) {
// Replace the last view by the new one if both are mono_record
this.view_stack.pop();
}
this.view_stack.push(view);
this.active_view = view;
if (!view.loaded) {
if (!view.controller) {
view.controller = this.create_view(view, view_options);
}
view.$fragment = $('<div>');
view.loaded = view.controller.appendTo(view.$fragment).done(function () {
// Remove the unnecessary outer div
view.$fragment = view.$fragment.contents();
self.trigger("controller_inited", view.type, view.controller);
});
// Ensure that the fields_view has been loaded
var views_def;
if (!view.fields_view) {
views_def = this.load_views(view.require_fields);
}
// Call do_search on the searchview to compute domains, contexts and groupbys
if (this.search_view_loaded &&
this.flags.auto_search &&
view.controller.searchable !== false) {
this.active_search = $.Deferred();
$.when(this.search_view_loaded, view.loaded).done(function() {
self.searchview.do_search();
});
}
return $.when(view.loaded, this.active_search)
.then(function() {
return self._display_view(view_options, old_view).then(function() {
self.trigger('switch_mode', view_type, view_options);
});
}).fail(function(e) {
if (!(e && e.code === 200 && e.data.exception_type)) {
self.do_warn(_t("Error"), view.controller.display_name + _t(" view couldn't be loaded"));
}
// Restore internal state
self.active_view = old_view;
return $.when(views_def).then(function () {
if (view.multi_record) {
self.view_stack = [];
} else if (self.view_stack.length > 0 && !(_.last(self.view_stack).multi_record)) {
// Replace the last view by the new one if both are mono_record
self.view_stack.pop();
}).always(function () {
self.currently_switching = false;
}
self.view_stack.push(view);
self.active_view = view;
if (!view.loaded) {
if (!view.controller) {
view.controller = self.create_view(view, view_options);
}
view.$fragment = $('<div>');
view.loaded = view.controller.appendTo(view.$fragment).done(function () {
// Remove the unnecessary outer div
view.$fragment = view.$fragment.contents();
self.trigger("controller_inited", view.type, view.controller);
});
}
// Call do_search on the searchview to compute domains, contexts and groupbys
if (self.search_view_loaded &&
self.flags.auto_search &&
view.controller.searchable !== false) {
self.active_search = $.Deferred();
$.when(self.search_view_loaded, view.loaded).done(function() {
self.searchview.do_search();
});
}
return $.when(view.loaded, self.active_search)
.then(function() {
return self._display_view(view_options, old_view).then(function() {
self.trigger('switch_mode', view_type, view_options);
});
}).fail(function(e) {
if (!(e && e.code === 200 && e.data.exception_type)) {
self.do_warn(_t("Error"), view.controller.display_name + _t(" view couldn't be loaded"));
}
// Restore internal state
self.active_view = old_view;
self.view_stack.pop();
});
}).always(function () {
self.currently_switching = false;
});
},
_display_view: function (view_options, old_view) {
@@ -218,17 +257,12 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
var self = this;
var View = this.registry.get(view.type);
var options = _.clone(view.options);
if (view.type === "form" && ((this.action && (this.action.target === 'new' || this.action.target === 'inline')) ||
if (view.type === "form" && ((this.action.target === 'new' || this.action.target === 'inline') ||
(view_options && view_options.mode === 'edit'))) {
options.initial_mode = options.initial_mode || 'edit';
}
var controller = new View(this, this.dataset, view.view_id, options);
if (view.embedded_view) {
controller.set_embedded_view(view.embedded_view);
}
var controller = new View(this, this.dataset, view.fields_view, options);
controller.on('switch_mode', this, this.switch_mode.bind(this));
controller.on('history_back', this, function () {
@@ -306,12 +340,6 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
}
return this.active_view.control_elements;
},
/**
* @returns {Number|Boolean} the view id of the given type, false if not found
*/
get_view_id: function(view_type) {
return this.views[view_type] && this.views[view_type].view_id || false;
},
/**
* Sets up the current viewmanager's search view.
* Sets $searchview and $searchview_buttons in control_elements to send to the ControlPanel
@@ -325,28 +353,24 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
this.searchview.destroy();
}
var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false;
var search_defaults = {};
var context = this.action ? this.action.context : [];
var context = this.action.context || [];
_.each(context, function (value, key) {
var match = /^search_default_(.*)$/.exec(key);
if (match) {
search_defaults[match[1]] = value;
}
});
var options = {
hidden: this.flags.search_view === false,
disable_custom_filters: this.flags.search_disable_custom_filters,
$buttons: $("<div>"),
action: this.action,
search_defaults: search_defaults,
};
// Instantiate the SearchView, but do not append it nor its buttons to the DOM as this will
// be done later, simultaneously to all other ControlPanel elements
this.searchview = new SearchView(this, this.dataset, view_id, search_defaults, options);
this.searchview = new SearchView(this, this.dataset, this.search_fields_view, options);
this.searchview.on('search_data', this, this.search.bind(this));
return $.when(this.searchview.appendTo($("<div>"))).done(function() {
+80 -63
View File
@@ -3,6 +3,7 @@ odoo.define('web.form_common', function (require) {
var core = require('web.core');
var data = require('web.data');
var data_manager = require('web.data_manager');
var Dialog = require('web.Dialog');
var ListView = require('web.ListView');
var pyeval = require('web.pyeval');
@@ -769,7 +770,7 @@ var AbstractField = FormWidget.extend(FieldInterface, {
on_translate: function() {
var self = this;
var trans = new data.DataSet(this, 'ir.translation');
return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
return trans.call_button('translate_fields', [this.view.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
self.do_action(r);
});
},
@@ -793,7 +794,6 @@ var ViewDialog = Dialog.extend({ // FIXME should use ViewManager
* options:
* -readonly: only applicable when not in creation mode, default to false
* - alternative_form_view
* - view_id
* - write_function
* - read_function
* - create_function
@@ -814,7 +814,6 @@ var ViewDialog = Dialog.extend({ // FIXME should use ViewManager
this.context = options.context || {};
this.options = _.extend(this.options || {}, options || {});
this.on('closed', this, this.select);
this.on_selected = options.on_selected || (function() {});
},
@@ -838,6 +837,8 @@ var ViewDialog = Dialog.extend({ // FIXME should use ViewManager
};
this.dataset.parent_view = this.options.parent_view;
this.dataset.child_name = this.options.child_name;
this.on('closed', this, this.select);
},
select: function() {
@@ -898,33 +899,36 @@ var FormViewDialog = ViewDialog.extend({
open: function() {
var self = this;
self._super();
self.init_dataset();
var _super = this._super.bind(this);
this.init_dataset();
if (self.res_id) {
self.dataset.ids = [self.res_id];
self.dataset.index = 0;
if (this.res_id) {
this.dataset.ids = [this.res_id];
this.dataset.index = 0;
} else {
self.dataset.index = null;
this.dataset.index = null;
}
var options = _.clone(self.options.form_view_options) || {};
if (self.res_id !== null) {
options.initial_mode = self.options.readonly ? "view" : "edit";
var options = _.clone(this.options.form_view_options) || {};
if (this.res_id !== null) {
options.initial_mode = this.options.readonly ? "view" : "edit";
}
_.extend(options, {
$buttons: self.$buttons,
$buttons: this.$buttons,
});
var FormView = core.view_registry.get('form');
self.view_form = new FormView(self, self.dataset, self.options.view_id || false, options);
if (self.options.alternative_form_view) {
self.view_form.set_embedded_view(self.options.alternative_form_view);
var fields_view_def;
if (this.options.alternative_form_view) {
fields_view_def = $.when(this.options.alternative_form_view);
} else {
fields_view_def = data_manager.load_fields_view(this.dataset, this.options.view_id, 'form', false);
}
self.do_hide();
self.view_form.appendTo(self.$el);
self.view_form.on("form_view_loaded", self, function() {
self.view_form.do_show().then(function() {
self.do_show();
fields_view_def.then(function (fields_view) {
self.view_form = new FormView(self, self.dataset, fields_view, options);
var fragment = document.createDocumentFragment();
self.view_form.appendTo(fragment).then(function () {
self.view_form.do_show().then(function() {
_super().$el.append(fragment);
});
});
});
@@ -965,49 +969,60 @@ var SelectCreateDialog = ViewDialog.extend({
},
open: function() {
this._super();
this.init_dataset();
if (this.options.initial_view == "search") {
var context = pyeval.sync_eval_domains_and_contexts({
domains: [],
contexts: [this.context]
}).context;
var search_defaults = {};
_.each(context, function (value_, key) {
var match = /^search_default_(.*)$/.exec(key);
if (match) {
search_defaults[match[1]] = value_;
}
});
this.setup(search_defaults);
} else { // "form"
this.create_edit_record();
if(this.options.initial_view !== "search") {
return this.create_edit_record();
}
var _super = this._super.bind(this);
this.init_dataset();
var context = pyeval.sync_eval_domains_and_contexts({
domains: [],
contexts: [this.context]
}).context;
var search_defaults = {};
_.each(context, function (value_, key) {
var match = /^search_default_(.*)$/.exec(key);
if (match) {
search_defaults[match[1]] = value_;
}
});
data_manager
.load_views(this.dataset, [[false, 'list'], [false, 'search']], {})
.then(this.setup.bind(this, search_defaults))
.then(function (fragment) {
_super().$el.append(fragment);
});
return this;
},
setup: function(search_defaults) {
setup: function(search_defaults, fields_views) {
var self = this;
if (this.searchview) {
this.searchview.destroy();
}
var $header = $('<div/>').addClass('o_modal_header').appendTo(this.$el);
var fragment = document.createDocumentFragment();
var $header = $('<div/>').addClass('o_modal_header').appendTo(fragment);
var $pager = $('<div/>').addClass('o_pager').appendTo($header);
var $buttons = $('<div/>').addClass('o_search_options').appendTo($header);
this.searchview = new SearchView(this, this.dataset, false, search_defaults, {$buttons: $buttons});
this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
if (self.initial_ids) {
self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
contexts.concat(self.context), groupbys);
self.initial_ids = undefined;
var options = {
$buttons: $('<div/>').addClass('o_search_options').appendTo($header),
search_defaults: search_defaults,
};
this.searchview = new SearchView(this, this.dataset, fields_views.search, options);
this.searchview.on('search_data', this, function(domains, contexts, groupbys) {
if (this.initial_ids) {
this.do_search(domains.concat([[["id", "in", this.initial_ids]], this.domain]),
contexts.concat(this.context), groupbys);
this.initial_ids = undefined;
} else {
self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
this.do_search(domains.concat([this.domain]), contexts.concat(this.context), groupbys);
}
});
this.searchview.prependTo($header).done(function() {
return this.searchview.prependTo($header).then(function() {
self.searchview.toggle_visibility(true);
self.view_list = new SelectCreateListView(self,
self.dataset, false,
self.dataset, fields_views.list,
_.extend({'deletable': false,
'selectable': !self.options.disable_multiple_selection,
'import_enabled': false,
@@ -1019,17 +1034,6 @@ var SelectCreateDialog = ViewDialog.extend({
e.cancel = true;
});
self.view_list.popup = self;
self.view_list.appendTo(self.$el).then(function() {
self.view_list.do_show();
self.view_list.render_pager($pager);
}).then(function() {
if (self.options.initial_facet) {
self.searchview.query.reset([self.options.initial_facet], {
preventSearch: true,
});
}
self.searchview.do_search();
});
var buttons = [
{text: _t("Cancel"), classes: "btn-default o_form_button_cancel", close: true}
@@ -1045,6 +1049,19 @@ var SelectCreateDialog = ViewDialog.extend({
}});
}
self.set_buttons(buttons);
return self.view_list.appendTo(fragment).then(function() {
self.view_list.do_show();
self.view_list.render_pager($pager);
if (self.options.initial_facet) {
self.searchview.query.reset([self.options.initial_facet], {
preventSearch: true,
});
}
self.searchview.do_search();
return fragment;
});
});
},
do_search: function(domains, contexts, groupbys) {
@@ -1060,8 +1077,8 @@ var SelectCreateDialog = ViewDialog.extend({
this.$footer.find(".o_selectcreatepopup_search_select").prop('disabled', this.selected_ids.length <= 0);
},
create_edit_record: function() {
new FormViewDialog(this.getParent(), this.options).open();
this.close();
return new FormViewDialog(this.__parentedParent, this.options).open();
},
});
@@ -12,7 +12,6 @@ var Model = require('web.DataModel');
var session = require('web.session');
var utils = require('web.utils');
var ViewManager = require('web.ViewManager');
var formats = require('web.formats');
var _t = core._t;
var QWeb = core.qweb;
@@ -752,11 +751,9 @@ var FieldX2Many = AbstractManyField.extend({
var view = {
view_id: false,
view_type: view_type === "tree" ? "list" : view_type,
options: {}
fields_view: self.field.views && self.field.views[view_type],
options: {},
};
if (self.field.views && self.field.views[view_type]) {
view.embedded_view = self.field.views[view_type];
}
if(view.view_type === "list") {
_.extend(view.options, {
action_buttons: false, // to avoid 'Save' and 'Discard' buttons to appear in X2M fields
@@ -774,6 +771,7 @@ var FieldX2Many = AbstractManyField.extend({
}
} else if (view.view_type === "kanban") {
_.extend(view.options, {
action_buttons: true,
confirm_on_delete: false,
});
if (self.get("effective_readonly")) {
@@ -824,9 +822,7 @@ var FieldX2Many = AbstractManyField.extend({
});
});
$.async_when().done(function () {
if (!self.isDestroyed()) {
self.viewmanager.appendTo(self.$el);
}
self.alive( self.viewmanager.appendTo(self.$el));
});
return def;
},
@@ -899,7 +895,7 @@ var X2ManyDataSet = data.BufferedDataSet.extend({
var X2ManyViewManager = ViewManager.extend({
init: function(parent, dataset, views, flags, x2many_views) {
// By default, render buttons and pager in X2M fields, but no sidebar
var flags = _.extend({}, flags, {
flags = _.extend({}, flags, {
headless: false,
search_view: false,
action_buttons: true,
@@ -925,7 +921,6 @@ var X2ManyViewManager = ViewManager.extend({
res_model: self.x2m.field.relation,
res_id: id,
context: self.x2m.build_context(),
title: _t("Open: ") + self.x2m.string,
create_function: function(data, options) {
return self.x2m.data_create(data, options);
@@ -952,7 +947,6 @@ var X2ManyViewManager = ViewManager.extend({
var X2ManyListView = ListView.extend({
is_valid: function () {
var self = this;
if (!this.fields_view || !this.editable()){
return true;
}
@@ -966,7 +960,7 @@ var X2ManyListView = ListView.extend({
field.no_rerender = true;
current_values[field.name] = field.get('value');
});
var cached_records = _.filter(this.dataset.cache, function(item){return !_.isEmpty(item.values)});
var cached_records = _.filter(this.dataset.cache, function(item){return !_.isEmpty(item.values);});
var valid = _.every(cached_records, function(record){
_.each(fields, function(field){
var value = record.values[field.name];
@@ -1018,14 +1012,16 @@ var X2ManyList = ListView.List.extend({
.click(function (e) {
e.preventDefault();
e.stopPropagation();
// FIXME: there should also be an API for that one
if (self.view.editor.form.__blur_timeout) {
clearTimeout(self.view.editor.form.__blur_timeout);
self.view.editor.form.__blur_timeout = false;
var def;
if (self.view.editable()) {
// FIXME: there should also be an API for that one
if (self.view.editor.form.__blur_timeout) {
clearTimeout(self.view.editor.form.__blur_timeout);
self.view.editor.form.__blur_timeout = false;
}
def = self.view.save_edition();
}
self.view.save_edition().done(function () {
self.view.do_add_record();
});
$.when(def).done(self.view.do_add_record.bind(self));
}));
var $padding = this.$current.find('tr:not([data-id]):first');
@@ -1040,12 +1036,12 @@ var X2ManyList = ListView.List.extend({
var One2ManyListView = X2ManyListView.extend({
_template: 'One2Many.listview',
init: function (parent, dataset, view_id, options) {
var self = this;
this._super(parent, dataset, view_id, _.extend(options || {}, {
init: function () {
this._super.apply(this, arguments);
this.options = _.extend(this.options, {
GroupsType: One2ManyGroups,
ListType: X2ManyList
}));
});
this.on('edit:after', this, this.proxy('_after_edit'));
this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
@@ -1292,15 +1288,12 @@ var FieldOne2Many = FieldX2Many.extend({
},
});
/**
* @class
* @extends instance.web.ListView
*/
var Many2ManyListView = X2ManyListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
init: function (parent, dataset, view_id, options) {
this._super(parent, dataset, view_id, _.extend(options || {}, {
var Many2ManyListView = X2ManyListView.extend({
init: function () {
this._super.apply(this, arguments);
this.options = _.extend(this.options, {
ListType: X2ManyList,
}));
});
},
do_add_record: function () {
var self = this;
@@ -1322,7 +1315,7 @@ var Many2ManyListView = X2ManyListView.extend(/** @lends instance.web.form.Many2
do_activate_record: function(index, id) {
var self = this;
var pop = new common.FormViewDialog(this, {
res_model: this.dataset.model,
res_model: this.model,
res_id: id,
context: this.x2m.build_context(),
title: _t("Open: ") + this.x2m.string,
+37 -77
View File
@@ -16,45 +16,29 @@ var _lt = core._lt;
var QWeb = core.qweb;
var Class = core.Class;
/** @namespace */
// instance.web.form = instance.web.form || {};
/**
* Properties:
* - actual_mode: always "view", "edit" or "create". Read-only property. Determines
* the mode used by the view.
*/
var FormView = View.extend(common.FieldManagerMixin, {
/**
* Indicates that this view is not searchable, and thus that no search
* view should be displayed (if there is one active).
*/
accesskey: "F",
defaults: _.extend({}, View.prototype.defaults, {
not_interactible_on_create: false,
initial_mode: "view",
disable_autofocus: false,
footer_to_buttons: false,
}),
display_name: _lt('Form'),
icon: 'fa-edit',
multi_record: false,
// Indicates that this view is not searchable, and thus that no search view should be displayed.
searchable: false,
template: "FormView",
display_name: _lt('Form'),
view_type: "form",
multi_record: false,
accesskey: "F",
icon: 'fa-edit',
/**
* @constructs instance.web.FormView
* @extends instance.web.View
*
* @param {instance.web.Session} session the current openerp session
* @param {instance.web.DataSet} dataset the dataset this view will work with
* @param {String} view_id the identifier of the OpenERP view object
*
* @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance
*/
init: function(parent, dataset, view_id, options) {
init: function() {
var self = this;
this._super(parent);
this.ViewManager = parent;
this.set_default_options(options);
this.dataset = dataset;
this.model = dataset.model;
this.view_id = view_id || false;
this.fields_view = {};
this._super.apply(this, arguments);
this.fields = {};
this.fields_order = [];
this.datarecord = {};
@@ -67,12 +51,6 @@ var FormView = View.extend(common.FieldManagerMixin, {
this.widgets_registry = core.form_custom_registry;
this.has_been_loaded = $.Deferred();
this.translatable_fields = [];
_.defaults(this.options, {
"not_interactible_on_create": false,
"initial_mode": "view",
"disable_autofocus": false,
"footer_to_buttons": false,
});
this.is_initialized = $.Deferred();
this.mutating_mutex = new utils.Mutex();
this.save_list = [];
@@ -104,34 +82,13 @@ var FormView = View.extend(common.FieldManagerMixin, {
});
});
},
view_loading: function(r) {
return this.load_form(r);
},
destroy: function() {
_.each(this.get_widgets(), function(w) {
w.off('focused blurred');
w.destroy();
});
if (this.$el) {
this.$el.off('.formBlur');
}
this._super();
},
load_form: function(data) {
start: function() {
var self = this;
if (!data) {
throw new Error(_t("No data provided."));
}
if (this.arch) {
throw "Form view does not support multiple calls to load_form";
}
this.fields_order = [];
this.fields_view = data;
this.rendering_engine.set_fields_registry(this.fields_registry);
this.rendering_engine.set_tags_registry(this.tags_registry);
this.rendering_engine.set_widgets_registry(this.widgets_registry);
this.rendering_engine.set_fields_view(data);
this.rendering_engine.set_fields_view(this.fields_view);
var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
this.rendering_engine.render_to($dest);
@@ -143,7 +100,7 @@ var FormView = View.extend(common.FieldManagerMixin, {
// Add bounce effect on button 'Edit' when click on readonly page view.
this.$el.find(".oe_form_group_row,.oe_form_field,label,h1,.oe_title,.oe_notebook_page, .oe_list_content").on('click', function (e) {
if(self.get("actual_mode") == "view" && self.$buttons && !$(e.target).is('[data-toggle]')) {
if(self.get("actual_mode") === "view" && self.$buttons && !$(e.target).is('[data-toggle]')) {
var $button = self.$buttons.find(".oe_form_button_edit");
$button.openerpBounce();
e.stopPropagation();
@@ -152,14 +109,23 @@ var FormView = View.extend(common.FieldManagerMixin, {
});
//bounce effect on red button when click on statusbar.
this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
if((self.get("actual_mode") == "view")) {
if((self.get("actual_mode") === "view")) {
var $button = self.$el.find(".oe_highlight:not(.o_form_invisible)").css({'float':'left','clear':'none'});
$button.openerpBounce();
e.stopPropagation();
}
});
this.trigger('form_view_loaded', data);
return $.when();
return this._super();
},
destroy: function() {
_.each(this.get_widgets(), function(w) {
w.off('focused blurred');
w.destroy();
});
if (this.$el) {
this.$el.off('.formBlur');
}
this._super();
},
/**
* Render the buttons according to the FormView.buttons template and add listeners on it.
@@ -181,14 +147,10 @@ var FormView = View.extend(common.FieldManagerMixin, {
// Show or hide the buttons according to the view mode
this.toggle_buttons();
this.$buttons.on('click', '.oe_form_button_create',
this.guard_active(this.on_button_create));
this.$buttons.on('click', '.oe_form_button_edit',
this.guard_active(this.on_button_edit));
this.$buttons.on('click', '.oe_form_button_save',
this.guard_active(this.on_button_save));
this.$buttons.on('click', '.oe_form_button_cancel',
this.guard_active(this.on_button_cancel));
this.$buttons.on('click', '.oe_form_button_create', this.on_button_create);
this.$buttons.on('click', '.oe_form_button_edit', this.on_button_edit);
this.$buttons.on('click', '.oe_form_button_save', this.on_button_save);
this.$buttons.on('click', '.oe_form_button_cancel', this.on_button_cancel);
if ($node) {
this.$buttons.appendTo($node);
@@ -311,10 +273,8 @@ var FormView = View.extend(common.FieldManagerMixin, {
}
},
/**
*
* @param {Object} [options]
* @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
* @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
* @param {Boolean} [options.mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
* @param {Boolean} [options.reload=true] whether the form should reload its content on show, or use the currently loaded record
* @return {$.Deferred}
*/
do_show: function (options) {
@@ -521,7 +481,7 @@ var FormView = View.extend(common.FieldManagerMixin, {
// In case of a o2m virtual id, we should pass an empty ids list
ids.push(self.datarecord.id);
}
def = self.alive(new Model(self.dataset.model).call(
def = self.alive(self.dataset.call(
"onchange", [ids, values, trigger_field_name, onchange_specs, context]));
}
this.onchanges_mutex.exec(function(){
@@ -1199,7 +1159,7 @@ var FormView = View.extend(common.FieldManagerMixin, {
all_users = d.$el.find('#formview_default_all').is(':checked');
new data.DataSet(self, 'ir.values').call(
'set_default', [
self.dataset.model,
self.model,
field_to_set,
self.fields[field_to_set].get_value(),
all_users,
+20 -25
View File
@@ -5,8 +5,8 @@ odoo.define('web.GraphView', function (require) {
*---------------------------------------------------------*/
var core = require('web.core');
var data_manager = require('web.data_manager');
var GraphWidget = require('web.GraphWidget');
var Model = require('web.DataModel');
var View = require('web.View');
var _lt = core._lt;
@@ -17,22 +17,31 @@ var GraphView = View.extend({
className: 'oe_graph',
display_name: _lt('Graph'),
icon: 'fa-bar-chart',
view_type: 'graph',
require_fields: true,
init: function(parent, dataset, view_id, options) {
this._super(parent, dataset, view_id, options);
init: function () {
this._super.apply(this, arguments);
this.model = new Model(dataset.model, {group_by_no_leaf: true});
this.measures = [];
this.active_measure = '__count__';
this.initial_groupbys = [];
this.widget = undefined;
},
start: function () {
var load_fields = this.model.call('fields_get', [], {context: this.dataset.get_context()})
.then(this.prepare_fields.bind(this));
return $.when(this._super(), load_fields);
willStart: function () {
var self = this;
var fields_def = data_manager.load_fields(this.dataset).then(this.prepare_fields.bind(this));
this.fields_view.arch.children.forEach(function (field) {
var name = field.attrs.name;
if (field.attrs.interval) {
name += ':' + field.attrs.interval;
}
if (field.attrs.type === 'measure') {
self.active_measure = name;
} else {
self.initial_groupbys.push(name);
}
});
return $.when(this._super(), fields_def);
},
/**
* Render the buttons according to the GraphView.buttons and
@@ -59,20 +68,6 @@ var GraphView = View.extend({
$(li).toggleClass('selected', $(li).data('field') === self.active_measure);
});
},
view_loading: function (fvg) {
var self = this;
fvg.arch.children.forEach(function (field) {
var name = field.attrs.name;
if (field.attrs.interval) {
name += ':' + field.attrs.interval;
}
if (field.attrs.type === 'measure') {
self.active_measure = name;
} else {
self.initial_groupbys.push(name);
}
});
},
do_show: function () {
this.do_push_state({});
return this._super();
@@ -92,7 +87,7 @@ var GraphView = View.extend({
do_search: function (domain, context, group_by) {
if (!this.widget) {
this.initial_groupbys = context.graph_groupbys || this.initial_groupbys;
this.widget = new GraphWidget(this, this.dataset.model, {
this.widget = new GraphWidget(this, this.model, {
measure: context.graph_measure || this.active_measure,
mode: context.graph_mode || this.active_mode,
domain: domain,
+74 -123
View File
@@ -32,33 +32,33 @@ var row_decoration = [
'decoration-warning'
];
var ListView = View.extend( /** @lends instance.web.ListView# */ {
var ListView = View.extend({
_template: 'ListView',
accesskey: 'L',
icon: 'fa-list-ul',
display_name: _lt('List'),
defaults: {
defaults: _.extend({}, View.prototype.defaults, {
// records can be selected one by one
'selectable': true,
selectable: true,
// list rows can be deleted
'deletable': false,
deletable: false,
// whether the column headers should be displayed
'header': true,
header: true,
// display addition button, with that label
'addable': _lt("Create"),
addable: _lt("Create"),
// whether the list view can be sorted, note that once a view has been
// sorted it can not be reordered anymore
'sortable': true,
sortable: true,
// whether the view rows can be reordered (via vertical drag & drop)
'reorderable': true,
'action_buttons': true,
reorderable: true,
action_buttons: true,
//whether the editable property of the view has to be disabled
'disable_editable_mode': false,
},
view_type: 'tree',
disable_editable_mode: false,
}),
events: {
'click thead th.oe_sortable[data-id]': 'sort_by_column'
},
icon: 'fa-list-ul',
/**
* Core class for list-type displays.
*
@@ -69,11 +69,11 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ {
* See constructor parameters and method documentations for information on
* the default behaviors and possible options for the list view.
*
* @constructs instance.web.ListView
* @extends instance.web.View
* @constructs ListView
* @extends View
*
* @param parent parent object
* @param {instance.web.DataSet} dataset the dataset the view should work with
* @param {DataSet} dataset the dataset the view should work with
* @param {String} view_id the listview's identifier, if any
* @param {Object} options A set of options used to configure the view
* @param {Boolean} [options.selectable=true] determines whether view rows are selectable (e.g. via a checkbox)
@@ -83,13 +83,14 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ {
* @param {Boolean} [options.sortable=true] is it possible to sort the table by clicking on column headers
* @param {Boolean} [options.reorderable=true] is it possible to reorder list rows
*/
init: function(parent, dataset, view_id, options) {
init: function() {
var self = this;
this._super(parent);
this.set_default_options(_.extend({}, this.defaults, options || {}));
this.dataset = dataset;
this.model = dataset.model;
this.view_id = view_id;
this._super.apply(this, arguments);
this.options = _.defaults(this.options, {
GroupsType: ListView.Groups,
ListType: ListView.List,
});
this.previous_colspan = null;
this.colors = null;
this.fonts = null;
@@ -105,8 +106,8 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ {
} else {
this.groups.datagroup = new DataGroup(
this, this.model,
dataset.get_domain(),
dataset.get_context());
this.dataset.get_domain(),
this.dataset.get_context());
this.groups.datagroup.sort = this.dataset._sort;
}
@@ -125,21 +126,22 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ {
if (!this.options.$pager || !this.options.$pager.length) {
this.options.$pager = false;
}
this.name = "" + this.fields_view.arch.attrs.string;
// the view's number of records per page (|| section)
this._limit = (this.options.limit ||
this.defaults.limit ||
(this.getParent().action || {}).limit ||
parseInt(this.fields_view.arch.attrs.limit, 10) ||
80);
},
view_loading: function(r) {
return this.load_list(r);
},
set_default_options: function (options) {
this._super(options);
_.defaults(this.options, {
GroupsType: ListView.Groups,
ListType: ListView.List
});
//Sort
var default_order = this.fields_view.arch.attrs.default_order,
unsorted = !this.dataset._sort.length;
if (unsorted && default_order && !this.grouped) {
this.dataset.set_sort(default_order.split(','));
}
},
/**
* Set a custom Group construct as the root of the List View.
@@ -169,14 +171,41 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ {
}
});
},
/**
* View startup method, the default behavior is to set the ``oe_list``
* class on its root element and to perform an RPC load call.
*
* @returns {$.Deferred} loading promise
*/
willStart: function() {
// Retrieve the decoration defined on the model's list view
this.decoration = _.pick(this.fields_view.arch.attrs, function(value, key) {
return row_decoration.indexOf(key) >= 0;
});
this.decoration = _.mapObject(this.decoration, function(value) {
return py.parse(py.tokenize(value));
});
this.setup_columns(this.fields_view.fields, this.grouped);
return this._super();
},
start: function() {
var self = this;
this.$el.html(QWeb.render(this._template, this));
this.$el.addClass(this.fields_view.arch.attrs['class']);
this.$el.addClass('oe_list o_list_view');
// Selecting records
this.$('.oe_list_record_selector').click(function() {
var checked = self.$('.oe_list_record_selector').prop('checked');
self.$('.oe_list_record_selector input').prop('checked', checked || false);
var selection = self.groups.get_selection();
$(self.groups).trigger('selected', [selection.ids, selection.records]);
});
if(this.dataset._sort.length){
if(this.dataset._sort[0].indexOf('-') === -1){
this.$el.find('th[data-id=' + this.dataset._sort[0] + ']').addClass("sortdown");
}else {
this.$el.find('th[data-id=' + this.dataset._sort[0].split('-')[1] + ']').addClass("sortup");
}
}
this.trigger('list_view_loaded', data, this.grouped);
return this._super();
},
/**
@@ -201,73 +230,6 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ {
});
return classnames;
},
/**
* Called after loading the list view's description, sets up such things
* as the view table's columns, renders the table itself and hooks up the
* various table-level and row-level DOM events (action buttons, deletion
* buttons, selection of records, [New] button, selection of a given
* record, ...)
*
* Sets up the following:
*
* * Processes arch and fields to generate a complete field descriptor for each field
* * Create the table itself and allocate visible columns
* * Hook in the top-level (header) [New|Add] and [Delete] button
* * Sets up showing/hiding the top-level [Delete] button based on records being selected or not
* * Sets up event handlers for action buttons and per-row deletion button
* * Hooks global callback for clicking on a row
*
* @param {Object} data wrapped fields_view_get result
* @param {Object} data.fields_view fields_view_get result (processed)
* @param {Object} data.fields_view.fields mapping of fields for the current model
* @param {Object} data.fields_view.arch current list view descriptor
* @param {Boolean} grouped Is the list view grouped
*/
load_list: function(data) {
var self = this;
this.fields_view = data;
this.name = "" + this.fields_view.arch.attrs.string;
this._limit = parseInt(this.fields_view.arch.attrs.limit, 10) || this._limit;
// Retrieve the decoration defined on the model's list view
this.decoration = _.pick(this.fields_view.arch.attrs, function(value, key) {
return row_decoration.indexOf(key) >= 0;
});
this.decoration = _.mapObject(this.decoration, function(value) {
return py.parse(py.tokenize(value));
});
this.setup_columns(this.fields_view.fields, this.grouped);
this.$el.html(QWeb.render(this._template, this));
this.$el.addClass(this.fields_view.arch.attrs['class']);
// Head hook
// Selecting records
this.$el.find('.oe_list_record_selector').click(function(){
self.$el.find('.oe_list_record_selector input').prop('checked',
self.$el.find('.oe_list_record_selector').prop('checked') || false);
var selection = self.groups.get_selection();
$(self.groups).trigger(
'selected', [selection.ids, selection.records]);
});
//Sort
var default_order = this.fields_view.arch.attrs.default_order,
unsorted = !this.dataset._sort.length;
if (unsorted && default_order && !this.grouped) {
this.dataset.set_sort(default_order.split(','));
}
if(this.dataset._sort.length){
if(this.dataset._sort[0].indexOf('-') == -1){
this.$el.find('th[data-id=' + this.dataset._sort[0] + ']').addClass("sortdown");
}else {
this.$el.find('th[data-id=' + this.dataset._sort[0].split('-')[1] + ']').addClass("sortup");
}
}
this.trigger('list_view_loaded', data, this.grouped);
},
/**
* Render the buttons according to the ListView.buttons template and
* add listeners on it.
@@ -499,16 +461,6 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ {
this.sidebar.do_hide();
}
},
/**
* Reloads the list view based on the current settings (dataset & al)
*
* @deprecated
* @param {Boolean} [grouped] Should the list be displayed grouped
* @param {Object} [context] context to send the server while loading the view
*/
reload_view: function (grouped, context) {
return this.load_view(context);
},
/**
* re-renders the content of the list view
*
@@ -617,8 +569,7 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ {
this.no_leaf = !!context['group_by_no_leaf'];
this.grouped = !!group_by;
return this.alive(this.load_view(context)).then(
this.proxy('reload_content'));
return this.reload_content();
},
/**
* Handles the signal to delete lines from the records list
@@ -720,7 +671,7 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ {
c.set_eval_context(_.extend({
active_id: id,
active_ids: [id],
active_model: this.dataset.model
active_model: this.model
}, this.records.get(id).toContext()));
if (action.context) {
c.add(action.context);
@@ -960,7 +911,7 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ {
});
core.view_registry.add('list', ListView);
ListView.List = Class.extend( /** @lends instance.web.ListView.List# */{
ListView.List = Class.extend({
/**
* List display for the ListView, handles basic DOM events and transforms
* them in the relevant higher-level events, to which the list view (or
@@ -1187,7 +1138,7 @@ ListView.List = Class.extend( /** @lends instance.web.ListView.List# */{
}
}
return column.format(record.toForm().data, {
model: this.dataset.model,
model: this.model,
id: record.get('id')
});
},
@@ -1299,7 +1250,7 @@ ListView.List = Class.extend( /** @lends instance.web.ListView.List# */{
}
});
ListView.Groups = Class.extend( /** @lends instance.web.ListView.Groups# */{
ListView.Groups = Class.extend({
passthrough_events: 'action deleted row_link',
/**
* Grouped display for the ListView. Handles basic DOM events and interacts
@@ -38,19 +38,18 @@ var Editor = Widget.extend({
delegate: this.getParent(),
});
this.delegate = this.options.delegate;
this.record = null;
this.form = new (this.options.formView)(this, this.delegate.dataset, false, {
},
start: function () {
var self = this;
var form_fields_view = this._validate_view(this.delegate.edition_view(this));
this.form = new (this.options.formView)(this, this.delegate.dataset, form_fields_view, {
initial_mode: 'edit',
is_list_editable: true,
disable_autofocus: true,
$buttons: $(),
$pager: $(),
});
},
start: function () {
var self = this;
this.form.embedded_view = this._validate_view(this.delegate.edition_view(this));
return $.when(this._super(), this.form.appendTo($('<div/>')).then(function() {
self.form.$el.addClass(self.$el.attr('class'));
self.replaceElement(self.form.$el);
@@ -258,7 +257,7 @@ ListView.include(/** @lends instance.web.ListView# */{
this._super.apply(this, arguments);
}
},
load_list: function (data, grouped) {
start: function () {
// tree/@editable takes priority on everything else if present.
var result = this._super.apply(this, arguments);
+70 -72
View File
@@ -6,6 +6,7 @@ odoo.define('web.PivotView', function (require) {
var core = require('web.core');
var crash_manager = require('web.crash_manager');
var data_manager = require('web.data_manager');
var formats = require('web.formats');
var framework = require('web.framework');
var Model = require('web.DataModel');
@@ -19,10 +20,6 @@ var _t = core._t;
var QWeb = core.qweb;
var PivotView = View.extend({
template: 'PivotView',
icon: 'fa-table',
display_name: _lt('Pivot'),
view_type: 'pivot',
events: {
'click .oe-opened': 'on_open_header_click',
'click .oe-closed': 'on_closed_header_click',
@@ -30,17 +27,20 @@ var PivotView = View.extend({
'click td': 'on_cell_click',
'click .measure-row': 'on_measure_row_click',
},
display_name: _lt('Pivot'),
icon: 'fa-table',
require_fields: true,
template: 'PivotView',
init: function(parent, dataset, view_id, options) {
this._super(parent, dataset, view_id, options);
this.model = new Model(dataset.model, {group_by_no_leaf: true});
init: function() {
this._super.apply(this, arguments);
this._model = new Model(this.model, {group_by_no_leaf: true});
this.fields = {};
this.measures = {};
this.groupable_fields = {};
this.ready = false; // will be ready after the first do_search
this.data_loaded = $.Deferred();
this.title = options.title;
this.title = this.options.title || this.fields_view.arch.attrs.string;
this.main_row = {
root: undefined,
@@ -63,30 +63,72 @@ var PivotView = View.extend({
this.numbering = {};
this.widgets = [];
this.enable_linking = !this.fields_view.arch.attrs.disable_linking;
},
willStart: function () {
var self = this;
return session.rpc('/web/pivot/check_xlwt').then(function(result) {
var fields_def = data_manager.load_fields(this.dataset);
var xlwt_def = session.rpc('/web/pivot/check_xlwt').then(function(result) {
self.xlwt_installed = result;
});
this.fields_view.arch.children.forEach(function (field) {
var name = field.attrs.name;
if (field.attrs.interval) {
name += ':' + field.attrs.interval;
}
//noinspection FallThroughInSwitchStatementJS
switch (field.attrs.type) {
case 'measure':
self.widgets.push(field.attrs.widget || "");
self.active_measures.push(name);
break;
case 'col':
self.initial_col_groupby.push(name);
break;
default:
if ('operator' in field.attrs) {
self.active_measures.push(name);
break;
}
case 'row':
self.initial_row_groupby.push(name);
}
});
if ((!this.active_measures.length) || this.fields_view.arch.attrs.display_quantity) {
this.active_measures.push('__count__');
}
return $.when(fields_def, xlwt_def, this._super()).then(function (fields) {
self.prepare_fields(fields);
// add active measures to the measure list. This is very rarely necessary, but it
// can be useful if one is working with a functional field non stored, but in a
// model with an overrided read_group method. In this case, the pivot view could
// work, and the measure should be allowed. However, be careful if you define a
// measure in your pivot view: non stored functional fields will probably not work
// (their aggregate will always be 0).
_.each(self.active_measures, function (m) {
if (!(m in self.measures)) {
self.measures[m] = self.fields[m];
}
});
});
},
start: function () {
this.$table_container = this.$('.o-pivot-table');
var load_fields = this.model.call('fields_get', [], {context: this.dataset.get_context()})
.then(this.prepare_fields.bind(this));
return $.when(this._super(), load_fields).then(this.render_field_selection.bind(this));
},
render_field_selection: function () {
var self = this;
var context = {fields: _.chain(this.groupable_fields).pairs().sortBy(function(f){return f[1].string;}).value()};
this.$el.toggleClass('oe_enable_linking', this.enable_linking);
var context = {
fields: _.chain(this.groupable_fields).pairs().sortBy(function(f) {
return f[1].string;
}).value(),
};
this.$field_selection = this.$('.o-field-selection');
this.$field_selection.html(QWeb.render('PivotView.FieldSelection', context));
core.bus.on('click', self, function () {
self.$field_selection.find('ul').first().hide();
core.bus.on('click', this, function () {
this.$field_selection.find('ul').first().hide();
});
return this._super();
},
/**
* Render the buttons according to the PivotView.buttons template and
@@ -127,49 +169,6 @@ var PivotView = View.extend({
this.sidebar.appendTo($node);
}
},
view_loading: function (fvg) {
var self = this;
this.title = this.title || fvg.arch.attrs.string;
this.enable_linking = !fvg.arch.attrs.disable_linking;
this.$el.toggleClass('oe-enable-linking', this.enable_linking);
fvg.arch.children.forEach(function (field) {
var name = field.attrs.name;
if (field.attrs.interval) {
name += ':' + field.attrs.interval;
}
//noinspection FallThroughInSwitchStatementJS
switch (field.attrs.type) {
case 'measure':
self.widgets.push(field.attrs.widget || "");
self.active_measures.push(name);
break;
case 'col':
self.initial_col_groupby.push(name);
break;
default:
if ('operator' in field.attrs) {
self.active_measures.push(name);
break;
}
case 'row':
self.initial_row_groupby.push(name);
}
});
if ((!self.active_measures.length) || fvg.arch.attrs.display_quantity){
self.active_measures.push('__count__');
}
// add active measures to the measure list. This is very rarely necessary, but it
// can be useful if one is working with a functional field non stored, but in a
// model with an overrided read_group method. In this case, the pivot view could
// work, and the measure should be allowed. However, be careful if you define a
// measure in your pivot view: non stored functional fields will probably not work
// (their aggregate will always be 0).
_.each(this.active_measures, function (m) {
if (!(m in self.measures)) {
self.measures[m] = self.fields[m];
}
});
},
prepare_fields: function (fields) {
var self = this,
groupable_types = ['many2one', 'char', 'boolean',
@@ -277,15 +276,14 @@ var PivotView = View.extend({
col_domain = this.headers[col_id].domain,
context = _.omit(_.clone(this.context), 'group_by');
var views = [
[this.options.action_views_ids.list || false, 'list'],
[this.options.action_views_ids.form || false, 'form']
];
var views = _.filter(this.options.action.views, function (view) {
return view[1] === 'form' || view[1] === 'list';
});
return this.do_action({
type: 'ir.actions.act_window',
name: this.title,
res_model: this.model.name,
res_model: this.model,
views: views,
view_type : "list",
view_mode : "list",
@@ -345,7 +343,7 @@ var PivotView = View.extend({
groupbys.push([field].concat(other_groupbys.slice(0,i)));
}
return $.when.apply(null, groupbys.map(function (groupby) {
return self.model.query(fields)
return self._model.query(fields)
.filter(header.domain.length ? header.domain : self.domain)
.context(self.context)
.lazy(false)
@@ -398,7 +396,7 @@ var PivotView = View.extend({
}
}
return $.when.apply(null, groupbys.map(function (groupby) {
return self.model.query(fields)
return self._model.query(fields)
.filter(self.domain)
.context(self.context)
.lazy(false)
@@ -68,7 +68,9 @@ var Input = Widget.extend( /** @lends instance.web.search.Input# */{
"get_domain not implemented for widget " + this.attrs.type);
},
load_attrs: function (attrs) {
attrs.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
if (!_.isObject(attrs.modifiers)) {
attrs.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
}
this.attrs = attrs;
},
/**
+119 -94
View File
@@ -2,7 +2,7 @@ odoo.define('web.FavoriteMenu', function (require) {
"use strict";
var core = require('web.core');
var Model = require('web.DataModel');
var data_manager = require('web.data_manager');
var pyeval = require('web.pyeval');
var session = require('web.session');
var Widget = require('web.Widget');
@@ -26,18 +26,18 @@ return Widget.extend({
}
},
},
init: function (parent, query, target_model, action_id) {
init: function (parent, query, target_model, action_id, filters) {
this._super.apply(this,arguments);
this.searchview = parent;
this.query = query;
this.target_model = target_model;
this.model = new Model('ir.filters');
this.filters = {};
this.$filters = {};
this.action_id = action_id;
this.filters = {};
_.each(filters, this.add_filter.bind(this));
},
start: function () {
var self = this;
this.$filters = {};
this.$save_search = this.$('.oe-save-search');
this.$save_name = this.$('.oe-save-name');
this.$inputs = this.$save_name.find('input');
@@ -55,12 +55,10 @@ return Widget.extend({
}
})
.on('reset', this.proxy('clear_selection'));
return this.model.call('get_filters', [this.target_model, this.action_id],
{context: this.searchview.dataset.context})
.done(this.proxy('prepare_dropdown_menu'));
},
prepare_dropdown_menu: function (filters) {
filters.map(this.append_filter.bind(this));
_.each(this.filters, this.append_filter.bind(this));
return this._super();
},
toggle_save_menu: function (is_open) {
this.$save_search
@@ -115,17 +113,18 @@ return Widget.extend({
var filter = {
name: filter_name,
user_id: shared_filter ? false : session.uid,
model_id: this.searchview.dataset.model,
model_id: this.target_model,
context: results.context,
domain: results.domain,
sort: JSON.stringify(this.searchview.dataset._sort),
is_default: default_filter,
action_id: this.action_id,
};
return this.model.call('create_or_replace', [filter]).done(function (id) {
return data_manager.create_filter(filter).done(function (id) {
filter.id = id;
self.toggle_save_menu(false);
self.$save_name.find('input').val('').prop('checked', false);
self.add_filter(filter);
self.append_filter(filter);
self.toggle_filter(filter, true);
});
@@ -187,31 +186,39 @@ return Widget.extend({
clear_selection: function () {
this.$('li.selected').removeClass('selected');
},
/**
* Adds a filter description to the filters dict
* @param {Object} [filter] the filter description
*/
add_filter: function (filter) {
this.filters[this.key_for(filter)] = filter;
},
/**
* Creates a $filter JQuery node, adds it to the $filters dict and appends it to the filter menu
* @param {Object} [filter] the filter description
*/
append_filter: function (filter) {
var self = this,
key = this.key_for(filter),
$filter;
var self = this;
var key = this.key_for(filter);
this.$divider.show();
if (key in this.$filters) {
$filter = this.$filters[key];
} else {
this.filters[key] = filter;
$filter = $('<li></li>')
.insertBefore(this.$divider)
if (!(key in this.$filters)) {
var $filter = $('<li>')
.addClass(filter.user_id ? 'oe_searchview_custom_private'
: 'oe_searchview_custom_public')
.toggleClass('oe_searchview_custom_default', filter.is_default)
.append($('<a>').text(filter.name));
.append($('<a>').text(filter.name))
.append($('<span>', {
class: 'fa fa-trash-o remove-filter',
on: {
click: function (event) {
event.stopImmediatePropagation();
self.remove_filter(filter, $filter, key);
},
},
}))
.insertBefore(this.$divider);
this.$filters[key] = $filter;
this.$filters[key].addClass(filter.user_id ? 'oe_searchview_custom_private'
: 'oe_searchview_custom_public');
$('<span>')
.addClass('fa fa-trash-o remove-filter')
.click(function (event) {
event.stopImmediatePropagation();
self.remove_filter(filter, $filter, key);
})
.appendTo($filter);
}
this.$filters[key].unbind('click').click(function () {
self.toggle_filter(filter);
@@ -244,14 +251,16 @@ return Widget.extend({
if (!confirm(filter.user_id ? warning : global_warning)) {
return;
}
this.model.call('unlink', [filter.id]).done(function () {
$filter.remove();
delete self.$filters[key];
delete self.filters[key];
if (_.isEmpty(self.filters)) {
self.$divider.hide();
}
});
return data_manager
.delete_filter(filter)
.done(function () {
$filter.remove();
delete self.$filters[key];
delete self.filters[key];
if (_.isEmpty(self.filters)) {
self.$divider.hide();
}
});
},
});
@@ -260,6 +269,7 @@ return Widget.extend({
odoo.define('web.FilterMenu', function (require) {
"use strict";
var data_manager = require('web.data_manager');
var search_filters = require('web.search_filters');
var search_inputs = require('web.search_inputs');
var Widget = require('web.Widget');
@@ -282,22 +292,12 @@ return Widget.extend({
}
},
},
init: function (parent, filters, fields_def) {
init: function (parent, filters) {
this._super(parent);
this.filters = filters || [];
this.searchview = parent;
this.propositions = [];
this.fields_def = fields_def.then(function (data) {
var fields = {
id: { string: 'ID', type: 'id', searchable: true }
};
_.each(data, function(field_def, field_name) {
if (field_def.selectable !== false && field_name !== 'id') {
fields[field_name] = field_def;
}
});
return fields;
});
this.custom_filters_open = false;
},
start: function () {
var self = this;
@@ -311,23 +311,41 @@ return Widget.extend({
$('<li class="divider">').insertBefore(self.$add_filter);
}
});
this.append_proposition().then(function (prop) {
prop.$el.hide();
});
},
get_fields: function () {
if (!this._fields_def) {
this._fields_def = data_manager.load_fields(this.searchview.dataset).then(function (data) {
var fields = {
id: { string: 'ID', type: 'id', searchable: true }
};
_.each(data, function(field_def, field_name) {
if (field_def.selectable !== false && field_name !== 'id') {
fields[field_name] = field_def;
}
});
return fields;
});
}
return this._fields_def;
},
toggle_custom_filter_menu: function (is_open) {
this.$add_filter
.toggleClass('o_closed_menu', !(_.isUndefined(is_open)) ? !is_open : undefined)
.toggleClass('o_open_menu', is_open);
this.$add_filter_menu.toggle(is_open);
if (this.$add_filter.hasClass('o_closed_menu') && (!this.propositions.length)) {
this.append_proposition();
var self = this;
this.custom_filters_open = !_.isUndefined(is_open) ? is_open : !this.custom_filters_open;
var def;
if (this.custom_filters_open && !this.propositions.length) {
def = this.append_proposition();
}
this.$('.oe-filter-condition').toggle(is_open);
$.when(def).then(function () {
self.$add_filter
.toggleClass('o_closed_menu', !self.custom_filters_open)
.toggleClass('o_open_menu', self.custom_filters_open);
self.$add_filter_menu.toggle(self.custom_filters_open);
self.$('.oe-filter-condition').toggle(self.custom_filters_open);
});
},
append_proposition: function () {
var self = this;
return this.fields_def.then(function (fields) {
return this.get_fields().then(function (fields) {
var prop = new search_filters.ExtendedSearchProposition(self, fields);
self.propositions.push(prop);
prop.insertBefore(self.$add_filter_menu);
@@ -368,9 +386,10 @@ return Widget.extend({
odoo.define('web.GroupByMenu', function (require) {
"use strict";
var Widget = require('web.Widget');
var core = require('web.core');
var data_manager = require('web.data_manager');
var search_inputs = require('web.search_inputs');
var Widget = require('web.Widget');
var QWeb = core.qweb;
@@ -387,15 +406,13 @@ return Widget.extend({
this.toggle_add_menu();
},
},
init: function (parent, groups, fields_def) {
init: function (parent, groups) {
this._super(parent);
this.groups = groups || [];
this.groupable_fields = [];
this.searchview = parent;
this.fields_def = fields_def.then(this.proxy('get_groupable_fields'));
},
start: function () {
var self = this;
this.$menu = this.$('.group-by-menu');
var divider = this.$menu.find('.divider');
_.invoke(this.groups, 'insertBefore', divider);
@@ -403,35 +420,43 @@ return Widget.extend({
divider.show();
}
this.$add_group = this.$menu.find('.add-custom-group');
this.fields_def.then(function () {
self.$menu.append(QWeb.render('GroupByMenuSelector', self));
self.$add_group_menu = self.$('.oe-add-group');
self.$group_selector = self.$('.oe-group-selector');
self.$('.oe-select-group').click(function () {
self.toggle_add_menu(false);
var field = self.$group_selector.find(':selected').data('name');
self.add_groupby_to_menu(field);
});
});
},
get_groupable_fields: function (fields) {
var groupable_types = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime'];
var filter_group_field = _.filter(fields, function(field, name) {
if (field.store && _.contains(groupable_types, field.type)) {
field.name = name;
return field;
}
});
this.groupable_fields = _.sortBy(filter_group_field, 'string');
get_fields: function () {
var self = this;
if (!this._fields_def) {
this._fields_def = data_manager.load_fields(this.searchview.dataset).then(function (fields) {
var groupable_types = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime'];
var filter_group_field = _.filter(fields, function(field, name) {
if (field.store && _.contains(groupable_types, field.type)) {
field.name = name;
return field;
}
});
self.groupable_fields = _.sortBy(filter_group_field, 'string');
self.$menu.append(QWeb.render('GroupByMenuSelector', self));
self.$add_group_menu = self.$('.oe-add-group');
self.$group_selector = self.$('.oe-group-selector');
self.$('.oe-select-group').click(function () {
self.toggle_add_menu(false);
var field = self.$group_selector.find(':selected').data('name');
self.add_groupby_to_menu(field);
});
});
}
return this._fields_def;
},
toggle_add_menu: function (is_open) {
this.$add_group
.toggleClass('o_closed_menu', !(_.isUndefined(is_open)) ? !is_open : undefined)
.toggleClass('o_open_menu', is_open);
this.$add_group_menu.toggle(is_open);
if (this.$add_group.hasClass('o_open_menu')) {
this.$group_selector.focus();
}
var self = this;
this.get_fields().then(function () {
self.$add_group
.toggleClass('o_closed_menu', !(_.isUndefined(is_open)) ? !is_open : undefined)
.toggleClass('o_open_menu', is_open);
self.$add_group_menu.toggle(is_open);
if (self.$add_group.hasClass('o_open_menu')) {
self.$group_selector.focus();
}
});
},
add_groupby_to_menu: function (field_name) {
var filter = new search_inputs.Filter({attrs:{
+44 -50
View File
@@ -3,13 +3,13 @@ odoo.define('web.SearchView', function (require) {
var AutoComplete = require('web.AutoComplete');
var core = require('web.core');
var data_manager = require('web.data_manager');
var FavoriteMenu = require('web.FavoriteMenu');
var FilterMenu = require('web.FilterMenu');
var GroupByMenu = require('web.GroupByMenu');
var Model = require('web.DataModel');
var pyeval = require('web.pyeval');
var search_inputs = require('web.search_inputs');
var utils = require('web.utils');
var View = require('web.View');
var Widget = require('web.Widget');
var Backbone = window.Backbone;
@@ -317,8 +317,14 @@ var FacetValueView = Widget.extend({
}
});
var SearchView = Widget.extend(/** @lends instance.web.SearchView# */{
template: "SearchView",
var SearchView = View.extend({
defaults: _.extend({}, View.prototype.defaults, {
hidden: false,
disable_filters: false,
disable_groupby: false,
disable_favorites: false,
disable_custom_filters: false,
}),
events: {
// focus last input if view itself is clicked
'click': function (e) {
@@ -355,45 +361,47 @@ var SearchView = Widget.extend(/** @lends instance.web.SearchView# */{
this.$el.autocomplete('widget').css('z-index', 9999);
},
},
template: "SearchView",
/**
* @constructs instance.web.SearchView
* @extends instance.web.Widget
* @constructs SearchView
* @extends View
*
* @param parent
* @param dataset
* @param view_id
* @param defaults
* @param fields_view
* @param {Object} [options]
* @param {Boolean} [options.hidden=false] hide the search view
* @param {Boolean} [options.disable_custom_filters=false] do not load custom filters from ir.filters
*/
init: function(parent, dataset, view_id, defaults, options) {
this.options = _.defaults(options || {}, {
hidden: false,
disable_filters: false,
disable_groupby: false,
disable_favorites: false,
disable_custom_filters: false,
});
this._super(parent);
init: function() {
this._super.apply(this, arguments);
this.title = this.options.action && this.options.action.name;
this.action_id = this.options.action && this.options.action.id;
this.query = undefined;
this.dataset = dataset;
this.view_id = view_id;
this.title = options.action && options.action.name;
this.search_fields = [];
this.filters = [];
this.groupbys = [];
this.visible_filters = (localStorage.visible_search_menu === 'true');
this.input_subviews = []; // for user input in searchbar
this.defaults = defaults || {};
this.headless = this.options.hidden && _.isEmpty(this.defaults);
this.search_defaults = this.options.search_defaults || {};
this.headless = this.options.hidden && _.isEmpty(this.search_defaults);
this.$buttons = this.options.$buttons;
this.filter_menu = undefined;
this.groupby_menu = undefined;
this.favorite_menu = undefined;
this.action_id = this.options && this.options.action && this.options.action.id;
},
},
willStart: function () {
var self = this;
var def;
if (!this.options.disable_favorites) {
def = data_manager.load_filters(this.dataset, this.action_id).then(function (filters) {
self.favorite_filters = filters;
});
}
return $.when(this._super(), def);
},
start: function() {
if (this.headless) {
this.do_hide();
@@ -405,43 +413,29 @@ var SearchView = Widget.extend(/** @lends instance.web.SearchView# */{
.on('add change reset remove', this.proxy('do_search'))
.on('change', this.proxy('renderChangedFacets'))
.on('add reset remove', this.proxy('renderFacets'));
var load_view = this.dataset._model.fields_view_get({
view_id: this.view_id,
view_type: 'search',
context: this.dataset.get_context(),
});
this.$('.oe_searchview_unfold_drawer')
.toggleClass('fa-caret-down', !this.visible_filters)
.toggleClass('fa-caret-up', this.visible_filters);
return this.alive($.when(this._super(), this.alive(load_view).then(this.view_loaded.bind(this))));
},
get_title: function() {
return this.title;
},
view_loaded: function (r) {
var menu_defs = [];
this.fields_view_get = r;
this.view_id = this.view_id || r.view_id;
this.prepare_search_inputs();
if (this.$buttons) {
var fields_def = new Model(this.dataset.model).call('fields_get', {
context: this.dataset.get_context()
});
if (!this.options.disable_filters) {
this.filter_menu = new FilterMenu(this, this.filters, fields_def);
this.filter_menu = new FilterMenu(this, this.filters);
menu_defs.push(this.filter_menu.appendTo(this.$buttons));
}
if (!this.options.disable_groupby) {
this.groupby_menu = new GroupByMenu(this, this.groupbys, fields_def);
this.groupby_menu = new GroupByMenu(this, this.groupbys);
menu_defs.push(this.groupby_menu.appendTo(this.$buttons));
}
if (!this.options.disable_favorites) {
this.favorite_menu = new FavoriteMenu(this, this.query, this.dataset.model, this.action_id);
this.favorite_menu = new FavoriteMenu(this, this.query, this.dataset.model, this.action_id, this.favorite_filters);
menu_defs.push(this.favorite_menu.appendTo(this.$buttons));
}
}
return $.when.apply($, menu_defs).then(this.proxy('set_default_filters'));
return $.when.apply($, menu_defs).then(this.set_default_filters.bind(this));
},
get_title: function() {
return this.title;
},
set_default_filters: function () {
var self = this,
@@ -449,10 +443,10 @@ var SearchView = Widget.extend(/** @lends instance.web.SearchView# */{
if (!self.options.disable_custom_filters && default_custom_filter) {
return this.favorite_menu.toggle_filter(default_custom_filter, true);
}
if (!_.isEmpty(this.defaults)) {
if (!_.isEmpty(this.search_defaults)) {
var inputs = this.search_fields.concat(this.filters, this.groupbys),
defaults = _.invoke(inputs, 'facet_for_defaults', this.defaults);
return $.when.apply(null, defaults).then(function () {
search_defaults = _.invoke(inputs, 'facet_for_defaults', this.search_defaults);
return $.when.apply(null, search_defaults).then(function () {
self.query.reset(_(arguments).compact(), {preventSearch: true});
});
}
@@ -667,7 +661,7 @@ var SearchView = Widget.extend(/** @lends instance.web.SearchView# */{
// * this.group_by: group_bys
prepare_search_inputs: function () {
var self = this,
arch = this.fields_view_get.arch;
arch = this.fields_view.arch;
var filters = [].concat.apply([], _.map(arch.children, function (item) {
return item.tag !== 'group' ? eval_item(item) : item.children.map(eval_item);
@@ -702,7 +696,7 @@ var SearchView = Widget.extend(/** @lends instance.web.SearchView# */{
}
if (filter.item.tag === 'field') {
var attrs = filter.item.attrs;
var field = self.fields_view_get.fields[attrs.name];
var field = self.fields_view.fields[attrs.name];
// M2O combined with selection widget is pointless and broken in search views,
// but has been used in the past for unsupported hacks -> ignore it
+29 -55
View File
@@ -14,41 +14,21 @@ var View = require('web.View');
var _lt = core._lt;
var QWeb = core.qweb;
var TreeView = View.extend(/** @lends instance.web.TreeView# */{
var TreeView = View.extend({
display_name: _lt('Tree'),
icon: 'fa-align-left',
view_type: 'tree',
/**
* Indicates that this view is not searchable, and thus that no search
* view should be displayed (if there is one active).
*/
searchable : false,
// Indicates that this view is not searchable, and thus that no search view should be displayed.
searchable: false,
/**
* Genuine tree view (the one displayed as a tree, not the list)
*
* @constructs instance.web.TreeView
* @extends instance.web.View
*
* @param parent
* @param dataset
* @param view_id
* @param options
*/
init: function(parent, dataset, view_id, options) {
this._super(parent);
this.dataset = dataset;
this.model = dataset.model;
this.view_id = view_id;
init: function() {
this._super.apply(this, arguments);
this.records = {};
this.options = _.extend({}, this.defaults, options || {});
_.bindAll(this, 'color_for');
},
view_loading: function(r) {
return this.load_tree(r);
this.fields = this.fields_view.fields;
this.children_field = this.fields_view.field_parent;
},
/**
@@ -67,20 +47,29 @@ var TreeView = View.extend(/** @lends instance.web.TreeView# */{
}
return fields;
},
load_tree: function (fields_view) {
var self = this;
var has_toolbar = !!fields_view.arch.attrs.toolbar;
// field name in OpenERP is kinda stupid: this is the name of the field
// holding the ids to the children of the current node, why call it
// field_parent?
this.children_field = fields_view.field_parent;
this.fields_view = fields_view;
willStart: function () {
_(this.fields_view.arch.children).each(function (field) {
if (field.attrs.modifiers) {
field.attrs.modifiers = JSON.parse(field.attrs.modifiers);
}
});
this.fields = fields_view.fields;
if (this.fields_view.arch.attrs.colors) {
this.colors = _(this.fields_view.arch.attrs.colors.split(';')).chain()
.compact()
.map(function(color_pair) {
var pair = color_pair.split(':'),
color = pair[0],
expr = pair[1];
return [color, py.parse(py.tokenize(expr)), expr];
}).value();
}
return this._super();
},
start: function () {
var self = this;
var has_toolbar = !!this.fields_view.arch.attrs.toolbar;
this.hook_row_click();
this.$el.html(QWeb.render('TreeView', {
'title': this.fields_view.arch.attrs.string,
@@ -120,17 +109,7 @@ var TreeView = View.extend(/** @lends instance.web.TreeView# */{
// TODO store open nodes in url ?...
this.do_push_state({});
if (!this.fields_view.arch.attrs.colors) {
return;
}
this.colors = _(this.fields_view.arch.attrs.colors.split(';')).chain()
.compact()
.map(function(color_pair) {
var pair = color_pair.split(':'),
color = pair[0],
expr = pair[1];
return [color, py.parse(py.tokenize(expr)), expr];
}).value();
return this._super();
},
/**
* Returns the color for the provided record in the current view (from the
@@ -225,7 +204,7 @@ var TreeView = View.extend(/** @lends instance.web.TreeView# */{
activate: function(id) {
var self = this;
var local_context = {
active_model: self.dataset.model,
active_model: self.model,
active_id: id,
active_ids: [id]
};
@@ -234,7 +213,7 @@ var TreeView = View.extend(/** @lends instance.web.TreeView# */{
this.dataset.get_context(), local_context));
return this.rpc('/web/treeview/action', {
id: id,
model: this.dataset.model,
model: this.model,
context: ctx
}).then(function (actions) {
if (!actions.length) { return; }
@@ -260,11 +239,6 @@ var TreeView = View.extend(/** @lends instance.web.TreeView# */{
$child_row.toggle(show);
}, this);
},
do_hide: function () {
this.hidden = true;
this._super();
}
});
core.view_registry.add('tree', TreeView);
+2 -2
View File
@@ -5,6 +5,7 @@ var ActionManager = require('web.ActionManager');
var core = require('web.core');
var crash_manager = require('web.crash_manager');
var data = require('web.data');
var data_manager = require('web.data_manager');
var framework = require('web.framework');
var Loading = require('web.Loading');
var Menu = require('web.Menu');
@@ -16,7 +17,6 @@ var UserMenu = require('web.UserMenu');
var utils = require('web.utils');
var Widget = require('web.Widget');
var QWeb = core.qweb;
var _t = core._t;
var WebClient = Widget.extend({
@@ -310,7 +310,7 @@ var WebClient = Widget.extend({
},
on_menu_action: function(options) {
var self = this;
return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
return this.menu_dm.add(data_manager.load_action(options.action_id))
.then(function (result) {
return self.action_mutex.exec(function() {
if (options.needaction) {
+40 -44
View File
@@ -171,7 +171,7 @@ odoo.define_section('editor', ['web.ListEditor'], function (test, mock) {
});
});
odoo.define_section('list.edition', ['web.data', 'web.ListView'], function (test, mock) {
odoo.define_section('list.edition', ['web.data', 'web.ListView', 'web.data_manager'], function (test, mock) {
function setup () {
var records = {};
@@ -193,23 +193,12 @@ odoo.define_section('list.edition', ['web.data', 'web.ListView'], function (test
}
return [];
});
mock.add('demo:fields_view_get', function () {
return {
type: 'tree',
fields: {
a: {type: 'char', string: "A"},
b: {type: 'char', string: "B"},
c: {type: 'char', string: "C"}
},
arch: '<tree><field name="a"/><field name="b"/><field name="c"/></tree>',
};
});
mock.add('demo:onchange', function () {
return {};
});
}
test('newrecord', function (assert, data, ListView) {
test('newrecord', function (assert, data, ListView, data_manager) {
setup();
assert.expect(6);
var got_defaults = false;
@@ -224,7 +213,16 @@ odoo.define_section('list.edition', ['web.data', 'web.ListView'], function (test
});
var ds = new data.DataSetStatic(null, 'demo', null, [1]);
var l = new ListView({}, ds, false, {editable: 'top'});
var fields_view = data_manager._postprocess_fvg({
type: 'tree',
fields: {
a: {type: 'char', string: "A"},
b: {type: 'char', string: "B"},
c: {type: 'char', string: "C"}
},
arch: '<tree><field name="a"/><field name="b"/><field name="c"/></tree>',
});
var l = new ListView({}, ds, fields_view, {editable: 'top'});
var $fix = $( "#qunit-fixture");
return l.appendTo($fix)
@@ -249,26 +247,25 @@ odoo.define_section('list.edition', ['web.data', 'web.ListView'], function (test
});
});
odoo.define_section('list.edition.events', ['web.data', 'web.ListView'], function (test, mock) {
odoo.define_section('list.edition.events', ['web.data', 'web.ListView', 'web.data_manager'], function (test, mock) {
function fields_view_get () {
return {
type: 'tree',
fields: {
a: {type: 'char', string: "A"},
b: {type: 'char', string: "B"},
c: {type: 'char', string: "C"}
},
arch: '<tree><field name="a"/><field name="b"/><field name="c"/></tree>',
};
}
function setup () {
mock.add('demo:read', function () {
return [{ id: 1, a: 'foo', b: 'bar', c: 'baz' }];
});
mock.add('demo:fields_view_get', function () {
return {
type: 'tree',
fields: {
a: {type: 'char', string: "A"},
b: {type: 'char', string: "B"},
c: {type: 'char', string: "C"}
},
arch: '<tree><field name="a"/><field name="b"/><field name="c"/></tree>',
};
});
}
test('edition events',function (assert, data, ListView) {
test('edition events',function (assert, data, ListView, data_manager) {
setup();
assert.expect(4);
var ds = new data.DataSetStatic(null, 'demo', null, [1]);
@@ -276,7 +273,8 @@ odoo.define_section('list.edition.events', ['web.data', 'web.ListView'], functio
counter: 0,
onEvent: function (e) { this.counter++; }
};
var l = new ListView({}, ds, false, {editable: 'top'});
var fields_view = data_manager._postprocess_fvg(fields_view_get());
var l = new ListView({}, ds, fields_view, {editable: 'top'});
l.on('edit:before edit:after', o, o.onEvent);
var $fix = $( "#qunit-fixture");
@@ -293,11 +291,12 @@ odoo.define_section('list.edition.events', ['web.data', 'web.ListView'], functio
});
});
test('edition events: cancelling', function (assert, data, ListView) {
test('edition events: cancelling', function (assert, data, ListView, data_manager) {
setup();
var edit_after = false;
var ds = new data.DataSetStatic(null, 'demo', null, [1]);
var l = new ListView({}, ds, false, {editable: 'top'});
var fields_view = data_manager._postprocess_fvg(fields_view_get());
var l = new ListView({}, ds, fields_view, {editable: 'top'});
l.on('edit:before', {}, function (e) {
e.cancel = true;
});
@@ -321,24 +320,14 @@ odoo.define_section('list.edition.events', ['web.data', 'web.ListView'], functio
});
});
odoo.define_section('list.edition.onwrite', ['web.data', 'web.ListView'], function (test, mock) {
odoo.define_section('list.edition.onwrite', ['web.data', 'web.ListView', 'web.data_manager'], function (test, mock) {
test('record-to-read', function (assert, data, ListView) {
test('record-to-read', function (assert, data, ListView, data_manager) {
assert.expect(4);
mock.add('demo:onchange', function () {
return {};
});
mock.add('demo:fields_view_get', function () {
return {
type: 'tree',
fields: {
a: {type: 'char', string: "A"}
},
arch: '<tree on_write="on_write" colors="red:a == \'foo\'"><field name="a"/></tree>',
};
});
mock.add('demo:read', function (args, kwargs) {
if (_.isEmpty(args[0])) {
return [];
@@ -358,7 +347,14 @@ odoo.define_section('list.edition.onwrite', ['web.data', 'web.ListView'], functi
mock.add('demo:on_write', function () { return [42]; });
var ds = new data.DataSetStatic(null, 'demo', null, []);
var l = new ListView({}, ds, false, {editable: 'top'});
var fields_view = data_manager._postprocess_fvg({
type: 'tree',
fields: {
a: {type: 'char', string: "A"}
},
arch: '<tree on_write="on_write" colors="red:a == \'foo\'"><field name="a"/></tree>',
});
var l = new ListView({}, ds, fields_view, {editable: 'top'});
var $fix = $( "#qunit-fixture");
return l.appendTo($fix)
+11 -13
View File
@@ -1,17 +1,8 @@
odoo.define_section('list.buttons', ['web.ListView', 'web.data'], function (test, mock) {
odoo.define_section('list.buttons', ['web.ListView', 'web.data', 'web.data_manager'], function (test, mock) {
test('record-deletion', function (assert, ListView, data) {
test('record-deletion', function (assert, ListView, data, data_manager) {
assert.expect(2);
mock.add('demo:fields_view_get', function () {
return {
type: 'tree',
fields: {
a: {type: 'char', string: "A"}
},
arch: '<tree><field name="a"/><button type="object" name="foo"/></tree>',
};
});
mock.add('demo:read', function (args, kwargs) {
if (_.isEqual(args[0], [1, 2, 3])) {
return [
@@ -29,10 +20,18 @@ odoo.define_section('list.buttons', ['web.ListView', 'web.data'], function (test
});
mock.add('/web/dataset/call_button', function () { return false; });
var ds = new data.DataSetStatic(null, 'demo', null, [1, 2, 3]);
var fields_view = data_manager._postprocess_fvg({
type: 'tree',
fields: {
a: {type: 'char', string: "A"}
},
arch: '<tree><field name="a"/><button type="object" name="foo"/></tree>',
});
var list = new ListView({
do_action: odoo.testing.noop
}, ds, false, {editable: 'top'});
}, ds, fields_view, {editable: 'top'});
var $fix = $( "#qunit-fixture");
@@ -52,6 +51,5 @@ odoo.define_section('list.buttons', ['web.ListView', 'web.data'], function (test
assert.strictEqual($fix.find('table tbody tr[data-id]').length, 2,
"should have 2 rows left");
});
});
});
+67 -83
View File
@@ -156,28 +156,19 @@ odoo.define_section('search.query', ['web.SearchView'], function (test) {
* @return {instance.web.SearchView}
*/
function makeSearchView (test, dummy_widget_attributes, defaults, options) {
function makeSearchView (test, dummy_widget_attributes, defaults, options, raw_fields_view) {
var core = test.deps['web.core'];
var search_inputs = test.deps['web.search_inputs'];
var data = test.deps['web.data'];
var data_manager = test.deps['web.data_manager'];
var SearchView = test.deps['web.SearchView'];
var mock = test.mock;
var assert = test.assert
var assert = test.assert;
var DummyWidget = search_inputs.Field.extend(dummy_widget_attributes || {});
core.search_widgets_registry.add('dummy', DummyWidget);
mock.add('dummy.model:fields_view_get', function () {
return {
type: 'search',
fields: {
dummy: {type: 'char', string: "Dummy", searchable: true}
},
arch: '<search><field name="dummy" widget="dummy"/></search>'
};
}, true);
mock.add('ir.filters:get_filters', function () {
return [];
});
@@ -188,16 +179,22 @@ function makeSearchView (test, dummy_widget_attributes, defaults, options) {
};
});
// instance.client = { action_manager: { inner_action: undefined } };
var dataset = new data.DataSet(null, 'dummy.model');
var mock_parent = {getParent: function () {return null;}};
var mock_parent = {
getParent: function () { return null; },
};
options = _.defaults(options || {}, {$buttons: $('<div>')});
var view = new SearchView(mock_parent, dataset, false, defaults, options);
options.search_defaults = defaults;
var fields_view = data_manager._postprocess_fvg(raw_fields_view || {
type: 'search',
fields: {
dummy: {type: 'char', string: "Dummy", searchable: true}
},
arch: '<search><field name="dummy" widget="dummy"/></search>'
});
var view = new SearchView(mock_parent, dataset, fields_view, options);
view.on('invalid_search', this, function () {
assert.ok(false, JSON.stringify([].slice(arguments)));
@@ -205,7 +202,7 @@ function makeSearchView (test, dummy_widget_attributes, defaults, options) {
return view;
};
odoo.define_section('search.defaults', ['web.search_inputs', 'web.SearchView', 'web.core', 'web.data'], function (test, mock) {
odoo.define_section('search.defaults', ['web.search_inputs', 'web.SearchView', 'web.core', 'web.data', 'web.data_manager'], function (test, mock) {
test('calling', function (assert) {
assert.expect(2);
var defaults_called = false;
@@ -431,7 +428,7 @@ odoo.define_section('search.defaults', ['web.search_inputs', 'web.SearchView', '
});
odoo.define_section('search.completions', ['web.search_inputs', 'web.SearchView', 'web.core', 'web.data'], function (test, mock) {
odoo.define_section('search.completions', ['web.search_inputs', 'web.SearchView', 'web.core', 'web.data', 'web.data_manager'], function (test, mock) {
test('calling', function (assert) {
assert.expect(4);
@@ -851,7 +848,7 @@ odoo.define_section('search.completions', ['web.search_inputs', 'web.SearchView'
});
});
odoo.define_section('search.serialization', ['web.search_inputs', 'web.SearchView', 'web.core', 'web.data'], function (test, mock) {
odoo.define_section('search.serialization', ['web.search_inputs', 'web.SearchView', 'web.core', 'web.data', 'web.data_manager'], function (test, mock) {
test('No facet, no call', function (assert, search_inputs, SearchView, core) {
assert.expect(6);
@@ -1191,7 +1188,7 @@ odoo.define_section('search.serialization', ['web.search_inputs', 'web.SearchVie
});
});
odoo.define_section('search.serialization', ['web.search_inputs', 'web.SearchView', 'web.core', 'web.data'], function (test, mock) {
odoo.define_section('search.serialization', ['web.search_inputs', 'web.SearchView', 'web.core', 'web.data', 'web.data_manager'], function (test, mock) {
test('is-drawn', function (assert) {
assert.expect(3);
@@ -1210,26 +1207,23 @@ odoo.define_section('search.serialization', ['web.search_inputs', 'web.SearchVie
});
odoo.define_section('search.filters', ['web.search_inputs', 'web.SearchView', 'web.core', 'web.data'], function (test, mock) {
function setup () {
mock.add('dummy.model:fields_view_get', function () {
// view with a single group of filters
return {
type: 'search',
fields: {},
arch: '<search>' +
'<filter string="Foo1" domain="[ [\'foo\', \'=\', \'1\'] ]"/>' +
'<filter name="foo2" string="Foo2" domain="[ [\'foo\', \'=\', \'2\'] ]"/>' +
'<filter string="Foo3" domain="[ [\'foo\', \'=\', \'3\'] ]"/>' +
'</search>',
};
});
odoo.define_section('search.filters', ['web.search_inputs', 'web.SearchView', 'web.core', 'web.data', 'web.data_manager'], function (test, mock) {
function fields_view_get () {
// view with a single group of filters
return {
type: 'search',
fields: {},
arch: '<search>' +
'<filter string="Foo1" domain="[ [\'foo\', \'=\', \'1\'] ]"/>' +
'<filter name="foo2" string="Foo2" domain="[ [\'foo\', \'=\', \'2\'] ]"/>' +
'<filter string="Foo3" domain="[ [\'foo\', \'=\', \'3\'] ]"/>' +
'</search>',
};
}
test('drawn', function (assert) {
setup();
assert.expect(3);
var view = makeSearchView(this);
var view = makeSearchView(this, {}, {}, {}, fields_view_get());
var $fix = $('#qunit-fixture');
return view.appendTo($fix)
@@ -1237,10 +1231,9 @@ odoo.define_section('search.filters', ['web.search_inputs', 'web.SearchView', 'w
var $filters = view.$buttons.find('.filters-menu li'),
$favorites = view.$buttons.find('.o_favorites_menu li'),
$groupby = view.$buttons.find('.group-by-menu li');
// 3 filters, 1 separator, 1 button add filter,
// 1 filter condition menu, 1 apply button
assert.equal($filters.length, 7,
'filter menu should have 7 elements total');
// 3 filters, 1 separator, 1 button add filter, 1 apply button
assert.equal($filters.length, 6,
'filter menu should have 6 elements total');
// 1 divider, 1 save search button, 1 text input, 2 checkboxes,
// 1 save button, 3 add to dashboard things (a, input, button)
@@ -1249,9 +1242,9 @@ odoo.define_section('search.filters', ['web.search_inputs', 'web.SearchView', 'w
// assert.equal($favorites.length, 9,
// "favorites menu should have 9 elements");
// 1 divider, 1 add custom group button, 1 select groupby, 1 apply button,
assert.equal($groupby.length, 4,
"groupby menu should have 4 element");
// 1 divider, 1 add custom group button
assert.equal($groupby.length, 2,
"groupby menu should have 2 element");
assert.equal(_.str.strip($filters.find('a')[0].textContent), "Foo1",
"Text content of first filter option should match filter string");
});
@@ -1259,10 +1252,9 @@ odoo.define_section('search.filters', ['web.search_inputs', 'web.SearchView', 'w
test('click adding from empty query', function (assert, search_inputs) {
assert.expect(4);
setup();
var Filter = search_inputs.Filter;
var view = makeSearchView(this);
var view = makeSearchView(this, {}, {}, {}, fields_view_get());
var $fix = $('#qunit-fixture');
return view.appendTo($fix)
.done(function () {
@@ -1281,8 +1273,7 @@ odoo.define_section('search.filters', ['web.search_inputs', 'web.SearchView', 'w
test('click adding from existing query', function (assert) {
assert.expect(4);
setup();
var view = makeSearchView(this, {}, {foo2: true});
var view = makeSearchView(this, {}, {foo2: true}, {}, fields_view_get());
var $fix = $('#qunit-fixture');
return view.appendTo($fix)
@@ -1304,10 +1295,9 @@ odoo.define_section('search.filters', ['web.search_inputs', 'web.SearchView', 'w
test('click removing from query', function (assert) {
assert.expect(4);
setup();
var calls = 0;
var view = makeSearchView(this, {}, {foo2: true});
var view = makeSearchView(this, {}, {foo2: true}, {}, fields_view_get());
view.on('search_data', null, function () {
++calls;
});
@@ -1325,24 +1315,21 @@ odoo.define_section('search.filters', ['web.search_inputs', 'web.SearchView', 'w
});
});
odoo.define_section('search.groupby', ['web.search_inputs', 'web.SearchView', 'web.core', 'web.data'], function (test, mock) {
odoo.define_section('search.groupby', ['web.search_inputs', 'web.SearchView', 'web.core', 'web.data', 'web.data_manager'], function (test, mock) {
test('basic', ['web.FavoriteMenu', 'web.FilterMenu', 'web.GroupByMenu'], function (assert, search_inputs, SearchView, core, data, FavoriteMenu, FilterMenu, GroupByMenu) {
test('basic', ['web.FavoriteMenu', 'web.FilterMenu', 'web.GroupByMenu'], function (assert, search_inputs, SearchView, core, data, data_manager, FavoriteMenu, FilterMenu, GroupByMenu) {
assert.expect(12);
mock.add('dummy.model:fields_view_get', function () {
return {
type: 'search',
fields: {},
arch: [
'<search>',
'<filter string="Foo" context="{\'group_by\': \'foo\'}"/>',
'<filter string="Bar" context="{\'group_by\': \'bar\'}"/>',
'<filter string="Baz" context="{\'group_by\': \'baz\'}"/>',
'</search>'
].join(''),
};
var view = makeSearchView(this, {}, {}, {}, {
type: 'search',
fields: {},
arch: [
'<search>',
'<filter string="Foo" context="{\'group_by\': \'foo\'}"/>',
'<filter string="Bar" context="{\'group_by\': \'bar\'}"/>',
'<filter string="Baz" context="{\'group_by\': \'baz\'}"/>',
'</search>'
].join(''),
});
var view = makeSearchView(this);
var $fix = $('#qunit-fixture');
return view.appendTo($fix)
.done(function () {
@@ -1378,22 +1365,19 @@ odoo.define_section('search.groupby', ['web.search_inputs', 'web.SearchView', 'w
test('unified multiple groupby groups', function (assert, search_inputs, SearchView, core, data) {
assert.expect(3);
mock.add('dummy.model:fields_view_get', function () {
return {
type: 'search',
fields: {},
arch: [
'<search>',
'<filter string="Foo" context="{\'group_by\': \'foo\'}"/>',
'<separator/>',
'<filter string="Bar" context="{\'group_by\': \'bar\'}"/>',
'<separator/>',
'<filter string="Baz" context="{\'group_by\': \'baz\'}"/>',
'</search>'
].join(''),
};
var view = makeSearchView(this, {}, {}, {}, {
type: 'search',
fields: {},
arch: [
'<search>',
'<filter string="Foo" context="{\'group_by\': \'foo\'}"/>',
'<separator/>',
'<filter string="Bar" context="{\'group_by\': \'bar\'}"/>',
'<separator/>',
'<filter string="Baz" context="{\'group_by\': \'baz\'}"/>',
'</search>'
].join(''),
});
var view = makeSearchView(this);
var $fix = $('#qunit-fixture');
return view.appendTo($fix)
@@ -1415,7 +1399,7 @@ odoo.define_section('search.groupby', ['web.search_inputs', 'web.SearchView', 'w
});
});
odoo.define_section('search.filters.saved', ['web.search_inputs', 'web.SearchView', 'web.core', 'web.data'], function (test, mock) {
odoo.define_section('search.filters.saved', ['web.search_inputs', 'web.SearchView', 'web.core', 'web.data', 'web.data_manager'], function (test, mock) {
test('checkboxing', function (assert) {
assert.expect(6);
@@ -1531,7 +1515,7 @@ odoo.define_section('search.filters.saved', ['web.search_inputs', 'web.SearchVie
});
});
odoo.define_section('search.advanced', ['web.search_inputs', 'web.SearchView', 'web.core', 'web.data'], function (test, mock) {
odoo.define_section('search.advanced', ['web.search_inputs', 'web.SearchView', 'web.core', 'web.data', 'web.data_manager'], function (test, mock) {
test('single-advanced', function (assert) {
assert.expect(6);
+2
View File
@@ -115,6 +115,8 @@
<script type="text/javascript" src="/web/static/src/js/framework/misc.js"></script>
<script type="text/javascript" src="/web/static/src/js/framework/session_instance.js"></script>
<script type="text/javascript" src="/web/static/src/js/framework/crash_manager.js"></script>
<script type="text/javascript" src="/web/static/src/js/framework/data_manager.js"></script>
<script type="text/javascript" src="/web/static/src/js/services/data_manager.js"></script>
<script type="text/javascript" src="/web/static/src/js/widgets/change_password.js"></script>
<script type="text/javascript" src="/web/static/src/js/widgets/debug_manager.js"></script>
<script type="text/javascript" src="/web/static/src/js/widgets/data_export.js"></script>
@@ -61,65 +61,32 @@ function isNullOrUndef(value) {
}
var CalendarView = View.extend({
template: "CalendarView",
defaults: _.extend({}, View.prototype.defaults, {
confirm_on_delete: true,
}),
display_name: _lt('Calendar'),
icon: 'fa-calendar',
quick_create_instance: widgets.QuickCreate,
template: "CalendarView",
init: function (parent, dataset, view_id, options) {
this._super(parent);
this.ready = $.Deferred();
this.set_default_options(options);
this.dataset = dataset;
this.model = dataset.model;
this.fields_view = {};
this.view_id = view_id;
this.view_type = 'calendar';
init: function () {
this._super.apply(this, arguments);
this.color_map = {};
this.range_start = null;
this.range_stop = null;
this.selected_filters = [];
this.info_fields = [];
this.title = (this.options.action)? this.options.action.name : '';
this.shown = $.Deferred();
},
set_default_options: function(options) {
this._super(options);
_.defaults(this.options, {
confirm_on_delete: true
});
},
destroy: function() {
if (this.$calendar) {
this.$calendar.fullCalendar('destroy');
}
if (this.$small_calendar) {
this.$small_calendar.datepicker('destroy');
}
this._super.apply(this, arguments);
},
view_loading: function (fv) {
/* xml view calendar options */
var attrs = fv.arch.attrs,
self = this;
this.fields_view = fv;
this.$calendar = this.$(".o_calendar_widget");
this.info_fields = [];
var attrs = this.fields_view.arch.attrs;
if (!attrs.date_start) {
throw new Error(_t("Calendar view has not defined 'date_start' attribute."));
}
this.$el.addClass(attrs['class']);
this.name = fv.name || attrs.string;
this.view_id = fv.view_id;
this.fields = this.fields_view.fields;
this.name = this.fields_view.name || attrs.string;
this.mode = attrs.mode; // one of month, week or day
this.date_start = attrs.date_start; // Field name of starting date field
this.date_delay = attrs.date_delay; // duration
@@ -129,7 +96,7 @@ var CalendarView = View.extend({
this.attendee_people = attrs.attendee;
// Check whether the date field is editable (i.e. if the events can be dragged and dropped)
this.editable = !this.options.read_only_mode && !this.fields_view.fields[this.date_start].readonly;
this.editable = !this.options.read_only_mode && !this.fields[this.date_start].readonly;
//if quick_add = False, we don't allow quick_add
//if quick_add = not specified in view, we use the default quick_create_instance
@@ -155,7 +122,7 @@ var CalendarView = View.extend({
this.colorIsAttendee = !(isNullOrUndef(attrs.color_is_attendee) || !_.str.toBoolElse(attrs.color_is_attendee, true));
// if we have not sidebar, (eg: Dashboard), we don't use the filter "coworkers"
if (isNullOrUndef(self.options.sidebar)) {
if (isNullOrUndef(this.options.sidebar)) {
this.useContacts = false;
this.colorIsAttendee = false;
this.attendee_people = undefined;
@@ -203,32 +170,38 @@ var CalendarView = View.extend({
}
}
this.fields = fv.fields;
for (var fld = 0; fld < fv.arch.children.length; fld++) {
this.info_fields.push(fv.arch.children[fld].attrs.name);
for (var fld = 0; fld < this.fields_view.arch.children.length; fld++) {
this.info_fields.push(this.fields_view.arch.children[fld].attrs.name);
}
self.shown.done(this._do_show_init.bind(this));
var edit_check = new Model(this.dataset.model)
.call("check_access_rights", ["write", false])
.then(function (write_right) {
self.write_right = write_right;
});
var init = new Model(this.dataset.model)
.call("check_access_rights", ["create", false])
.then(function (create_right) {
self.create_right = create_right;
self.ready.resolve();
self.trigger('calendar_view_loaded', fv);
});
return $.when(edit_check, init);
},
_do_show_init: function () {
willStart: function () {
var self = this;
var write_def = this.dataset.call("check_access_rights", ["write", false]);
var create_def = this.dataset.call("check_access_rights", ["create", false]);
return $.when(write_def, create_def, this._super()).then(function (write, create) {
self.write_right = write;
self.create_right = create;
});
},
start: function () {
this.$calendar = this.$(".o_calendar_widget");
this.$el.addClass(this.fields_view.arch.attrs.class);
this.shown.done(this._do_show_init.bind(this));
return this._super();
},
destroy: function() {
if (this.$calendar) {
this.$calendar.fullCalendar('destroy');
}
if (this.$small_calendar) {
this.$small_calendar.datepicker('destroy');
}
this._super.apply(this, arguments);
},
_do_show_init: function () {
this.init_calendar().then(function() {
$(window).trigger('resize');
self.trigger('calendar_view_loaded', self.fields_view);
});
},
/**
@@ -876,9 +849,9 @@ var CalendarView = View.extend({
}
}
else {
var dialog = new form_common.FormViewDialog(this, {
res_model: this.dataset.model,
res_id: parseInt(id).toString() == id ? parseInt(id) : id,
new form_common.FormViewDialog(this, {
res_model: this.model,
res_id: parseInt(id).toString() === id ? parseInt(id) : id,
context: this.dataset.get_context(),
title: title,
view_id: +this.open_popup_action,
+10 -31
View File
@@ -2,7 +2,6 @@ odoo.define('web_calendar.widgets', function(require) {
"use strict";
var core = require('web.core');
var data = require('web.data');
var Dialog = require('web.Dialog');
var form_common = require('web.form_common');
var Widget = require('web.Widget');
@@ -69,12 +68,12 @@ var QuickCreate = Dialog.extend({
});
this.$input = this.$('input').keyup(function enterHandler (e) {
if(e.keyCode == $.ui.keyCode.ENTER) {
if(e.keyCode === $.ui.keyCode.ENTER) {
self.$input.off('keyup', enterHandler);
if (!self.quick_add()){
self.$input.on('keyup', enterHandler);
}
} else if (e.keyCode == $.ui.keyCode.ESCAPE && self._buttons) {
} else if (e.keyCode === $.ui.keyCode.ESCAPE && self._buttons) {
self.close();
}
});
@@ -114,20 +113,6 @@ var QuickCreate = Dialog.extend({
});
},
/**
* Show full form popup
*/
get_form_popup_infos: function() {
var parent = this.getParent();
var infos = {
view_id: false,
title: this.name,
};
if (!_.isUndefined(parent) && !(_.isUndefined(parent.ViewManager))) {
infos.view_id = parent.ViewManager.get_view_id('form');
}
return infos;
},
slow_create: function(_data) {
//if all day, we could reset time to display 00:00:00
@@ -140,26 +125,21 @@ var QuickCreate = Dialog.extend({
defaults['default_' + field_name] = val;
});
var pop_infos = self.get_form_popup_infos();
var context = new data.CompoundContext(this.dataset.context, defaults);
var pop = new form_common.FormViewDialog(this, {
res_model: this.dataset.model,
context: this.dataset.get_context(defaults),
title: this.get_title(),
disable_multiple_selection: true,
view_id: pop_infos.view_id,
// Ensuring we use ``self.dataset`` and DO NOT create a new one.
create_function: function(data, options) {
return self.dataset.create(data, options).done(function(r) {
}).fail(function (r, event) {
return self.dataset.create(data, options).fail(function (r) {
if (!r.data.message) { //else manage by openerp
throw new Error(r);
}
});
},
read_function: function(id, fields, options) {
return self.dataset.read_ids.apply(self.dataset, arguments).done(function() {
}).fail(function (r, event) {
read_function: function() {
return self.dataset.read_ids.apply(self.dataset, arguments).fail(function (r) {
if (!r.data.message) { //else manage by openerp
throw new Error(r);
}
@@ -171,7 +151,7 @@ var QuickCreate = Dialog.extend({
def.resolve();
}
});
pop.on('create_completed', self, function(id) {
pop.on('create_completed', self, function() {
created = true;
self.trigger('slowadded');
});
@@ -231,13 +211,12 @@ var SidebarFilter = Widget.extend({
this.$el.html(QWeb.render('CalendarView.sidebar.responsible', { filters: filters }));
},
filter_click: function(e) {
var self = this;
if (self.view.all_filters[0] && e.target.value == self.view.all_filters[0].value) {
self.view.all_filters[0].is_checked = e.target.checked;
if (this.view.all_filters[0] && e.target.value === this.view.all_filters[0].value) {
this.view.all_filters[0].is_checked = e.target.checked;
} else {
self.view.all_filters[e.target.value].is_checked = e.target.checked;
this.view.all_filters[e.target.value].is_checked = e.target.checked;
}
self.view.$calendar.fullCalendar('refetchEvents');
this.view.$calendar.fullCalendar('refetchEvents');
},
select_previous: function(e) {
$(e.target).siblings('input').trigger('click');
+31 -43
View File
@@ -18,45 +18,17 @@ var QWeb = core.qweb;
var DiagramView = View.extend({
display_name: _lt('Diagram'),
icon: 'fa-code-fork',
view_type: 'diagram',
searchable: false,
multi_record: false,
init: function(parent, dataset, view_id, options) {
this._super(parent);
this.set_default_options(options);
this.view_manager = parent;
this.dataset = dataset;
this.model = this.dataset.model;
this.view_id = view_id;
searchable: false,
init: function() {
this._super.apply(this, arguments);
this.domain = this.dataset._domain || [];
this.context = {};
this.ids = this.dataset.ids;
},
view_loading: function(r) {
if (window.Raphael) {
return this.load_diagram(r);
if (this.ids && this.ids.length) {
this.id = this.ids[this.dataset.index || 0];
}
var self = this;
return $.when(
ajax.loadJS('/web_diagram/static/lib/js/jquery.mousewheel.js'),
ajax.loadJS('/web_diagram/static/lib/js/raphael.js')).then(function () {
return self.load_diagram(r);
});
},
toTitleCase: function(str) {
return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
},
load_diagram: function(result) {
var self = this;
if(this.ids && this.ids.length) {
this.id = this.ids[self.dataset.index || 0];
}
this.fields_view = result;
this.view_id = this.fields_view.view_id;
this.fields = this.fields_view.fields;
this.nodes = this.fields_view.arch.children[0];
this.connectors = this.fields_view.arch.children[1];
@@ -65,19 +37,35 @@ var DiagramView = View.extend({
this.labels = _.filter(this.fields_view.arch.children, function(label) {
return label.tag === "label";
});
},
this.$el.html(QWeb.render("DiagramView", {'widget': self}));
this.$el.addClass('o_diagram_view').addClass(this.fields_view.arch.attrs['class']);
willStart: function() {
if (window.Raphael) {
return $.when();
}
return $.when(
ajax.loadJS('/web_diagram/static/lib/js/jquery.mousewheel.js'),
ajax.loadJS('/web_diagram/static/lib/js/raphael.js')
);
},
start: function() {
var self = this;
this.$el.html(QWeb.render("DiagramView", {'widget': this}));
this.$el.addClass('o_diagram_view').addClass(this.fields_view.arch.attrs.class);
_.each(self.labels,function(label){
_.each(this.labels,function(label){
self.$('.o_diagram_header').append($('<span>').html(label.attrs.string));
});
var diagram_info;
if(this.id) {
self.get_diagram_info();
diagram_info = this.get_diagram_info();
}
this.trigger('diagram_view_loaded', result);
return $.when(this._super(), diagram_info);
},
toTitleCase: function(str) {
return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
},
get_diagram_info: function() {
@@ -112,7 +100,7 @@ var DiagramView = View.extend({
params.connectors_fields.push(self.fields[conn.attrs.name]['string']|| this.toTitleCase(conn.attrs.name));
params.connectors.push(conn.attrs.name);
});
this.rpc(
return this.rpc(
'/web_diagram/diagram/get_diagram_info',params).done(function(result) {
self.draw_diagram(result);
}
@@ -277,7 +265,7 @@ var DiagramView = View.extend({
var title = _t('Activity');
var pop = new form_common.FormViewDialog(self, {
res_model: self.node,
domain: self.dataset.domain,
domain: self.domain,
context: self.context || self.dataset.context,
title: _t("Create:") + title,
disable_multiple_selection: true,
@@ -321,7 +309,7 @@ var DiagramView = View.extend({
var title = _t('Transition');
var pop = new form_common.FormViewDialog(self, {
res_model: self.connector,
domain: this.dataset.domain,
domain: this.domain,
context: this.context || this.dataset.context,
title: _t("Create:") + title,
disable_multiple_selection: true,
+37 -44
View File
@@ -26,11 +26,6 @@ var fields_registry = kanban_widgets.registry;
var KanbanView = View.extend({
accesskey: "K",
className: "o_kanban_view",
display_name: _lt("Kanban"),
icon: 'fa-th-large',
mobile_friendly: true,
view_type: "kanban",
custom_events: {
'kanban_record_open': 'open_record',
'kanban_record_edit': 'edit_record',
@@ -49,27 +44,30 @@ var KanbanView = View.extend({
'kanban_load_more': 'load_more',
'kanban_call_method': 'call_method',
},
defaults: _.extend(View.prototype.defaults, {
quick_creatable: true,
creatable: true,
create_text: undefined,
read_only_mode: false,
confirm_on_delete: true,
}),
display_name: _lt("Kanban"),
icon: 'fa-th-large',
mobile_friendly: true,
init: function (parent, dataset, view_id, options) {
this._super(parent, dataset, view_id, options);
_.defaults(this.options, {
"quick_creatable": true,
"creatable": true,
"create_text": undefined,
"read_only_mode": false,
"confirm_on_delete": true,
});
init: function () {
this._super.apply(this, arguments);
// qweb setup
this.qweb = new QWeb2.Engine();
this.qweb.debug = session.debug;
this.qweb.default_dict = _.clone(QWeb.default_dict);
this.model = this.dataset.model;
this.limit = options.limit || 40;
this.limit = this.options.limit || 40;
this.fields_keys = _.keys(this.fields_view.fields);
this.grouped = undefined;
this.group_by_field = undefined;
this.default_group_by = undefined;
this.default_group_by = this.fields_view.arch.attrs.default_group_by;
this.grouped_by_m2o = undefined;
this.relation = undefined;
this.is_empty = undefined;
@@ -77,39 +75,39 @@ var KanbanView = View.extend({
this.m2m_context = {};
this.widgets = [];
this.data = undefined;
this.model = dataset.model;
this.quick_creatable = options.quick_creatable;
this.no_content_msg = options.action && (options.action.get_empty_list_help || options.action.help);
this.quick_creatable = this.options.quick_creatable;
this.no_content_msg = this.options.action &&
(this.options.action.get_empty_list_help || this.options.action.help);
this.search_orderer = new utils.DropMisordered();
},
view_loading: function(fvg) {
this.$el.addClass(fvg.arch.attrs.class);
this.fields_view = fvg;
this.default_group_by = fvg.arch.attrs.default_group_by;
this.fields_keys = _.keys(this.fields_view.fields);
// use default order if defined in xml description
var default_order = this.fields_view.arch.attrs.default_order;
if (!this.dataset._sort.length && default_order) {
this.dataset.set_sort(default_order.split(','));
}
},
willStart: function() {
// add qweb templates
for (var i=0, ii=this.fields_view.arch.children.length; i < ii; i++) {
var child = this.fields_view.arch.children[i];
if (child.tag === "templates") {
transform_qweb_template(child, fvg, this.many2manys);
transform_qweb_template(child, this.fields_view, this.many2manys);
this.qweb.add_template(utils.json_node_to_xml(child));
break;
} else if (child.tag === 'field') {
var ftype = child.attrs.widget || this.fields_view.fields[child.attrs.name].type;
if(ftype == "many2many" && "context" in child.attrs) {
if(ftype === "many2many" && "context" in child.attrs) {
this.m2m_context[child.attrs.name] = child.attrs.context;
}
}
}
this.trigger('kanban_view_loaded');
return this._super();
},
start: function() {
this.$el.addClass(this.fields_view.arch.attrs.class);
return this._super();
},
do_search: function(domain, context, group_by) {
@@ -254,7 +252,7 @@ var KanbanView = View.extend({
var is_empty = true;
return $.when.apply(null, _.map(groups, function (group) {
var def = $.when([]);
var dataset = new data.DataSetSearch(self, self.dataset.model,
var dataset = new data.DataSetSearch(self, self.model,
new data.CompoundContext(self.dataset.get_context(), group.model.context()), group.model.domain());
if (self.dataset._sort) {
dataset.set_sort(self.dataset._sort);
@@ -303,16 +301,11 @@ var KanbanView = View.extend({
* $node may be undefined, in which case the ListView inserts them into this.options.$buttons
*/
render_buttons: function($node) {
var display = false;
if (this.options.action_buttons !== false) {
display = this.is_action_enabled('create');
} else if (!this.view_id && !this.options.read_only_mode) {
display = this.is_action_enabled('write') || this.is_action_enabled('create');
if (this.options.action_buttons !== false && this.is_action_enabled('create')) {
this.$buttons = $(QWeb.render("KanbanView.buttons", {'widget': this}));
this.$buttons.on('click', 'button.o-kanban-button-new', this.add_record.bind(this));
this.$buttons.appendTo($node);
}
this.$buttons = $(QWeb.render("KanbanView.buttons", {'widget': this, display: display}));
this.$buttons.on('click', 'button.o-kanban-button-new', this.add_record.bind(this));
this.$buttons.appendTo($node);
},
render_pager: function($node, options) {
@@ -499,7 +492,7 @@ var KanbanView = View.extend({
context.set_eval_context({
active_id: event.target.id,
active_ids: [event.target.id],
active_model: this.dataset.model,
active_model: this.model,
});
this.do_execute_action(event.data, this.dataset, event.target.id).then(function () {
self.reload_record(event.target);
@@ -660,7 +653,7 @@ var KanbanView = View.extend({
event.preventDefault();
var popup = new form_common.SelectCreatePopup(this);
popup.select_element(
self.dataset.model,
self.model,
{
title: _t("Create: "),
initial_view: "form",
@@ -688,7 +681,7 @@ var KanbanView = View.extend({
model.call('create', [{name: event.data.value}], {
context: this.search_context,
}).then(function (id) {
var dataset = new data.DataSetSearch(self, self.dataset.model, self.dataset.get_context(), []);
var dataset = new data.DataSetSearch(self, self.model, self.dataset.get_context(), []);
var group_data = {
records: [],
title: event.data.value,
@@ -1,13 +1,11 @@
<template>
<t t-name="KanbanView.buttons">
<t t-if="display">
<div>
<button type="button" class="btn btn-primary btn-sm o-kanban-button-new" accesskey="C">
<t t-esc="widget.options.create_text || _t('Create')"/>
</button>
</div>
</t>
<div>
<button type="button" class="btn btn-primary btn-sm o-kanban-button-new" accesskey="C">
<t t-esc="widget.options.create_text || _t('Create')"/>
</button>
</div>
</t>
<t t-name="KanbanView.Group">
+32
View File
@@ -1532,6 +1532,38 @@ class BaseModel(object):
return view
def load_views(self, cr, uid, views, options=None, context=None):
""" Returns the fields_views of given views, and optionally filters and fields.
:param views: list of [view_id, view_type]
:param options.toolbar: true to include contextual actions when loading fields_views
:param options.load_filters: true to return the model's filters
:param options.action_id: id of the action to get the filters
:param options.load_fields: true to load the model's fields
:return: dictionary with fields_views, filters and fields
"""
if options is None:
options = {}
if context is None:
context = {}
result = {}
toolbar = options.get('toolbar')
result['fields_views'] = {
v_type: self.fields_view_get(cr, uid, v_id, v_type if v_type != 'list' else 'tree',
toolbar=toolbar if v_type != 'search' else False,
context=context)
for [v_id, v_type] in views
}
if options.get('load_filters'):
result['filters'] = self.pool['ir.filters'].get_filters(cr, uid, self._name, options.get('action_id'), context=context)
if options.get('load_fields'):
result['fields'] = self.fields_get(cr, uid, context=context)
return result
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
""" fields_view_get([view_id | view_type='form'])