1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
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
27 from tools.view_validation import valid_view
31 _logger = logging.getLogger(__name__)
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
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),
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\'')
46 cr.execute('CREATE INDEX ir_ui_view_custom_user_id_ref_id ON ir_ui_view_custom (user_id, ref_id)')
51 def _type_field(self, cr, uid, ids, name, args, context=None):
53 for record in self.browse(cr, uid, ids, context):
54 # Get the type from the inherited view if any.
56 result[record.id] = record.inherit_id.type
58 result[record.id] = etree.fromstring(record.arch.encode('utf8')).tag
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=[
70 ('calendar', 'Calendar'),
71 ('diagram','Diagram'),
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."),
84 'arch': '<?xml version="1.0"?>\n<tree string="My view">\n\t<field name="name"/>\n</tree>',
87 _order = "priority,name"
89 # Holds the RNG schema
90 _relaxng_validator = None
92 def create(self, cr, uid, values, context=None):
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
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)
104 if not self._relaxng_validator:
105 frng = tools.file_open(os.path.join('base','rng','view.rng'))
107 relaxng_doc = etree.parse(frng)
108 self._relaxng_validator = etree.RelaxNG(relaxng_doc)
110 _logger.exception('Failed to load RelaxNG XML schema for views validation')
113 return self._relaxng_validator
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()``.
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.
124 fvg = self.pool.get(view.model).fields_view_get(cr, uid, view_id=view.id, view_type=view.type, context=context)
127 _logger.exception("Can't render view %s for model: %s", view.xml_id, view.model)
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
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))
149 if not valid_view(view_arch):
154 (_check_xml, 'Invalid XML for View Architecture!', ['arch'])
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)')
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.
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), ...]
176 user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id)
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
182 query_params = (view_id, model, tuple(self.pool._init_modules))
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
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))]
196 def write(self, cr, uid, ids, vals, context=None):
197 if not isinstance(ids, (list, tuple)):
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)])
204 self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
206 return super(view, self).write(cr, uid, ids, vals, context)
208 def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
218 _Model_Obj=self.pool.get(model)
219 _Node_Obj=self.pool.get(node_obj)
220 _Arrow_Obj=self.pool.get(conn_obj)
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
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
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'])
243 blank_nodes.append({'id': a['id'],'name':a['name']})
245 if a.has_key('flow_start') and a['flow_start']:
246 start.append(a['id'])
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])
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 + ' '
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)
264 result = g.result_get()
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,
272 'blank_nodes': blank_nodes,
273 'node_parent_field': _Model_Field,}
275 class view_sc(osv.osv):
276 _name = 'ir.ui.view_sc'
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)
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)')
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
301 _order = 'sequence,name'
303 'resource': lambda *a: 'ir.ui.menu',
304 'user_id': lambda obj, cr, uid, context: uid,
307 ('shortcut_unique', 'unique(res_id, resource, user_id)', 'Shortcut for this menu already exists!'),
310 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: