[REM] shortcuts, deprecated by bookmarks
[odoo/odoo.git] / openerp / addons / base / ir / ir_ui_menu.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #    Copyright (C) 2010-2012 OpenERP SA (<http://openerp.com>).
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as
10 #    published by the Free Software Foundation, either version 3 of the
11 #    License, or (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 import base64
24 import re
25 import threading
26 from openerp.tools.safe_eval import safe_eval as eval
27 from openerp import tools
28 import openerp.modules
29 from openerp.osv import fields, osv
30 from openerp.tools.translate import _
31 from openerp import SUPERUSER_ID
32
33 MENU_ITEM_SEPARATOR = "/"
34
35 class ir_ui_menu(osv.osv):
36     _name = 'ir.ui.menu'
37
38     def __init__(self, *args, **kwargs):
39         self.cache_lock = threading.RLock()
40         self._cache = {}
41         super(ir_ui_menu, self).__init__(*args, **kwargs)
42         self.pool.get('ir.model.access').register_cache_clearing_method(self._name, 'clear_cache')
43
44     def clear_cache(self):
45         with self.cache_lock:
46             # radical but this doesn't frequently happen
47             if self._cache:
48                 # Normally this is done by openerp.tools.ormcache
49                 # but since we do not use it, set it by ourself.
50                 self.pool._any_cache_cleared = True
51             self._cache = {}
52
53     def _filter_visible_menus(self, cr, uid, ids, context=None):
54         """Filters the give menu ids to only keep the menu items that should be
55            visible in the menu hierarchy of the current user.
56            Uses a cache for speeding up the computation.
57         """
58         with self.cache_lock:
59             modelaccess = self.pool.get('ir.model.access')
60             user_groups = set(self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['groups_id'])['groups_id'])
61             result = []
62             for menu in self.browse(cr, uid, ids, context=context):
63                 # this key works because user access rights are all based on user's groups (cfr ir_model_access.check)
64                 key = (cr.dbname, menu.id, tuple(user_groups))
65                 if key in self._cache:
66                     if self._cache[key]:
67                         result.append(menu.id)
68                     #elif not menu.groups_id and not menu.action:
69                     #    result.append(menu.id)
70                     continue
71
72                 self._cache[key] = False
73                 if menu.groups_id:
74                     restrict_to_groups = [g.id for g in menu.groups_id]
75                     if not user_groups.intersection(restrict_to_groups):
76                         continue
77                     #result.append(menu.id)
78                     #self._cache[key] = True
79                     #continue
80
81                 if menu.action:
82                     # we check if the user has access to the action of the menu
83                     data = menu.action
84                     if data:
85                         model_field = { 'ir.actions.act_window':    'res_model',
86                                         'ir.actions.report.xml':    'model',
87                                         'ir.actions.wizard':        'model',
88                                         'ir.actions.server':        'model_id',
89                                       }
90
91                         field = model_field.get(menu.action._name)
92                         if field and data[field]:
93                             if not modelaccess.check(cr, uid, data[field], 'read', False):
94                                 continue
95                 else:
96                     # if there is no action, it's a 'folder' menu
97                     if not menu.child_id:
98                         # not displayed if there is no children
99                         continue
100
101                 result.append(menu.id)
102                 self._cache[key] = True
103             return result
104
105     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
106         if context is None:
107             context = {}
108
109         ids = super(ir_ui_menu, self).search(cr, uid, args, offset=0,
110             limit=None, order=order, context=context, count=False)
111
112         if not ids:
113             if count:
114                 return 0
115             return []
116
117         # menu filtering is done only on main menu tree, not other menu lists
118         if context.get('ir.ui.menu.full_list'):
119             result = ids
120         else:
121             result = self._filter_visible_menus(cr, uid, ids, context=context)
122
123         if offset:
124             result = result[long(offset):]
125         if limit:
126             result = result[:long(limit)]
127
128         if count:
129             return len(result)
130         return result
131
132     def name_get(self, cr, uid, ids, context=None):
133         res = []
134         for id in ids:
135             elmt = self.browse(cr, uid, id, context=context)
136             res.append((id, self._get_one_full_name(elmt)))
137         return res
138
139     def _get_full_name(self, cr, uid, ids, name=None, args=None, context=None):
140         if context is None:
141             context = {}
142         res = {}
143         for elmt in self.browse(cr, uid, ids, context=context):
144             res[elmt.id] = self._get_one_full_name(elmt)
145         return res
146
147     def _get_one_full_name(self, elmt, level=6):
148         if level<=0:
149             return '...'
150         if elmt.parent_id:
151             parent_path = self._get_one_full_name(elmt.parent_id, level-1) + MENU_ITEM_SEPARATOR
152         else:
153             parent_path = ''
154         return parent_path + elmt.name
155
156     def create(self, *args, **kwargs):
157         self.clear_cache()
158         return super(ir_ui_menu, self).create(*args, **kwargs)
159
160     def write(self, *args, **kwargs):
161         self.clear_cache()
162         return super(ir_ui_menu, self).write(*args, **kwargs)
163
164     def unlink(self, cr, uid, ids, context=None):
165         # Detach children and promote them to top-level, because it would be unwise to
166         # cascade-delete submenus blindly. We also can't use ondelete=set null because
167         # that is not supported when _parent_store is used (would silently corrupt it).
168         # TODO: ideally we should move them under a generic "Orphans" menu somewhere?
169         if isinstance(ids, (int, long)):
170             ids = [ids]
171         local_context = dict(context or {})
172         local_context['ir.ui.menu.full_list'] = True
173         direct_children_ids = self.search(cr, uid, [('parent_id', 'in', ids)], context=local_context)
174         if direct_children_ids:
175             self.write(cr, uid, direct_children_ids, {'parent_id': False})
176
177         result = super(ir_ui_menu, self).unlink(cr, uid, ids, context=context)
178         self.clear_cache()
179         return result
180
181     def copy(self, cr, uid, id, default=None, context=None):
182         ir_values_obj = self.pool.get('ir.values')
183         res = super(ir_ui_menu, self).copy(cr, uid, id, context=context)
184         datas=self.read(cr,uid,[res],['name'])[0]
185         rex=re.compile('\([0-9]+\)')
186         concat=rex.findall(datas['name'])
187         if concat:
188             next_num=int(concat[0])+1
189             datas['name']=rex.sub(('(%d)'%next_num),datas['name'])
190         else:
191             datas['name'] += '(1)'
192         self.write(cr,uid,[res],{'name':datas['name']})
193         ids = ir_values_obj.search(cr, uid, [
194             ('model', '=', 'ir.ui.menu'),
195             ('res_id', '=', id),
196             ])
197         for iv in ir_values_obj.browse(cr, uid, ids):
198             ir_values_obj.copy(cr, uid, iv.id, default={'res_id': res},
199                                context=context)
200         return res
201
202     def _action(self, cursor, user, ids, name, arg, context=None):
203         res = {}
204         ir_values_obj = self.pool.get('ir.values')
205         value_ids = ir_values_obj.search(cursor, user, [
206             ('model', '=', self._name), ('key', '=', 'action'),
207             ('key2', '=', 'tree_but_open'), ('res_id', 'in', ids)],
208             context=context)
209         values_action = {}
210         for value in ir_values_obj.browse(cursor, user, value_ids, context=context):
211             values_action[value.res_id] = value.value
212         for menu_id in ids:
213             res[menu_id] = values_action.get(menu_id, False)
214         return res
215
216     def _action_inv(self, cursor, user, menu_id, name, value, arg, context=None):
217         if context is None:
218             context = {}
219         ctx = context.copy()
220         if self.CONCURRENCY_CHECK_FIELD in ctx:
221             del ctx[self.CONCURRENCY_CHECK_FIELD]
222         ir_values_obj = self.pool.get('ir.values')
223         values_ids = ir_values_obj.search(cursor, user, [
224             ('model', '=', self._name), ('key', '=', 'action'),
225             ('key2', '=', 'tree_but_open'), ('res_id', '=', menu_id)],
226             context=context)
227         if value and values_ids:
228             ir_values_obj.write(cursor, user, values_ids, {'value': value}, context=ctx)
229         elif value:
230             # no values_ids, create binding
231             ir_values_obj.create(cursor, user, {
232                 'name': 'Menuitem',
233                 'model': self._name,
234                 'value': value,
235                 'key': 'action',
236                 'key2': 'tree_but_open',
237                 'res_id': menu_id,
238                 }, context=ctx)
239         elif values_ids:
240             # value is False, remove existing binding
241             ir_values_obj.unlink(cursor, user, values_ids, context=ctx)
242
243     def _get_icon_pict(self, cr, uid, ids, name, args, context):
244         res = {}
245         for m in self.browse(cr, uid, ids, context=context):
246             res[m.id] = ('stock', (m.icon,'ICON_SIZE_MENU'))
247         return res
248
249     def onchange_icon(self, cr, uid, ids, icon):
250         if not icon:
251             return {}
252         return {'type': {'icon_pict': 'picture'}, 'value': {'icon_pict': ('stock', (icon,'ICON_SIZE_MENU'))}}
253
254     def read_image(self, path):
255         if not path:
256             return False
257         path_info = path.split(',')
258         icon_path = openerp.modules.get_module_resource(path_info[0],path_info[1])
259         icon_image = False
260         if icon_path:
261             try:
262                 icon_file = tools.file_open(icon_path,'rb')
263                 icon_image = base64.encodestring(icon_file.read())
264             finally:
265                 icon_file.close()
266         return icon_image
267
268     def _get_image_icon(self, cr, uid, ids, names, args, context=None):
269         res = {}
270         for menu in self.browse(cr, uid, ids, context=context):
271             res[menu.id] = r = {}
272             for fn in names:
273                 fn_src = fn[:-5]    # remove _data
274                 r[fn] = self.read_image(menu[fn_src])
275
276         return res
277
278     def _get_needaction_enabled(self, cr, uid, ids, field_names, args, context=None):
279         """ needaction_enabled: tell whether the menu has a related action
280             that uses the needaction mechanism. """
281         res = dict.fromkeys(ids, False)
282         for menu in self.browse(cr, uid, ids, context=context):
283             if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.res_model:
284                 if menu.action.res_model in self.pool and self.pool[menu.action.res_model]._needaction:
285                     res[menu.id] = True
286         return res
287
288     def get_needaction_data(self, cr, uid, ids, context=None):
289         """ Return for each menu entry of ids :
290             - if it uses the needaction mechanism (needaction_enabled)
291             - the needaction counter of the related action, taking into account
292               the action domain
293         """
294         if context is None:
295             context = {}
296         res = {}
297         menu_ids = set()
298         for menu in self.browse(cr, uid, ids, context=context):
299             menu_ids.add(menu.id)
300             ctx = None
301             if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.context:
302                 try:
303                     # use magical UnquoteEvalContext to ignore undefined client-side variables such as `active_id`
304                     eval_ctx = tools.UnquoteEvalContext(**context)
305                     ctx = eval(menu.action.context, locals_dict=eval_ctx, nocopy=True) or None
306                 except Exception:
307                     # if the eval still fails for some reason, we'll simply skip this menu
308                     pass
309             menu_ref = ctx and ctx.get('needaction_menu_ref')
310             if menu_ref:
311                 if not isinstance(menu_ref, list):
312                     menu_ref = [menu_ref]
313                 model_data_obj = self.pool.get('ir.model.data')
314                 for menu_data in menu_ref:
315                     model, id = model_data_obj.get_object_reference(cr, uid, menu_data.split('.')[0], menu_data.split('.')[1])
316                     if (model == 'ir.ui.menu'):
317                         menu_ids.add(id)
318         menu_ids = list(menu_ids)
319
320         for menu in self.browse(cr, uid, menu_ids, context=context):
321             res[menu.id] = {
322                 'needaction_enabled': False,
323                 'needaction_counter': False,
324             }
325             if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.res_model:
326                 if menu.action.res_model in self.pool:
327                     obj = self.pool[menu.action.res_model]
328                     if obj._needaction:
329                         if menu.action.type == 'ir.actions.act_window':
330                             dom = menu.action.domain and eval(menu.action.domain, {'uid': uid}) or []
331                         else:
332                             dom = eval(menu.action.params_store or '{}', {'uid': uid}).get('domain')
333                         res[menu.id]['needaction_enabled'] = obj._needaction
334                         res[menu.id]['needaction_counter'] = obj._needaction_count(cr, uid, dom, context=context)
335         return res
336
337     _columns = {
338         'name': fields.char('Menu', size=64, required=True, translate=True),
339         'sequence': fields.integer('Sequence'),
340         'child_id': fields.one2many('ir.ui.menu', 'parent_id', 'Child IDs'),
341         'parent_id': fields.many2one('ir.ui.menu', 'Parent Menu', select=True, ondelete="restrict"),
342         'parent_left': fields.integer('Parent Left', select=True),
343         'parent_right': fields.integer('Parent Right', select=True),
344         'groups_id': fields.many2many('res.groups', 'ir_ui_menu_group_rel',
345             'menu_id', 'gid', 'Groups', help="If you have groups, the visibility of this menu will be based on these groups. "\
346                 "If this field is empty, OpenERP will compute visibility based on the related object's read access."),
347         'complete_name': fields.function(_get_full_name,
348             string='Full Path', type='char', size=128),
349         'icon': fields.selection(tools.icons, 'Icon', size=64),
350         'icon_pict': fields.function(_get_icon_pict, type='char', size=32),
351         'web_icon': fields.char('Web Icon File', size=128),
352         'web_icon_hover': fields.char('Web Icon File (hover)', size=128),
353         'web_icon_data': fields.function(_get_image_icon, string='Web Icon Image', type='binary', readonly=True, store=True, multi='icon'),
354         'web_icon_hover_data': fields.function(_get_image_icon, string='Web Icon Image (hover)', type='binary', readonly=True, store=True, multi='icon'),
355         'needaction_enabled': fields.function(_get_needaction_enabled,
356             type='boolean',
357             store=True,
358             string='Target model uses the need action mechanism',
359             help='If the menu entry action is an act_window action, and if this action is related to a model that uses the need_action mechanism, this field is set to true. Otherwise, it is false.'),
360         'action': fields.function(_action, fnct_inv=_action_inv,
361             type='reference', string='Action',
362             selection=[
363                 ('ir.actions.report.xml', 'ir.actions.report.xml'),
364                 ('ir.actions.act_window', 'ir.actions.act_window'),
365                 ('ir.actions.wizard', 'ir.actions.wizard'),
366                 ('ir.actions.act_url', 'ir.actions.act_url'),
367                 ('ir.actions.server', 'ir.actions.server'),
368                 ('ir.actions.client', 'ir.actions.client'),
369             ]),
370     }
371
372     def _rec_message(self, cr, uid, ids, context=None):
373         return _('Error ! You can not create recursive Menu.')
374
375     _constraints = [
376         (osv.osv._check_recursion, _rec_message, ['parent_id'])
377     ]
378     _defaults = {
379         'icon': 'STOCK_OPEN',
380         'icon_pict': ('stock', ('STOCK_OPEN', 'ICON_SIZE_MENU')),
381         'sequence': 10,
382     }
383     _order = "sequence,id"
384     _parent_store = True
385
386 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: