[IMP] ir.ui.view: filter inheriting views based on groups
[odoo/odoo.git] / openerp / addons / base / ir / ir_ui_view.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from osv import fields,osv
23 from lxml import etree
24 from tools import graph
25 from tools.safe_eval import safe_eval as eval
26 import tools
27 from tools.view_validation import valid_view
28 import os
29 import logging
30
31 _logger = logging.getLogger(__name__)
32
33 class view_custom(osv.osv):
34     _name = 'ir.ui.view.custom'
35     _order = 'create_date desc'  # search(limit=1) should return the last customization
36     _columns = {
37         'ref_id': fields.many2one('ir.ui.view', 'Original View', select=True, required=True, ondelete='cascade'),
38         'user_id': fields.many2one('res.users', 'User', select=True, required=True, ondelete='cascade'),
39         'arch': fields.text('View Architecture', required=True),
40     }
41
42     def _auto_init(self, cr, context=None):
43         super(view_custom, self)._auto_init(cr, context)
44         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_custom_user_id_ref_id\'')
45         if not cr.fetchone():
46             cr.execute('CREATE INDEX ir_ui_view_custom_user_id_ref_id ON ir_ui_view_custom (user_id, ref_id)')
47 view_custom()
48
49 class view(osv.osv):
50     _name = 'ir.ui.view'
51
52     def _type_field(self, cr, uid, ids, name, args, context=None):
53         result = {}
54         for record in self.browse(cr, uid, ids, context):
55             # Get the type from the inherited view if any.
56             if record.inherit_id:
57                 result[record.id] = record.inherit_id.type
58             else:
59                 result[record.id] = etree.fromstring(record.arch.encode('utf8')).tag
60         return result
61
62     _columns = {
63         'name': fields.char('View Name',size=64,  required=True),
64         'model': fields.char('Object', size=64, required=True, select=True),
65         'priority': fields.integer('Sequence', required=True),
66         'type': fields.function(_type_field, type='selection', selection=[
67             ('tree','Tree'),
68             ('form','Form'),
69             ('mdx','mdx'),
70             ('graph', 'Graph'),
71             ('calendar', 'Calendar'),
72             ('diagram','Diagram'),
73             ('gantt', 'Gantt'),
74             ('kanban', 'Kanban'),
75             ('search','Search')], string='View Type', required=True, select=True, store=True),
76         'arch': fields.text('View Architecture', required=True),
77         'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True),
78         'field_parent': fields.char('Child Field',size=64),
79         'xml_id': fields.function(osv.osv.get_xml_id, type='char', size=128, string="External ID",
80                                   help="ID of the view defined in xml file"),
81         'groups_id': fields.many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id',
82             string='Groups', help="If this field is empty, the view applies to all users. Otherwise, the view applies to the users of those groups only."),
83     }
84     _defaults = {
85         'arch': '<?xml version="1.0"?>\n<tree string="My view">\n\t<field name="name"/>\n</tree>',
86         'priority': 16
87     }
88     _order = "priority,name"
89
90     # Holds the RNG schema
91     _relaxng_validator = None  
92
93     def create(self, cr, uid, values, context=None):
94         if 'type' in values:
95             _logger.warning("Setting the `type` field is deprecated in the `ir.ui.view` model.")
96         return super(osv.osv, self).create(cr, uid, values, context)
97
98     def _relaxng(self):
99         if not self._relaxng_validator:
100             frng = tools.file_open(os.path.join('base','rng','view.rng'))
101             try:
102                 relaxng_doc = etree.parse(frng)
103                 self._relaxng_validator = etree.RelaxNG(relaxng_doc)
104             except Exception:
105                 _logger.exception('Failed to load RelaxNG XML schema for views validation')
106             finally:
107                 frng.close()
108         return self._relaxng_validator
109         
110         
111     def _check_render_view(self, cr, uid, view, context=None):
112         """Verify that the given view's hierarchy is valid for rendering, along with all the changes applied by
113            its inherited views, by rendering it using ``fields_view_get()``.
114            
115            @param browse_record view: view to validate
116            @return: the rendered definition (arch) of the view, always utf-8 bytestring (legacy convention)
117                if no error occurred, else False.  
118         """
119         try:
120             fvg = self.pool.get(view.model).fields_view_get(cr, uid, view_id=view.id, view_type=view.type, context=context)
121             return fvg['arch']
122         except:
123             _logger.exception("Can't render view %s for model: %s", view.xml_id, view.model)
124             return False
125
126     def _check_xml(self, cr, uid, ids, context=None):
127         for view in self.browse(cr, uid, ids, context):
128             # Sanity check: the view should not break anything upon rendering!
129             view_arch_utf8 = self._check_render_view(cr, uid, view, context=context)
130             # always utf-8 bytestring - legacy convention
131             if not view_arch_utf8: return False
132
133             # RNG-based validation is not possible anymore with 7.0 forms
134             # TODO 7.0: provide alternative assertion-based validation of view_arch_utf8
135             view_docs = [etree.fromstring(view_arch_utf8)]
136             if view_docs[0].tag == 'data':
137                 # A <data> element is a wrapper for multiple root nodes
138                 view_docs = view_docs[0]
139             validator = self._relaxng()
140             for view_arch in view_docs:
141                 if (view_arch.get('version') < '7.0') and validator and not validator.validate(view_arch):
142                     for error in validator.error_log:
143                         _logger.error(tools.ustr(error))
144                     return False
145                 if not valid_view(view_arch):
146                     return False
147         return True
148
149     _constraints = [
150         (_check_xml, 'Invalid XML for View Architecture!', ['arch'])
151     ]
152
153     def _auto_init(self, cr, context=None):
154         super(view, self)._auto_init(cr, context)
155         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_model_type_inherit_id\'')
156         if not cr.fetchone():
157             cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
158
159     def get_inheriting_views_arch(self, cr, uid, view_id, model, context=None):
160         """Retrieves the architecture of views that inherit from the given view, from the sets of
161            views that should currently be used in the system. During the module upgrade phase it
162            may happen that a view is present in the database but the fields it relies on are not
163            fully loaded yet. This method only considers views that belong to modules whose code
164            is already loaded. Custom views defined directly in the database are loaded only
165            after the module initialization phase is completely finished.
166
167            :param int view_id: id of the view whose inheriting views should be retrieved
168            :param str model: model identifier of the view's related model (for double-checking)
169            :rtype: list of tuples
170            :return: [(view_arch,view_id), ...]
171         """
172         user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id)
173         if self.pool._init:
174             # Module init currently in progress, only consider views from modules whose code was already loaded 
175             query = """SELECT v.id FROM ir_ui_view v LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
176                        WHERE v.inherit_id=%s AND v.model=%s AND md.module in %s  
177                        ORDER BY priority"""
178             query_params = (view_id, model, tuple(self.pool._init_modules))
179         else:
180             # Modules fully loaded, consider all views
181             query = """SELECT v.id FROM ir_ui_view v
182                        WHERE v.inherit_id=%s AND v.model=%s  
183                        ORDER BY priority"""
184             query_params = (view_id, model)
185         cr.execute(query, query_params)
186         view_ids = [v[0] for v in cr.fetchall()]
187         # filter views based on user groups
188         return [(view.arch, view.id)
189                 for view in self.browse(cr, 1, view_ids, context)
190                 if not (view.groups_id and user_groups.isdisjoint(view.groups_id))]
191
192     def write(self, cr, uid, ids, vals, context=None):
193         if not isinstance(ids, (list, tuple)):
194             ids = [ids]
195         result = super(view, self).write(cr, uid, ids, vals, context)
196
197         # drop the corresponding view customizations (used for dashboards for example), otherwise
198         # not all users would see the updated views
199         custom_view_ids = self.pool.get('ir.ui.view.custom').search(cr, uid, [('ref_id','in',ids)])
200         if custom_view_ids:
201             self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
202
203         return result
204
205     def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
206         nodes=[]
207         nodes_name=[]
208         transitions=[]
209         start=[]
210         tres={}
211         labels={}
212         no_ancester=[]
213         blank_nodes = []
214
215         _Model_Obj=self.pool.get(model)
216         _Node_Obj=self.pool.get(node_obj)
217         _Arrow_Obj=self.pool.get(conn_obj)
218
219         for model_key,model_value in _Model_Obj._columns.items():
220                 if model_value._type=='one2many':
221                     if model_value._obj==node_obj:
222                         _Node_Field=model_key
223                         _Model_Field=model_value._fields_id
224                     flag=False
225                     for node_key,node_value in _Node_Obj._columns.items():
226                         if node_value._type=='one2many':
227                              if node_value._obj==conn_obj:
228                                  if src_node in _Arrow_Obj._columns and flag:
229                                     _Source_Field=node_key
230                                  if des_node in _Arrow_Obj._columns and not flag:
231                                     _Destination_Field=node_key
232                                     flag = True
233
234         datas = _Model_Obj.read(cr, uid, id, [],context)
235         for a in _Node_Obj.read(cr,uid,datas[_Node_Field],[]):
236             if a[_Source_Field] or a[_Destination_Field]:
237                 nodes_name.append((a['id'],a['name']))
238                 nodes.append(a['id'])
239             else:
240                 blank_nodes.append({'id': a['id'],'name':a['name']})
241
242             if a.has_key('flow_start') and a['flow_start']:
243                 start.append(a['id'])
244             else:
245                 if not a[_Source_Field]:
246                     no_ancester.append(a['id'])
247             for t in _Arrow_Obj.read(cr,uid, a[_Destination_Field],[]):
248                 transitions.append((a['id'], t[des_node][0]))
249                 tres[str(t['id'])] = (a['id'],t[des_node][0])
250                 label_string = ""
251                 if label:
252                     for lbl in eval(label):
253                         if t.has_key(tools.ustr(lbl)) and tools.ustr(t[lbl])=='False':
254                             label_string = label_string + ' '
255                         else:
256                             label_string = label_string + " " + tools.ustr(t[lbl])
257                 labels[str(t['id'])] = (a['id'],label_string)
258         g  = graph(nodes, transitions, no_ancester)
259         g.process(start)
260         g.scale(*scale)
261         result = g.result_get()
262         results = {}
263         for node in nodes_name:
264             results[str(node[0])] = result[node[0]]
265             results[str(node[0])]['name'] = node[1]
266         return {'nodes': results,
267                 'transitions': tres,
268                 'label' : labels,
269                 'blank_nodes': blank_nodes,
270                 'node_parent_field': _Model_Field,}
271 view()
272
273 class view_sc(osv.osv):
274     _name = 'ir.ui.view_sc'
275     _columns = {
276         'name': fields.char('Shortcut Name', size=64), # Kept for backwards compatibility only - resource name used instead (translatable)
277         'res_id': fields.integer('Resource Ref.', help="Reference of the target resource, whose model/table depends on the 'Resource Name' field."),
278         'sequence': fields.integer('Sequence'),
279         'user_id': fields.many2one('res.users', 'User Ref.', required=True, ondelete='cascade', select=True),
280         'resource': fields.char('Resource Name', size=64, required=True, select=True)
281     }
282
283     def _auto_init(self, cr, context=None):
284         super(view_sc, self)._auto_init(cr, context)
285         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_sc_user_id_resource\'')
286         if not cr.fetchone():
287             cr.execute('CREATE INDEX ir_ui_view_sc_user_id_resource ON ir_ui_view_sc (user_id, resource)')
288
289     def get_sc(self, cr, uid, user_id, model='ir.ui.menu', context=None):
290         ids = self.search(cr, uid, [('user_id','=',user_id),('resource','=',model)], context=context)
291         results = self.read(cr, uid, ids, ['res_id'], context=context)
292         name_map = dict(self.pool.get(model).name_get(cr, uid, [x['res_id'] for x in results], context=context))
293         # Make sure to return only shortcuts pointing to exisintg menu items.
294         filtered_results = filter(lambda result: result['res_id'] in name_map, results)
295         for result in filtered_results:
296             result.update(name=name_map[result['res_id']])
297         return filtered_results
298
299     _order = 'sequence,name'
300     _defaults = {
301         'resource': lambda *a: 'ir.ui.menu',
302         'user_id': lambda obj, cr, uid, context: uid,
303     }
304     _sql_constraints = [
305         ('shortcut_unique', 'unique(res_id, resource, user_id)', 'Shortcut for this menu already exists!'),
306     ]
307
308 view_sc()
309
310 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
311