[MRG] merge with lp:openobject-server
[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-2011 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 tools.safe_eval import safe_eval as eval
27 import tools
28 import openerp.modules
29 from osv import fields, osv
30 from tools.translate import _
31 from openerp import SUPERUSER_ID
32
33 def one_in(setA, setB):
34     """Check the presence of an element of setA in setB
35     """
36     for x in setA:
37         if x in setB:
38             return True
39     return False
40
41 class ir_ui_menu(osv.osv):
42     _name = 'ir.ui.menu'
43
44     def __init__(self, *args, **kwargs):
45         self.cache_lock = threading.RLock()
46         self.clear_cache()
47         r = super(ir_ui_menu, self).__init__(*args, **kwargs)
48         self.pool.get('ir.model.access').register_cache_clearing_method(self._name, 'clear_cache')
49         return r
50
51     def clear_cache(self):
52         with self.cache_lock:
53             # radical but this doesn't frequently happen
54             self._cache = {}
55
56     def _filter_visible_menus(self, cr, uid, ids, context=None):
57         """Filters the give menu ids to only keep the menu items that should be
58            visible in the menu hierarchy of the current user.
59            Uses a cache for speeding up the computation.
60         """
61         with self.cache_lock:
62             modelaccess = self.pool.get('ir.model.access')
63             user_groups = set(self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['groups_id'])['groups_id'])
64             result = []
65             for menu in self.browse(cr, uid, ids, context=context):
66                 # this key works because user access rights are all based on user's groups (cfr ir_model_access.check)
67                 key = (cr.dbname, menu.id, tuple(user_groups))
68                 if key in self._cache:
69                     if self._cache[key]:
70                         result.append(menu.id)
71                     #elif not menu.groups_id and not menu.action:
72                     #    result.append(menu.id)
73                     continue
74
75                 self._cache[key] = False
76                 if menu.groups_id:
77                     restrict_to_groups = [g.id for g in menu.groups_id]
78                     if not user_groups.intersection(restrict_to_groups):
79                         continue
80                     #result.append(menu.id)
81                     #self._cache[key] = True
82                     #continue
83
84                 if menu.action:
85                     # we check if the user has access to the action of the menu
86                     data = menu.action
87                     if data:
88                         model_field = { 'ir.actions.act_window':    'res_model',
89                                         'ir.actions.report.xml':    'model',
90                                         'ir.actions.wizard':        'model',
91                                         'ir.actions.server':        'model_id',
92                                       }
93
94                         field = model_field.get(menu.action._name)
95                         if field and data[field]:
96                             if not modelaccess.check(cr, uid, data[field], 'read', False):
97                                 continue
98                 else:
99                     # if there is no action, it's a 'folder' menu
100                     if not menu.child_id:
101                         # not displayed if there is no children
102                         continue
103
104                 result.append(menu.id)
105                 self._cache[key] = True
106             return result
107
108     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
109         if context is None:
110             context = {}
111
112         ids = super(ir_ui_menu, self).search(cr, uid, args, offset=0,
113             limit=None, order=order, context=context, count=False)
114
115         if not ids:
116             if count:
117                 return 0
118             return []
119
120         # menu filtering is done only on main menu tree, not other menu lists
121         if context.get('ir.ui.menu.full_list'):
122             result = ids
123         else:
124             result = self._filter_visible_menus(cr, uid, ids, context=context)
125
126         if offset:
127             result = result[long(offset):]
128         if limit:
129             result = result[:long(limit)]
130
131         if count:
132             return len(result)
133         return result
134
135     def _get_full_name(self, cr, uid, ids, name, args, context):
136         res = {}
137         for m in self.browse(cr, uid, ids, context=context):
138             res[m.id] = self._get_one_full_name(m)
139         return res
140
141     def _get_one_full_name(self, menu, level=6):
142         if level<=0:
143             return '...'
144         if menu.parent_id:
145             parent_path = self._get_one_full_name(menu.parent_id, level-1) + "/"
146         else:
147             parent_path = ''
148         return parent_path + menu.name
149
150     def create(self, *args, **kwargs):
151         self.clear_cache()
152         return super(ir_ui_menu, self).create(*args, **kwargs)
153
154     def write(self, *args, **kwargs):
155         self.clear_cache()
156         return super(ir_ui_menu, self).write(*args, **kwargs)
157
158     def unlink(self, *args, **kwargs):
159         self.clear_cache()
160         return super(ir_ui_menu, self).unlink(*args, **kwargs)
161
162     def copy(self, cr, uid, id, default=None, context=None):
163         ir_values_obj = self.pool.get('ir.values')
164         res = super(ir_ui_menu, self).copy(cr, uid, id, context=context)
165         datas=self.read(cr,uid,[res],['name'])[0]
166         rex=re.compile('\([0-9]+\)')
167         concat=rex.findall(datas['name'])
168         if concat:
169             next_num=int(concat[0])+1
170             datas['name']=rex.sub(('(%d)'%next_num),datas['name'])
171         else:
172             datas['name']=datas['name']+'(1)'
173         self.write(cr,uid,[res],{'name':datas['name']})
174         ids = ir_values_obj.search(cr, uid, [
175             ('model', '=', 'ir.ui.menu'),
176             ('res_id', '=', id),
177             ])
178         for iv in ir_values_obj.browse(cr, uid, ids):
179             ir_values_obj.copy(cr, uid, iv.id, default={'res_id': res},
180                                context=context)
181         return res
182
183     def _action(self, cursor, user, ids, name, arg, context=None):
184         res = {}
185         ir_values_obj = self.pool.get('ir.values')
186         value_ids = ir_values_obj.search(cursor, user, [
187             ('model', '=', self._name), ('key', '=', 'action'),
188             ('key2', '=', 'tree_but_open'), ('res_id', 'in', ids)],
189             context=context)
190         values_action = {}
191         for value in ir_values_obj.browse(cursor, user, value_ids, context=context):
192             values_action[value.res_id] = value.value
193         for menu_id in ids:
194             res[menu_id] = values_action.get(menu_id, False)
195         return res
196
197     def _action_inv(self, cursor, user, menu_id, name, value, arg, context=None):
198         if context is None:
199             context = {}
200         ctx = context.copy()
201         if self.CONCURRENCY_CHECK_FIELD in ctx:
202             del ctx[self.CONCURRENCY_CHECK_FIELD]
203         ir_values_obj = self.pool.get('ir.values')
204         values_ids = ir_values_obj.search(cursor, user, [
205             ('model', '=', self._name), ('key', '=', 'action'),
206             ('key2', '=', 'tree_but_open'), ('res_id', '=', menu_id)],
207             context=context)
208         if value and values_ids:
209             ir_values_obj.write(cursor, user, values_ids, {'value': value}, context=ctx)
210         elif value:
211             # no values_ids, create binding
212             ir_values_obj.create(cursor, user, {
213                 'name': 'Menuitem',
214                 'model': self._name,
215                 'value': value,
216                 'key': 'action',
217                 'key2': 'tree_but_open',
218                 'res_id': menu_id,
219                 }, context=ctx)
220         elif values_ids:
221             # value is False, remove existing binding
222             ir_values_obj.unlink(cursor, user, values_ids, context=ctx)
223
224     def _get_icon_pict(self, cr, uid, ids, name, args, context):
225         res = {}
226         for m in self.browse(cr, uid, ids, context=context):
227             res[m.id] = ('stock', (m.icon,'ICON_SIZE_MENU'))
228         return res
229
230     def onchange_icon(self, cr, uid, ids, icon):
231         if not icon:
232             return {}
233         return {'type': {'icon_pict': 'picture'}, 'value': {'icon_pict': ('stock', (icon,'ICON_SIZE_MENU'))}}
234
235     def read_image(self, path):
236         if not path:
237             return False
238         path_info = path.split(',')
239         icon_path = openerp.modules.get_module_resource(path_info[0],path_info[1])
240         icon_image = False
241         if icon_path:
242             try:
243                 icon_file = tools.file_open(icon_path,'rb')
244                 icon_image = base64.encodestring(icon_file.read())
245             finally:
246                 icon_file.close()
247         return icon_image
248
249     def _get_image_icon(self, cr, uid, ids, names, args, context=None):
250         res = {}
251         for menu in self.browse(cr, uid, ids, context=context):
252             res[menu.id] = r = {}
253             for fn in names:
254                 fn_src = fn[:-5]    # remove _data
255                 r[fn] = self.read_image(menu[fn_src])
256
257         return res
258
259     def _get_needaction(self, cr, uid, ids, field_names, args, context=None):
260         res = {}
261         for menu in self.browse(cr, uid, ids, context=context):
262             res[menu.id] = {
263                 'needaction_enabled': False,
264                 'needaction_counter': False,
265             }
266             if menu.action and menu.action.type in ('ir.actions.act_window','ir.actions.client') and menu.action.res_model:
267                 obj = self.pool.get(menu.action.res_model)
268                 if obj and obj._needaction:
269                     if menu.action.type=='ir.actions.act_window':
270                         dom = menu.action.domain and eval(menu.action.domain, {'uid': uid}) or []
271                     else:
272                         dom = eval(menu.action.params_store or '{}', {'uid': uid}).get('domain')
273                     res[menu.id]['needaction_enabled'] = obj._needaction
274                     res[menu.id]['needaction_counter'] = obj._needaction_count(cr, uid, dom, context=context)
275         return res
276
277     _columns = {
278         'name': fields.char('Menu', size=64, required=True, translate=True),
279         'sequence': fields.integer('Sequence'),
280         'child_id' : fields.one2many('ir.ui.menu', 'parent_id','Child IDs'),
281         'parent_id': fields.many2one('ir.ui.menu', 'Parent Menu', select=True),
282         'groups_id': fields.many2many('res.groups', 'ir_ui_menu_group_rel',
283             'menu_id', 'gid', 'Groups', help="If you have groups, the visibility of this menu will be based on these groups. "\
284                 "If this field is empty, OpenERP will compute visibility based on the related object's read access."),
285         'complete_name': fields.function(_get_full_name, 
286             string='Full Path', type='char', size=128),
287         'icon': fields.selection(tools.icons, 'Icon', size=64),
288         'icon_pict': fields.function(_get_icon_pict, type='char', size=32),
289         'web_icon': fields.char('Web Icon File', size=128),
290         'web_icon_hover':fields.char('Web Icon File (hover)', size=128),
291         'web_icon_data': fields.function(_get_image_icon, string='Web Icon Image', type='binary', readonly=True, store=True, multi='icon'),
292         'web_icon_hover_data':fields.function(_get_image_icon, string='Web Icon Image (hover)', type='binary', readonly=True, store=True, multi='icon'),
293         'needaction_enabled': fields.function(_get_needaction, string='Target model uses the need action mechanism', type='boolean', 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.', multi='_get_needaction'),
294         'needaction_counter': fields.function(_get_needaction, string='Number of actions the user has to perform', type='integer', help='If the target model uses the need action mechanism, this field gives the number of actions the current user has to perform.', multi='_get_needaction'),
295         'action': fields.function(_action, fnct_inv=_action_inv,
296             type='reference', string='Action',
297             selection=[
298                 ('ir.actions.report.xml', 'ir.actions.report.xml'),
299                 ('ir.actions.act_window', 'ir.actions.act_window'),
300                 ('ir.actions.wizard', 'ir.actions.wizard'),
301                 ('ir.actions.act_url', 'ir.actions.act_url'),
302                 ('ir.actions.server', 'ir.actions.server'),
303                 ('ir.actions.client', 'ir.actions.client'),
304             ]),
305     }
306
307     def _rec_message(self, cr, uid, ids, context=None):
308         return _('Error ! You can not create recursive Menu.')
309
310     _constraints = [
311         (osv.osv._check_recursion, _rec_message , ['parent_id'])
312     ]
313     _defaults = {
314         'icon' : 'STOCK_OPEN',
315         'icon_pict': ('stock', ('STOCK_OPEN','ICON_SIZE_MENU')),
316         'sequence' : 10,
317     }
318     _order = "sequence,id"
319 ir_ui_menu()
320
321
322
323 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
324