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)):
195 result = super(view, self).write(cr, uid, ids, vals, context)
197 # drop the corresponding view customizations (used for dashboards for example), otherwise
198 # not all users would see the updated views
199 custom_view_ids = self.pool.get('ir.ui.view.custom').search(cr, uid, [('ref_id','in',ids)])
201 self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
205 def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
215 _Model_Obj=self.pool.get(model)
216 _Node_Obj=self.pool.get(node_obj)
217 _Arrow_Obj=self.pool.get(conn_obj)
219 for model_key,model_value in _Model_Obj._columns.items():
220 if model_value._type=='one2many':
221 if model_value._obj==node_obj:
222 _Node_Field=model_key
223 _Model_Field=model_value._fields_id
225 for node_key,node_value in _Node_Obj._columns.items():
226 if node_value._type=='one2many':
227 if node_value._obj==conn_obj:
228 if src_node in _Arrow_Obj._columns and flag:
229 _Source_Field=node_key
230 if des_node in _Arrow_Obj._columns and not flag:
231 _Destination_Field=node_key
234 datas = _Model_Obj.read(cr, uid, id, [],context)
235 for a in _Node_Obj.read(cr,uid,datas[_Node_Field],[]):
236 if a[_Source_Field] or a[_Destination_Field]:
237 nodes_name.append((a['id'],a['name']))
238 nodes.append(a['id'])
240 blank_nodes.append({'id': a['id'],'name':a['name']})
242 if a.has_key('flow_start') and a['flow_start']:
243 start.append(a['id'])
245 if not a[_Source_Field]:
246 no_ancester.append(a['id'])
247 for t in _Arrow_Obj.read(cr,uid, a[_Destination_Field],[]):
248 transitions.append((a['id'], t[des_node][0]))
249 tres[str(t['id'])] = (a['id'],t[des_node][0])
252 for lbl in eval(label):
253 if t.has_key(tools.ustr(lbl)) and tools.ustr(t[lbl])=='False':
254 label_string = label_string + ' '
256 label_string = label_string + " " + tools.ustr(t[lbl])
257 labels[str(t['id'])] = (a['id'],label_string)
258 g = graph(nodes, transitions, no_ancester)
261 result = g.result_get()
263 for node in nodes_name:
264 results[str(node[0])] = result[node[0]]
265 results[str(node[0])]['name'] = node[1]
266 return {'nodes': results,
269 'blank_nodes': blank_nodes,
270 'node_parent_field': _Model_Field,}
273 class view_sc(osv.osv):
274 _name = 'ir.ui.view_sc'
276 'name': fields.char('Shortcut Name', size=64), # Kept for backwards compatibility only - resource name used instead (translatable)
277 'res_id': fields.integer('Resource Ref.', help="Reference of the target resource, whose model/table depends on the 'Resource Name' field."),
278 'sequence': fields.integer('Sequence'),
279 'user_id': fields.many2one('res.users', 'User Ref.', required=True, ondelete='cascade', select=True),
280 'resource': fields.char('Resource Name', size=64, required=True, select=True)
283 def _auto_init(self, cr, context=None):
284 super(view_sc, self)._auto_init(cr, context)
285 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_sc_user_id_resource\'')
286 if not cr.fetchone():
287 cr.execute('CREATE INDEX ir_ui_view_sc_user_id_resource ON ir_ui_view_sc (user_id, resource)')
289 def get_sc(self, cr, uid, user_id, model='ir.ui.menu', context=None):
290 ids = self.search(cr, uid, [('user_id','=',user_id),('resource','=',model)], context=context)
291 results = self.read(cr, uid, ids, ['res_id'], context=context)
292 name_map = dict(self.pool.get(model).name_get(cr, uid, [x['res_id'] for x in results], context=context))
293 # Make sure to return only shortcuts pointing to exisintg menu items.
294 filtered_results = filter(lambda result: result['res_id'] in name_map, results)
295 for result in filtered_results:
296 result.update(name=name_map[result['res_id']])
297 return filtered_results
299 _order = 'sequence,name'
301 'resource': lambda *a: 'ir.ui.menu',
302 'user_id': lambda obj, cr, uid, context: uid,
305 ('shortcut_unique', 'unique(res_id, resource, user_id)', 'Shortcut for this menu already exists!'),
310 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: