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 ##############################################################################
23 from lxml import etree
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
32 _logger = logging.getLogger(__name__)
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
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),
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\'')
47 cr.execute('CREATE INDEX ir_ui_view_custom_user_id_ref_id ON ir_ui_view_custom (user_id, ref_id)')
52 def _type_field(self, cr, uid, ids, name, args, context=None):
54 for record in self.browse(cr, uid, ids, context):
55 # Get the type from the inherited view if any.
57 result[record.id] = record.inherit_id.type
59 result[record.id] = etree.fromstring(record.arch.encode('utf8')).tag
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=[
71 ('calendar', 'Calendar'),
72 ('diagram','Diagram'),
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."),
85 'arch': '<?xml version="1.0"?>\n<tree string="My view">\n\t<field name="name"/>\n</tree>',
89 _order = "priority,name"
91 # Holds the RNG schema
92 _relaxng_validator = None
94 def create(self, cr, uid, values, context=None):
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
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)
106 if not self._relaxng_validator:
107 frng = tools.file_open(os.path.join('base','rng','view.rng'))
109 relaxng_doc = etree.parse(frng)
110 self._relaxng_validator = etree.RelaxNG(relaxng_doc)
112 _logger.exception('Failed to load RelaxNG XML schema for views validation')
115 return self._relaxng_validator
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()``.
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.
126 fvg = self.pool.get(view.model).fields_view_get(cr, uid, view_id=view.id, view_type=view.type, context=context)
129 _logger.exception("Can't render view %s for model: %s", view.xml_id, view.model)
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
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))
151 if not valid_view(view_arch):
156 (_check_xml, 'Invalid XML for View Architecture!', ['arch'])
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)')
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.
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), ...]
178 user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id)
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
184 query_params = (view_id, model, tuple(self.pool._init_modules))
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
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))]
198 def write(self, cr, uid, ids, vals, context=None):
199 if not isinstance(ids, (list, tuple)):
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)])
206 self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
208 return super(view, self).write(cr, uid, ids, vals, context)
210 def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
220 _Model_Obj=self.pool.get(model)
221 _Node_Obj=self.pool.get(node_obj)
222 _Arrow_Obj=self.pool.get(conn_obj)
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
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
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'])
245 blank_nodes.append({'id': a['id'],'name':a['name']})
247 if a.has_key('flow_start') and a['flow_start']:
248 start.append(a['id'])
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])
257 for lbl in eval(label):
258 if t.has_key(tools.ustr(lbl)) and tools.ustr(t[lbl])=='False':
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)
266 result = g.result_get()
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,
274 'blank_nodes': blank_nodes,
275 'node_parent_field': _Model_Field,}
277 class view_sc(osv.osv):
278 _name = 'ir.ui.view_sc'
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)
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)')
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
303 _order = 'sequence,name'
305 'resource': 'ir.ui.menu',
306 'user_id': lambda obj, cr, uid, context: uid,
309 ('shortcut_unique', 'unique(res_id, resource, user_id)', 'Shortcut for this menu already exists!'),
312 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: