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)')
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',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=[
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>',
88 _order = "priority,name"
90 # Holds the RNG schema
91 _relaxng_validator = None
93 def create(self, cr, uid, values, context=None):
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)
99 if not self._relaxng_validator:
100 frng = tools.file_open(os.path.join('base','rng','view.rng'))
102 relaxng_doc = etree.parse(frng)
103 self._relaxng_validator = etree.RelaxNG(relaxng_doc)
105 _logger.exception('Failed to load RelaxNG XML schema for views validation')
108 return self._relaxng_validator
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()``.
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.
120 fvg = self.pool.get(view.model).fields_view_get(cr, uid, view_id=view.id, view_type=view.type, context=context)
123 _logger.exception("Can't render view %s for model: %s", view.xml_id, view.model)
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
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))
145 if not valid_view(view_arch):
150 (_check_xml, 'Invalid XML for View Architecture!', ['arch'])
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)')
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.
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), ...]
172 user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id)
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
178 query_params = (view_id, model, tuple(self.pool._init_modules))
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
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))]
192 def write(self, cr, uid, ids, vals, context=None):
193 if not isinstance(ids, (list, tuple)):
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)])
200 self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
202 return super(view, self).write(cr, uid, ids, vals, context)
204 def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
214 _Model_Obj=self.pool.get(model)
215 _Node_Obj=self.pool.get(node_obj)
216 _Arrow_Obj=self.pool.get(conn_obj)
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
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
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'])
239 blank_nodes.append({'id': a['id'],'name':a['name']})
241 if a.has_key('flow_start') and a['flow_start']:
242 start.append(a['id'])
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])
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 + ' '
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)
260 result = g.result_get()
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,
268 'blank_nodes': blank_nodes,
269 'node_parent_field': _Model_Field,}
272 class view_sc(osv.osv):
273 _name = 'ir.ui.view_sc'
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)
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)')
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
298 _order = 'sequence,name'
300 'resource': lambda *a: 'ir.ui.menu',
301 'user_id': lambda obj, cr, uid, context: uid,
304 ('shortcut_unique', 'unique(res_id, resource, user_id)', 'Shortcut for this menu already exists!'),
309 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: