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