From 82088b11e137fa44a4e68f9d1be60ee4718fc4e7 Mon Sep 17 00:00:00 2001 From: Holger Brunn Date: Tue, 20 Sep 2016 21:34:55 +0200 Subject: [PATCH] [IMP] web_menu_navbar_needaction: reflow, new features (#265) [IMP] web_menu_navbar_needaction: Several improvements: * Make the menu reflow after updating needactions * Allow to disable needaction completely or to set a custom domain * Support for clicking the number to end up on the first action * No need to block the UI for our request * Don't crash on corner cases, filter out search defaults from context, disable custom filters * Allow to define needaction domains for any menu * Support server actions * Support models implementing the function, but not the interface * Show a main menu's child needaction counters --- web_menu_navbar_needaction/README.rst | 3 + web_menu_navbar_needaction/__openerp__.py | 1 + .../models/ir_ui_menu.py | 108 +++++++++++++++++- .../src/css/web_menu_navbar_needaction.css | 4 + .../src/js/web_menu_navbar_needaction.js | 57 +++++++-- .../views/ir_ui_menu.xml | 16 +++ 6 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 web_menu_navbar_needaction/views/ir_ui_menu.xml diff --git a/web_menu_navbar_needaction/README.rst b/web_menu_navbar_needaction/README.rst index 81d54813..9e663d1a 100644 --- a/web_menu_navbar_needaction/README.rst +++ b/web_menu_navbar_needaction/README.rst @@ -1,5 +1,6 @@ .. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg :alt: License: AGPL-3 + ================================ Needaction counters in main menu ================================ @@ -15,6 +16,8 @@ To configure the update frequency, set the configuration parameter `web_menu_nav To disable updates, set the parameter to 0. +For more fine-grained control over the menus, you can turn off needaction for a menu by setting its field `needaction` to `False`, or in case you need a different needaction domain, set `needaction_domain` on the menuitem to the domain you need. A side effect of this is that you can use this addon to add needaction capabilities to any model, independent of it implementing the respective mixin by simply filling in your domain there. + Usage ===== diff --git a/web_menu_navbar_needaction/__openerp__.py b/web_menu_navbar_needaction/__openerp__.py index 4f54faf9..d631363c 100644 --- a/web_menu_navbar_needaction/__openerp__.py +++ b/web_menu_navbar_needaction/__openerp__.py @@ -29,6 +29,7 @@ 'mail', ], "data": [ + "views/ir_ui_menu.xml", "data/ir_config_parameter.xml", 'views/templates.xml', ], diff --git a/web_menu_navbar_needaction/models/ir_ui_menu.py b/web_menu_navbar_needaction/models/ir_ui_menu.py index 8cc1b553..c5ab8ac6 100644 --- a/web_menu_navbar_needaction/models/ir_ui_menu.py +++ b/web_menu_navbar_needaction/models/ir_ui_menu.py @@ -17,25 +17,123 @@ # along with this program. If not, see . # ############################################################################## -from openerp import models, api +import operator +from openerp import _, models, api, fields +from openerp.tools.safe_eval import safe_eval +from openerp.exceptions import Warning as UserError +from openerp.osv import expression class IrUiMenu(models.Model): _inherit = 'ir.ui.menu' + needaction = fields.Boolean( + help='Set to False to disable needaction for specific menu items', + default=True) + needaction_domain = fields.Text( + help='If your menu item needs a different domain, set it here. It ' + 'will override the model\'s needaction domain completely.') + @api.multi def get_navbar_needaction_data(self): result = {} for this in self: count_per_model = {} + action_menu = self.env['ir.ui.menu'].browse([]) + if not this.needaction: + continue for menu_id, needaction in self.search( [('id', 'child_of', this.ids)])._filter_visible_menus()\ .get_needaction_data().iteritems(): if needaction['needaction_enabled']: - model = self.env['ir.ui.menu'].browse(menu_id).action\ - .res_model + menu = self.env['ir.ui.menu'].browse(menu_id) + model = menu._get_needaction_model() count_per_model[model] = max( count_per_model.get(model), - needaction['needaction_counter']) - result[this.id] = sum(count_per_model.itervalues()) + needaction['needaction_counter'] + ) + if needaction['needaction_counter'] and not action_menu: + action_menu = menu + result[this.id] = { + 'count': sum(count_per_model.itervalues()), + } + if action_menu: + result[this.id].update({ + 'action_id': action_menu.action and + action_menu.action.id or None, + 'action_domain': action_menu._eval_needaction_domain(), + }) + return result + + @api.multi + def get_needaction_data(self): + result = super(IrUiMenu, self).get_needaction_data() + for this in self.sorted(operator.itemgetter('parent_left'), True): + data = result[this.id] + if data['needaction_enabled'] or this.needaction and\ + this.needaction_domain: + if not this.needaction: + data['needaction_enabled'] = False + data['needaction_counter'] = 0 + continue + if this.needaction_domain and\ + this._get_needaction_model() is not None: + data['needaction_enabled'] = True + data['needaction_counter'] = this._get_needaction_model()\ + .search_count(this._eval_needaction_domain()) + if not data['needaction_enabled'] and this.needaction and\ + this.child_id and this.parent_id and this.parent_id.parent_id: + # if the user didn't turn it off, show counters for submenus + # but only from the 3rd level (the first that is closed by + # default) + for child in this.child_id: + data['needaction_counter'] += result.get(child.id, {}).get( + 'needaction_counter', 0) + data['needaction_enabled'] = bool(data['needaction_counter']) return result + + @api.multi + def _eval_needaction_domain(self): + self.ensure_one() + eval_context = { + 'uid': self.env.user.id, + 'user': self.env.user, + } + if self.needaction_domain: + return safe_eval(self.needaction_domain, locals_dict=eval_context) + model = self._get_needaction_model() + if model is None or not hasattr(model, '_needaction_domain_get'): + return [] + return expression.AND([ + safe_eval( + 'domain' in self.action._fields and self.action.domain or '[]', + locals_dict=eval_context), + model._needaction_domain_get(), + ]) + + @api.multi + def _get_needaction_model(self): + if not self.action: + return None + model = None + if 'res_model' in self.action._fields: + model = self.action.res_model + elif 'model_id' in self.action._fields: + model = self.action.model_id.model + if model in self.env.registry: + return self.env[model] + return None + + @api.constrains('needaction_domain') + @api.multi + def _check_needaction_domain(self): + for this in self: + try: + expression.AND([ + this._eval_needaction_domain(), + expression.TRUE_DOMAIN, + ]) + except Exception as ex: + raise UserError( + _('Cannot evaluate %s to a search domain:\n%s') % + (self.needaction_domain, ex)) diff --git a/web_menu_navbar_needaction/static/src/css/web_menu_navbar_needaction.css b/web_menu_navbar_needaction/static/src/css/web_menu_navbar_needaction.css index 259502de..fc858479 100644 --- a/web_menu_navbar_needaction/static/src/css/web_menu_navbar_needaction.css +++ b/web_menu_navbar_needaction/static/src/css/web_menu_navbar_needaction.css @@ -2,3 +2,7 @@ { margin-left: .3em; } +#oe_main_menu_navbar .badge:hover +{ + transform: scale(1.1); +} diff --git a/web_menu_navbar_needaction/static/src/js/web_menu_navbar_needaction.js b/web_menu_navbar_needaction/static/src/js/web_menu_navbar_needaction.js index 799b6120..dcb26826 100644 --- a/web_menu_navbar_needaction/static/src/js/web_menu_navbar_needaction.js +++ b/web_menu_navbar_needaction/static/src/js/web_menu_navbar_needaction.js @@ -50,29 +50,72 @@ openerp.web_menu_navbar_needaction = function(instance) .map(function() { return parseInt(jQuery(this).attr('data-menu')); }) .get(); return new instance.web.Model('ir.ui.menu') - .call('get_navbar_needaction_data', [this.navbar_menu_ids]) + .call('get_navbar_needaction_data', [this.navbar_menu_ids], + {}, {shadow: true}) .then(this.proxy(this.process_navbar_needaction)); }, process_navbar_needaction: function(data) { var self = this; - _.each(data, function (needaction_count, menu_id) + _.each(data, function (needaction_data, menu_id) { var $item = self.$el.parents('body').find( _.str.sprintf('#oe_main_menu_navbar a[data-menu="%s"]', menu_id)); - if(!$item.length) + if(!$item.length || _.isEmpty(needaction_data)) { return; } $item.find('.badge').remove(); - if(needaction_count) + if(needaction_data.count) { - $item.append( - instance.web.qweb.render("Menu.needaction_counter", - {widget : {needaction_counter: needaction_count}})); + var $counter = jQuery( + instance.web.qweb.render("Menu.needaction_counter", + { + widget: { + needaction_counter: needaction_data.count, + } + })) + .appendTo($item); + if(needaction_data.action_id) + { + $counter.click(function(ev) + { + var parent = self.getParent(); + ev.stopPropagation(); + ev.preventDefault(); + return parent.menu_dm.add( + self.rpc('/web/action/load', { + action_id: needaction_data.action_id, + })) + .then(function(action) + { + return parent.action_mutex.exec(function() + { + action.domain = needaction_data.action_domain; + action.context = new instance.web.CompoundContext( + action.context || {} + ); + action.context = instance.web.pyeval.eval( + 'context', action.context + ); + _.each(_.keys(action.context), function(key) + { + if(key.startsWith('search_default')) + { + delete action.context[key]; + } + }); + return parent.action_manager.do_action( + action, {disable_custom_filters: true} + ); + }); + }); + }); + } } }); + instance.web.bus.trigger('resize'); }, }) diff --git a/web_menu_navbar_needaction/views/ir_ui_menu.xml b/web_menu_navbar_needaction/views/ir_ui_menu.xml new file mode 100644 index 00000000..22b82faa --- /dev/null +++ b/web_menu_navbar_needaction/views/ir_ui_menu.xml @@ -0,0 +1,16 @@ + + + + + ir.ui.menu + + + + + + + + + + +