[IMP] ir.ui.view: discard custom views before updating view arch
[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
196         # drop the corresponding view customizations (used for dashboards for example), otherwise
197         # not all users would see the updated views
198         custom_view_ids = self.pool.get('ir.ui.view.custom').search(cr, uid, [('ref_id','in',ids)])
199         if custom_view_ids:
200             self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
201
202         return super(view, self).write(cr, uid, ids, vals, context)
203
204     def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
205         nodes=[]
206         nodes_name=[]
207         transitions=[]
208         start=[]
209         tres={}
210         labels={}
211         no_ancester=[]
212         blank_nodes = []
213
214         _Model_Obj=self.pool.get(model)
215         _Node_Obj=self.pool.get(node_obj)
216         _Arrow_Obj=self.pool.get(conn_obj)
217
218         for model_key,model_value in _Model_Obj._columns.items():
219                 if model_value._type=='one2many':
220                     if model_value._obj==node_obj:
221                         _Node_Field=model_key
222                         _Model_Field=model_value._fields_id
223                     flag=False
224                     for node_key,node_value in _Node_Obj._columns.items():
225                         if node_value._type=='one2many':
226                              if node_value._obj==conn_obj:
227                                  if src_node in _Arrow_Obj._columns and flag:
228                                     _Source_Field=node_key
229                                  if des_node in _Arrow_Obj._columns and not flag:
230                                     _Destination_Field=node_key
231                                     flag = True
232
233         datas = _Model_Obj.read(cr, uid, id, [],context)
234         for a in _Node_Obj.read(cr,uid,datas[_Node_Field],[]):
235             if a[_Source_Field] or a[_Destination_Field]:
236                 nodes_name.append((a['id'],a['name']))
237                 nodes.append(a['id'])
238             else:
239                 blank_nodes.append({'id': a['id'],'name':a['name']})
240
241             if a.has_key('flow_start') and a['flow_start']:
242                 start.append(a['id'])
243             else:
244                 if not a[_Source_Field]:
245                     no_ancester.append(a['id'])
246             for t in _Arrow_Obj.read(cr,uid, a[_Destination_Field],[]):
247                 transitions.append((a['id'], t[des_node][0]))
248                 tres[str(t['id'])] = (a['id'],t[des_node][0])
249                 label_string = ""
250                 if label:
251                     for lbl in eval(label):
252                         if t.has_key(tools.ustr(lbl)) and tools.ustr(t[lbl])=='False':
253                             label_string = label_string + ' '
254                         else:
255                             label_string = label_string + " " + tools.ustr(t[lbl])
256                 labels[str(t['id'])] = (a['id'],label_string)
257         g  = graph(nodes, transitions, no_ancester)
258         g.process(start)
259         g.scale(*scale)
260         result = g.result_get()
261         results = {}
262         for node in nodes_name:
263             results[str(node[0])] = result[node[0]]
264             results[str(node[0])]['name'] = node[1]
265         return {'nodes': results,
266                 'transitions': tres,
267                 'label' : labels,
268                 'blank_nodes': blank_nodes,
269                 'node_parent_field': _Model_Field,}
270 view()
271
272 class view_sc(osv.osv):
273     _name = 'ir.ui.view_sc'
274     _columns = {
275         'name': fields.char('Shortcut Name', size=64), # Kept for backwards compatibility only - resource name used instead (translatable)
276         'res_id': fields.integer('Resource Ref.', help="Reference of the target resource, whose model/table depends on the 'Resource Name' field."),
277         'sequence': fields.integer('Sequence'),
278         'user_id': fields.many2one('res.users', 'User Ref.', required=True, ondelete='cascade', select=True),
279         'resource': fields.char('Resource Name', size=64, required=True, select=True)
280     }
281
282     def _auto_init(self, cr, context=None):
283         super(view_sc, self)._auto_init(cr, context)
284         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_sc_user_id_resource\'')
285         if not cr.fetchone():
286             cr.execute('CREATE INDEX ir_ui_view_sc_user_id_resource ON ir_ui_view_sc (user_id, resource)')
287
288     def get_sc(self, cr, uid, user_id, model='ir.ui.menu', context=None):
289         ids = self.search(cr, uid, [('user_id','=',user_id),('resource','=',model)], context=context)
290         results = self.read(cr, uid, ids, ['res_id'], context=context)
291         name_map = dict(self.pool.get(model).name_get(cr, uid, [x['res_id'] for x in results], context=context))
292         # Make sure to return only shortcuts pointing to exisintg menu items.
293         filtered_results = filter(lambda result: result['res_id'] in name_map, results)
294         for result in filtered_results:
295             result.update(name=name_map[result['res_id']])
296         return filtered_results
297
298     _order = 'sequence,name'
299     _defaults = {
300         'resource': lambda *a: 'ir.ui.menu',
301         'user_id': lambda obj, cr, uid, context: uid,
302     }
303     _sql_constraints = [
304         ('shortcut_unique', 'unique(res_id, resource, user_id)', 'Shortcut for this menu already exists!'),
305     ]
306
307 view_sc()
308
309 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
310