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