[IMP] ir_qweb, ir_ui_view: can use id insead of xml_id; can render ir.ui.view many2on...
[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 import collections
22 import copy
23 import fnmatch
24 import logging
25 from lxml import etree
26 from operator import itemgetter
27 import os
28 import simplejson
29 import werkzeug
30
31 import HTMLParser
32
33 import openerp
34 from openerp import tools
35 from openerp.http import request
36 from openerp.osv import fields, osv, orm
37 from openerp.tools import graph, SKIPPED_ELEMENT_TYPES
38 from openerp.tools.safe_eval import safe_eval as eval
39 from openerp.tools.view_validation import valid_view
40 from openerp.tools import misc
41 from openerp.tools.translate import _
42
43 _logger = logging.getLogger(__name__)
44
45 MOVABLE_BRANDING = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-xpath']
46
47 def keep_query(*args, **kw):
48     if not args and not kw:
49         args = ('*',)
50     params = kw.copy()
51     query_params = frozenset(werkzeug.url_decode(request.httprequest.query_string).keys())
52     for keep_param in args:
53         for param in fnmatch.filter(query_params, keep_param):
54             if param not in params and param in request.params:
55                 params[param] = request.params[param]
56     return werkzeug.urls.url_encode(params)
57
58 class view_custom(osv.osv):
59     _name = 'ir.ui.view.custom'
60     _order = 'create_date desc'  # search(limit=1) should return the last customization
61     _columns = {
62         'ref_id': fields.many2one('ir.ui.view', 'Original View', select=True, required=True, ondelete='cascade'),
63         'user_id': fields.many2one('res.users', 'User', select=True, required=True, ondelete='cascade'),
64         'arch': fields.text('View Architecture', required=True),
65     }
66
67     def _auto_init(self, cr, context=None):
68         super(view_custom, self)._auto_init(cr, context)
69         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_custom_user_id_ref_id\'')
70         if not cr.fetchone():
71             cr.execute('CREATE INDEX ir_ui_view_custom_user_id_ref_id ON ir_ui_view_custom (user_id, ref_id)')
72
73 class view(osv.osv):
74     _name = 'ir.ui.view'
75
76     def _get_model_data(self, cr, uid, ids, fname, args, context=None):
77         result = dict.fromkeys(ids, False)
78         IMD = self.pool['ir.model.data']
79         data_ids = IMD.search_read(cr, uid, [('res_id', 'in', ids), ('model', '=', 'ir.ui.view')], ['res_id'], context=context)
80         result.update(map(itemgetter('res_id', 'id'), data_ids))
81         return result
82
83     def _views_from_model_data(self, cr, uid, ids, context=None):
84         IMD = self.pool['ir.model.data']
85         data_ids = IMD.search_read(cr, uid, [('id', 'in', ids), ('model', '=', 'ir.ui.view')], ['res_id'], context=context)
86         return map(itemgetter('res_id'), data_ids)
87
88     _columns = {
89         'name': fields.char('View Name', required=True),
90         'model': fields.char('Object', select=True),
91         'priority': fields.integer('Sequence', required=True),
92         'type': fields.selection([
93             ('tree','Tree'),
94             ('form','Form'),
95             ('graph', 'Graph'),
96             ('calendar', 'Calendar'),
97             ('diagram','Diagram'),
98             ('gantt', 'Gantt'),
99             ('kanban', 'Kanban'),
100             ('search','Search'),
101             ('qweb', 'QWeb')], string='View Type'),
102         'arch': fields.text('View Architecture', required=True),
103         'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True),
104         'inherit_children_ids': fields.one2many('ir.ui.view','inherit_id', 'Inherit Views'),
105         'field_parent': fields.char('Child Field'),
106         'model_data_id': fields.function(_get_model_data, type='many2one', relation='ir.model.data', string="Model Data",
107                                          store={
108                                              _name: (lambda s, c, u, i, ctx=None: i, None, 10),
109                                              'ir.model.data': (_views_from_model_data, ['model', 'res_id'], 10),
110                                          }),
111         'xml_id': fields.function(osv.osv.get_xml_id, type='char', size=128, string="External ID",
112                                   help="ID of the view defined in xml file"),
113         'groups_id': fields.many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id',
114             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."),
115         'model_ids': fields.one2many('ir.model.data', 'res_id', domain=[('model','=','ir.ui.view')], auto_join=True),
116     }
117     _defaults = {
118         'priority': 16,
119     }
120     _order = "priority,name"
121
122     # Holds the RNG schema
123     _relaxng_validator = None
124
125     def _relaxng(self):
126         if not self._relaxng_validator:
127             frng = tools.file_open(os.path.join('base','rng','view.rng'))
128             try:
129                 relaxng_doc = etree.parse(frng)
130                 self._relaxng_validator = etree.RelaxNG(relaxng_doc)
131             except Exception:
132                 _logger.exception('Failed to load RelaxNG XML schema for views validation')
133             finally:
134                 frng.close()
135         return self._relaxng_validator
136
137     def _check_xml(self, cr, uid, ids, context=None):
138         if context is None:
139             context = {}
140         context = dict(context, check_view_ids=ids)
141
142         # Sanity checks: the view should not break anything upon rendering!
143         # Any exception raised below will cause a transaction rollback.
144         for view in self.browse(cr, uid, ids, context):
145             view_def = self.read_combined(cr, uid, view.id, None, context=context)
146             view_arch_utf8 = view_def['arch']
147             if view.type != 'qweb':
148                 view_doc = etree.fromstring(view_arch_utf8)
149                 # verify that all fields used are valid, etc.
150                 self.postprocess_and_fields(cr, uid, view.model, view_doc, view.id, context=context)
151                 # RNG-based validation is not possible anymore with 7.0 forms
152                 view_docs = [view_doc]
153                 if view_docs[0].tag == 'data':
154                     # A <data> element is a wrapper for multiple root nodes
155                     view_docs = view_docs[0]
156                 validator = self._relaxng()
157                 for view_arch in view_docs:
158                     if (view_arch.get('version') < '7.0') and validator and not validator.validate(view_arch):
159                         for error in validator.error_log:
160                             _logger.error(tools.ustr(error))
161                         return False
162                     if not valid_view(view_arch):
163                         return False
164         return True
165
166     _constraints = [
167         (_check_xml, 'Invalid view definition', ['arch'])
168     ]
169
170     def _auto_init(self, cr, context=None):
171         super(view, self)._auto_init(cr, context)
172         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_model_type_inherit_id\'')
173         if not cr.fetchone():
174             cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
175
176     def create(self, cr, uid, values, context=None):
177         if 'type' not in values:
178             if values.get('inherit_id'):
179                 values['type'] = self.browse(cr, uid, values['inherit_id'], context).type
180             else:
181                 values['type'] = etree.fromstring(values['arch']).tag
182
183         if not values.get('name'):
184             values['name'] = "%s %s" % (values['model'], values['type'])
185
186         self.read_template.clear_cache(self)
187         return super(view, self).create(cr, uid, values, context)
188
189     def write(self, cr, uid, ids, vals, context=None):
190         if not isinstance(ids, (list, tuple)):
191             ids = [ids]
192         if context is None:
193             context = {}
194
195         # drop the corresponding view customizations (used for dashboards for example), otherwise
196         # not all users would see the updated views
197         custom_view_ids = self.pool.get('ir.ui.view.custom').search(cr, uid, [('ref_id', 'in', ids)])
198         if custom_view_ids:
199             self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
200
201         self.read_template.clear_cache(self)
202         ret = super(view, self).write(cr, uid, ids, vals, context)
203
204         # if arch is modified views become noupdatable
205         if 'arch' in vals and not context.get('install_mode', False):
206             # TODO: should be doable in a read and a write
207             for view_ in self.browse(cr, uid, ids, context=context):
208                 if view_.model_data_id:
209                     self.pool.get('ir.model.data').write(cr, openerp.SUPERUSER_ID, view_.model_data_id.id, {'noupdate': True})
210         return ret
211
212     def copy(self, cr, uid, id, default=None, context=None):
213         if not default:
214             default = {}
215         default.update({
216             'model_ids': [],
217         })
218         return super(view, self).copy(cr, uid, id, default, context=context)
219
220     # default view selection
221     def default_view(self, cr, uid, model, view_type, context=None):
222         """ Fetches the default view for the provided (model, view_type) pair:
223          view with no parent (inherit_id=Fase) with the lowest priority.
224
225         :param str model:
226         :param int view_type:
227         :return: id of the default view of False if none found
228         :rtype: int
229         """
230         domain = [
231             ['model', '=', model],
232             ['type', '=', view_type],
233             ['inherit_id', '=', False],
234         ]
235         ids = self.search(cr, uid, domain, limit=1, order='priority', context=context)
236         if not ids:
237             return False
238         return ids[0]
239
240     #------------------------------------------------------
241     # Inheritance mecanism
242     #------------------------------------------------------
243     def get_inheriting_views_arch(self, cr, uid, view_id, model, context=None):
244         """Retrieves the architecture of views that inherit from the given view, from the sets of
245            views that should currently be used in the system. During the module upgrade phase it
246            may happen that a view is present in the database but the fields it relies on are not
247            fully loaded yet. This method only considers views that belong to modules whose code
248            is already loaded. Custom views defined directly in the database are loaded only
249            after the module initialization phase is completely finished.
250
251            :param int view_id: id of the view whose inheriting views should be retrieved
252            :param str model: model identifier of the inheriting views.
253            :rtype: list of tuples
254            :return: [(view_arch,view_id), ...]
255         """
256
257         user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id)
258
259         check_view_ids = context and context.get('check_view_ids') or (0,)
260         conditions = [['inherit_id', '=', view_id], ['model', '=', model]]
261         if self.pool._init:
262             # Module init currently in progress, only consider views from
263             # modules whose code is already loaded
264             conditions.extend([
265                 '|',
266                 ['model_ids.module', 'in', tuple(self.pool._init_modules)],
267                 ['id', 'in', check_view_ids],
268             ])
269         view_ids = self.search(cr, uid, conditions, context=context)
270
271         return [(view.arch, view.id)
272                 for view in self.browse(cr, 1, view_ids, context)
273                 if not (view.groups_id and user_groups.isdisjoint(view.groups_id))]
274
275     def raise_view_error(self, cr, uid, message, view_id, context=None):
276         view = self.browse(cr, uid, view_id, context)
277         not_avail = _('n/a')
278         message = ("%(msg)s\n\n" +
279                    _("Error context:\nView `%(view_name)s`") + 
280                    "\n[view_id: %(viewid)s, xml_id: %(xmlid)s, "
281                    "model: %(model)s, parent_id: %(parent)s]") % \
282                         {
283                           'view_name': view.name or not_avail, 
284                           'viewid': view_id or not_avail,
285                           'xmlid': view.xml_id or not_avail,
286                           'model': view.model or not_avail,
287                           'parent': view.inherit_id.id or not_avail,
288                           'msg': message,
289                         }
290         _logger.error(message)
291         raise AttributeError(message)
292
293     def locate_node(self, arch, spec):
294         """ Locate a node in a source (parent) architecture.
295
296         Given a complete source (parent) architecture (i.e. the field
297         `arch` in a view), and a 'spec' node (a node in an inheriting
298         view that specifies the location in the source view of what
299         should be changed), return (if it exists) the node in the
300         source view matching the specification.
301
302         :param arch: a parent architecture to modify
303         :param spec: a modifying node in an inheriting view
304         :return: a node in the source matching the spec
305         """
306         if spec.tag == 'xpath':
307             nodes = arch.xpath(spec.get('expr'))
308             return nodes[0] if nodes else None
309         elif spec.tag == 'field':
310             # Only compare the field name: a field can be only once in a given view
311             # at a given level (and for multilevel expressions, we should use xpath
312             # inheritance spec anyway).
313             for node in arch.iter('field'):
314                 if node.get('name') == spec.get('name'):
315                     return node
316             return None
317
318         for node in arch.iter(spec.tag):
319             if isinstance(node, SKIPPED_ELEMENT_TYPES):
320                 continue
321             if all(node.get(attr) == spec.get(attr) for attr in spec.attrib
322                    if attr not in ('position','version')):
323                 # Version spec should match parent's root element's version
324                 if spec.get('version') and spec.get('version') != arch.get('version'):
325                     return None
326                 return node
327         return None
328
329     def inherit_branding(self, specs_tree, view_id, root_id):
330         for node in specs_tree.iterchildren(tag=etree.Element):
331             xpath = node.getroottree().getpath(node)
332             if node.tag == 'data' or node.tag == 'xpath':
333                 self.inherit_branding(node, view_id, root_id)
334             else:
335                 node.set('data-oe-id', str(view_id))
336                 node.set('data-oe-source-id', str(root_id))
337                 node.set('data-oe-xpath', xpath)
338                 node.set('data-oe-model', 'ir.ui.view')
339                 node.set('data-oe-field', 'arch')
340
341         return specs_tree
342
343     def apply_inheritance_specs(self, cr, uid, source, specs_tree, inherit_id, context=None):
344         """ Apply an inheriting view (a descendant of the base view)
345
346         Apply to a source architecture all the spec nodes (i.e. nodes
347         describing where and what changes to apply to some parent
348         architecture) given by an inheriting view.
349
350         :param Element source: a parent architecture to modify
351         :param Elepect specs_tree: a modifying architecture in an inheriting view
352         :param inherit_id: the database id of specs_arch
353         :return: a modified source where the specs are applied
354         :rtype: Element
355         """
356         # Queue of specification nodes (i.e. nodes describing where and
357         # changes to apply to some parent architecture).
358         specs = [specs_tree]
359
360         while len(specs):
361             spec = specs.pop(0)
362             if isinstance(spec, SKIPPED_ELEMENT_TYPES):
363                 continue
364             if spec.tag == 'data':
365                 specs += [c for c in spec]
366                 continue
367             node = self.locate_node(source, spec)
368             if node is not None:
369                 pos = spec.get('position', 'inside')
370                 if pos == 'replace':
371                     if node.getparent() is None:
372                         source = copy.deepcopy(spec[0])
373                     else:
374                         for child in spec:
375                             node.addprevious(child)
376                         node.getparent().remove(node)
377                 elif pos == 'attributes':
378                     for child in spec.getiterator('attribute'):
379                         attribute = (child.get('name'), child.text and child.text.encode('utf8') or None)
380                         if attribute[1]:
381                             node.set(attribute[0], attribute[1])
382                         elif attribute[0] in node.attrib:
383                             del node.attrib[attribute[0]]
384                 else:
385                     sib = node.getnext()
386                     for child in spec:
387                         if pos == 'inside':
388                             node.append(child)
389                         elif pos == 'after':
390                             if sib is None:
391                                 node.addnext(child)
392                                 node = child
393                             else:
394                                 sib.addprevious(child)
395                         elif pos == 'before':
396                             node.addprevious(child)
397                         else:
398                             self.raise_view_error(cr, uid, _("Invalid position attribute: '%s'") % pos, inherit_id, context=context)
399             else:
400                 attrs = ''.join([
401                     ' %s="%s"' % (attr, spec.get(attr))
402                     for attr in spec.attrib
403                     if attr != 'position'
404                 ])
405                 tag = "<%s%s>" % (spec.tag, attrs)
406                 self.raise_view_error(cr, uid, _("Element '%s' cannot be located in parent view") % tag, inherit_id, context=context)
407
408         return source
409
410     def apply_view_inheritance(self, cr, uid, source, source_id, model, root_id=None, context=None):
411         """ Apply all the (directly and indirectly) inheriting views.
412
413         :param source: a parent architecture to modify (with parent modifications already applied)
414         :param source_id: the database view_id of the parent view
415         :param model: the original model for which we create a view (not
416             necessarily the same as the source's model); only the inheriting
417             views with that specific model will be applied.
418         :return: a modified source where all the modifying architecture are applied
419         """
420         if context is None: context = {}
421         if root_id is None:
422             root_id = source_id
423         sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model, context=context)
424         for (specs, view_id) in sql_inherit:
425             specs_tree = etree.fromstring(specs.encode('utf-8'))
426             if context.get('inherit_branding'):
427                 self.inherit_branding(specs_tree, view_id, root_id)
428             source = self.apply_inheritance_specs(cr, uid, source, specs_tree, view_id, context=context)
429             source = self.apply_view_inheritance(cr, uid, source, view_id, model, root_id=root_id, context=context)
430         return source
431
432     def read_combined(self, cr, uid, view_id, fields=None, context=None):
433         """
434         Utility function to get a view combined with its inherited views.
435
436         * Gets the top of the view tree if a sub-view is requested
437         * Applies all inherited archs on the root view
438         * Returns the view with all requested fields
439           .. note:: ``arch`` is always added to the fields list even if not
440                     requested (similar to ``id``)
441         """
442         if context is None: context = {}
443
444         # if view_id is not a root view, climb back to the top.
445         base = v = self.browse(cr, uid, view_id, context=context)
446         while v.inherit_id:
447             v = v.inherit_id
448         root_id = v.id
449
450         # arch and model fields are always returned
451         if fields:
452             fields = list(set(fields) | set(['arch', 'model']))
453
454         # read the view arch
455         [view] = self.read(cr, uid, [root_id], fields=fields, context=context)
456         arch_tree = etree.fromstring(view['arch'].encode('utf-8'))
457
458         if context.get('inherit_branding'):
459             arch_tree.attrib.update({
460                 'data-oe-model': 'ir.ui.view',
461                 'data-oe-id': str(root_id),
462                 'data-oe-field': 'arch',
463             })
464
465         # and apply inheritance
466         arch = self.apply_view_inheritance(
467             cr, uid, arch_tree, root_id, base.model, context=context)
468
469         return dict(view, arch=etree.tostring(arch, encoding='utf-8'))
470
471     #------------------------------------------------------
472     # Postprocessing: translation, groups and modifiers
473     #------------------------------------------------------
474     # TODO: 
475     # - split postprocess so that it can be used instead of translate_qweb
476     # - remove group processing from ir_qweb
477     #------------------------------------------------------
478     def postprocess(self, cr, user, model, node, view_id, in_tree_view, model_fields, context=None):
479         """Return the description of the fields in the node.
480
481         In a normal call to this method, node is a complete view architecture
482         but it is actually possible to give some sub-node (this is used so
483         that the method can call itself recursively).
484
485         Originally, the field descriptions are drawn from the node itself.
486         But there is now some code calling fields_get() in order to merge some
487         of those information in the architecture.
488
489         """
490         if context is None:
491             context = {}
492         result = False
493         fields = {}
494         children = True
495
496         modifiers = {}
497         Model = self.pool.get(model)
498         if not Model:
499             self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model),
500                                   view_id, context)
501
502         def encode(s):
503             if isinstance(s, unicode):
504                 return s.encode('utf8')
505             return s
506
507         def check_group(node):
508             """Apply group restrictions,  may be set at view level or model level::
509                * at view level this means the element should be made invisible to
510                  people who are not members
511                * at model level (exclusively for fields, obviously), this means
512                  the field should be completely removed from the view, as it is
513                  completely unavailable for non-members
514
515                :return: True if field should be included in the result of fields_view_get
516             """
517             if node.tag == 'field' and node.get('name') in Model._all_columns:
518                 column = Model._all_columns[node.get('name')].column
519                 if column.groups and not self.user_has_groups(
520                         cr, user, groups=column.groups, context=context):
521                     node.getparent().remove(node)
522                     fields.pop(node.get('name'), None)
523                     # no point processing view-level ``groups`` anymore, return
524                     return False
525             if node.get('groups'):
526                 can_see = self.user_has_groups(
527                     cr, user, groups=node.get('groups'), context=context)
528                 if not can_see:
529                     node.set('invisible', '1')
530                     modifiers['invisible'] = True
531                     if 'attrs' in node.attrib:
532                         del(node.attrib['attrs']) #avoid making field visible later
533                 del(node.attrib['groups'])
534             return True
535
536         if node.tag in ('field', 'node', 'arrow'):
537             if node.get('object'):
538                 attrs = {}
539                 views = {}
540                 xml = "<form>"
541                 for f in node:
542                     if f.tag == 'field':
543                         xml += etree.tostring(f, encoding="utf-8")
544                 xml += "</form>"
545                 new_xml = etree.fromstring(encode(xml))
546                 ctx = context.copy()
547                 ctx['base_model_name'] = model
548                 xarch, xfields = self.postprocess_and_fields(cr, user, node.get('object'), new_xml, view_id, ctx)
549                 views['form'] = {
550                     'arch': xarch,
551                     'fields': xfields
552                 }
553                 attrs = {'views': views}
554                 fields = xfields
555             if node.get('name'):
556                 attrs = {}
557                 try:
558                     if node.get('name') in Model._columns:
559                         column = Model._columns[node.get('name')]
560                     else:
561                         column = Model._inherit_fields[node.get('name')][2]
562                 except Exception:
563                     column = False
564
565                 if column:
566                     children = False
567                     views = {}
568                     for f in node:
569                         if f.tag in ('form', 'tree', 'graph', 'kanban', 'calendar'):
570                             node.remove(f)
571                             ctx = context.copy()
572                             ctx['base_model_name'] = model
573                             xarch, xfields = self.postprocess_and_fields(cr, user, column._obj or None, f, view_id, ctx)
574                             views[str(f.tag)] = {
575                                 'arch': xarch,
576                                 'fields': xfields
577                             }
578                     attrs = {'views': views}
579                 fields[node.get('name')] = attrs
580
581                 field = model_fields.get(node.get('name'))
582                 if field:
583                     orm.transfer_field_to_modifiers(field, modifiers)
584
585         elif node.tag in ('form', 'tree'):
586             result = Model.view_header_get(cr, user, False, node.tag, context)
587             if result:
588                 node.set('string', result)
589             in_tree_view = node.tag == 'tree'
590
591         elif node.tag == 'calendar':
592             for additional_field in ('date_start', 'date_delay', 'date_stop', 'color', 'all_day', 'attendee'):
593                 if node.get(additional_field):
594                     fields[node.get(additional_field)] = {}
595
596         if not check_group(node):
597             # node must be removed, no need to proceed further with its children
598             return fields
599
600         # The view architeture overrides the python model.
601         # Get the attrs before they are (possibly) deleted by check_group below
602         orm.transfer_node_to_modifiers(node, modifiers, context, in_tree_view)
603
604         # TODO remove attrs counterpart in modifiers when invisible is true ?
605
606         # translate view
607         if 'lang' in context:
608             Translations = self.pool['ir.translation']
609             if node.text and node.text.strip():
610                 trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.text.strip())
611                 if trans:
612                     node.text = node.text.replace(node.text.strip(), trans)
613             if node.tail and node.tail.strip():
614                 trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.tail.strip())
615                 if trans:
616                     node.tail =  node.tail.replace(node.tail.strip(), trans)
617
618             if node.get('string') and not result:
619                 trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.get('string'))
620                 if trans == node.get('string') and ('base_model_name' in context):
621                     # If translation is same as source, perhaps we'd have more luck with the alternative model name
622                     # (in case we are in a mixed situation, such as an inherited view where parent_view.model != model
623                     trans = Translations._get_source(cr, user, context['base_model_name'], 'view', context['lang'], node.get('string'))
624                 if trans:
625                     node.set('string', trans)
626
627             for attr_name in ('confirm', 'sum', 'avg', 'help', 'placeholder'):
628                 attr_value = node.get(attr_name)
629                 if attr_value:
630                     trans = Translations._get_source(cr, user, model, 'view', context['lang'], attr_value)
631                     if trans:
632                         node.set(attr_name, trans)
633
634         for f in node:
635             if children or (node.tag == 'field' and f.tag in ('filter','separator')):
636                 fields.update(self.postprocess(cr, user, model, f, view_id, in_tree_view, model_fields, context))
637
638         orm.transfer_modifiers_to_node(modifiers, node)
639         return fields
640
641     def _disable_workflow_buttons(self, cr, user, model, node):
642         """ Set the buttons in node to readonly if the user can't activate them. """
643         if model is None or user == 1:
644             # admin user can always activate workflow buttons
645             return node
646
647         # TODO handle the case of more than one workflow for a model or multiple
648         # transitions with different groups and same signal
649         usersobj = self.pool.get('res.users')
650         buttons = (n for n in node.getiterator('button') if n.get('type') != 'object')
651         for button in buttons:
652             user_groups = usersobj.read(cr, user, [user], ['groups_id'])[0]['groups_id']
653             cr.execute("""SELECT DISTINCT t.group_id
654                         FROM wkf
655                   INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
656                   INNER JOIN wkf_transition t ON (t.act_to = a.id)
657                        WHERE wkf.osv = %s
658                          AND t.signal = %s
659                          AND t.group_id is NOT NULL
660                    """, (model, button.get('name')))
661             group_ids = [x[0] for x in cr.fetchall() if x[0]]
662             can_click = not group_ids or bool(set(user_groups).intersection(group_ids))
663             button.set('readonly', str(int(not can_click)))
664         return node
665
666     def postprocess_and_fields(self, cr, user, model, node, view_id, context=None):
667         """ Return an architecture and a description of all the fields.
668
669         The field description combines the result of fields_get() and
670         postprocess().
671
672         :param node: the architecture as as an etree
673         :return: a tuple (arch, fields) where arch is the given node as a
674             string and fields is the description of all the fields.
675
676         """
677         fields = {}
678         Model = self.pool.get(model)
679         if not Model:
680             self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model), view_id, context)
681
682         if node.tag == 'diagram':
683             if node.getchildren()[0].tag == 'node':
684                 node_model = self.pool[node.getchildren()[0].get('object')]
685                 node_fields = node_model.fields_get(cr, user, None, context)
686                 fields.update(node_fields)
687                 if not node.get("create") and not node_model.check_access_rights(cr, user, 'create', raise_exception=False):
688                     node.set("create", 'false')
689             if node.getchildren()[1].tag == 'arrow':
690                 arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context)
691                 fields.update(arrow_fields)
692         else:
693             fields = Model.fields_get(cr, user, None, context)
694
695         fields_def = self.postprocess(cr, user, model, node, view_id, False, fields, context=context)
696         node = self._disable_workflow_buttons(cr, user, model, node)
697         if node.tag in ('kanban', 'tree', 'form', 'gantt'):
698             for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')):
699                 if not node.get(action) and not Model.check_access_rights(cr, user, operation, raise_exception=False):
700                     node.set(action, 'false')
701         arch = etree.tostring(node, encoding="utf-8").replace('\t', '')
702         for k in fields.keys():
703             if k not in fields_def:
704                 del fields[k]
705         for field in fields_def:
706             if field == 'id':
707                 # sometime, the view may contain the (invisible) field 'id' needed for a domain (when 2 objects have cross references)
708                 fields['id'] = {'readonly': True, 'type': 'integer', 'string': 'ID'}
709             elif field in fields:
710                 fields[field].update(fields_def[field])
711             else:
712                 message = _("Field `%(field_name)s` does not exist") % \
713                                 dict(field_name=field)
714                 self.raise_view_error(cr, user, message, view_id, context)
715         return arch, fields
716
717     #------------------------------------------------------
718     # QWeb template views
719     #------------------------------------------------------
720     @tools.ormcache_context(accepted_keys=('lang','inherit_branding', 'editable', 'translatable'))
721     def read_template(self, cr, uid, xml_id, context=None):
722         if isinstance(xml_id, (int, long)):
723             view_id = xml_id
724         else:
725             if '.' not in xml_id:
726                 raise ValueError('Invalid template id: %r' % (xml_id,))
727             view_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xml_id, raise_if_not_found=True)
728
729         arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch']
730         arch_tree = etree.fromstring(arch)
731
732         if 'lang' in context:
733             arch_tree = self.translate_qweb(cr, uid, view_id, arch_tree, context['lang'], context)
734
735         self.distribute_branding(arch_tree)
736         root = etree.Element('templates')
737         root.append(arch_tree)
738         arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
739         return arch
740
741     def clear_cache(self):
742         self.read_template.clear_cache(self)
743
744     def _contains_branded(self, node):
745         return node.tag == 't'\
746             or 't-raw' in node.attrib\
747             or any(self.is_node_branded(child) for child in node.iterdescendants())
748
749     def _pop_view_branding(self, element):
750         distributed_branding = dict(
751             (attribute, element.attrib.pop(attribute))
752             for attribute in MOVABLE_BRANDING
753             if element.get(attribute))
754         return distributed_branding
755
756     def distribute_branding(self, e, branding=None, parent_xpath='',
757                             index_map=misc.ConstantMapping(1)):
758         if e.get('t-ignore') or e.tag == 'head':
759             # remove any view branding possibly injected by inheritance
760             attrs = set(MOVABLE_BRANDING)
761             for descendant in e.iterdescendants(tag=etree.Element):
762                 if not attrs.intersection(descendant.attrib): continue
763                 self._pop_view_branding(descendant)
764             # TODO: find a better name and check if we have a string to boolean helper
765             return
766
767         node_path = e.get('data-oe-xpath')
768         if node_path is None:
769             node_path = "%s/%s[%d]" % (parent_xpath, e.tag, index_map[e.tag])
770         if branding and not (e.get('data-oe-model') or e.get('t-field')):
771             e.attrib.update(branding)
772             e.set('data-oe-xpath', node_path)
773         if not e.get('data-oe-model'): return
774
775         if set(('t-esc', 't-escf', 't-raw', 't-rawf')).intersection(e.attrib):
776             # nodes which fully generate their content and have no reason to
777             # be branded because they can not sensibly be edited
778             self._pop_view_branding(e)
779         elif self._contains_branded(e):
780             # if a branded element contains branded elements distribute own
781             # branding to children unless it's t-raw, then just remove branding
782             # on current element
783             distributed_branding = self._pop_view_branding(e)
784
785             if 't-raw' not in e.attrib:
786                 # TODO: collections.Counter if remove p2.6 compat
787                 # running index by tag type, for XPath query generation
788                 indexes = collections.defaultdict(lambda: 0)
789                 for child in e.iterchildren(tag=etree.Element):
790                     if child.get('data-oe-xpath'):
791                         # injected by view inheritance, skip otherwise
792                         # generated xpath is incorrect
793                         self.distribute_branding(child)
794                     else:
795                         indexes[child.tag] += 1
796                         self.distribute_branding(
797                             child, distributed_branding,
798                             parent_xpath=node_path, index_map=indexes)
799
800     def is_node_branded(self, node):
801         """ Finds out whether a node is branded or qweb-active (bears a
802         @data-oe-model or a @t-* *which is not t-field* as t-field does not
803         section out views)
804
805         :param node: an etree-compatible element to test
806         :type node: etree._Element
807         :rtype: boolean
808         """
809         return any(
810             (attr == 'data-oe-model' or (attr != 't-field' and attr.startswith('t-')))
811             for attr in node.attrib
812         )
813
814     def translate_qweb(self, cr, uid, id_, arch, lang, context=None):
815         # TODO: this should be moved in a place before inheritance is applied
816         #       but process() is only called on fields_view_get()
817         Translations = self.pool['ir.translation']
818         h = HTMLParser.HTMLParser()
819         def get_trans(text):
820             if not text or not text.strip():
821                 return None
822             text = h.unescape(text.strip())
823             if len(text) < 2 or (text.startswith('<!') and text.endswith('>')):
824                 return None
825             return Translations._get_source(cr, uid, 'website', 'view', lang, text, id_)
826
827         if arch.tag not in ['script']:
828             text = get_trans(arch.text)
829             if text:
830                 arch.text = arch.text.replace(arch.text.strip(), text)
831             tail = get_trans(arch.tail)
832             if tail:
833                 arch.tail = arch.tail.replace(arch.tail.strip(), tail)
834
835             for attr_name in ('title', 'alt', 'placeholder'):
836                 attr = get_trans(arch.get(attr_name))
837                 if attr:
838                     arch.set(attr_name, attr)
839             for node in arch.iterchildren("*"):
840                 self.translate_qweb(cr, uid, id_, node, lang, context)
841         return arch
842
843     @openerp.tools.ormcache()
844     def get_view_xmlid(self, cr, uid, id):
845         imd = self.pool['ir.model.data']
846         domain = [('model', '=', 'ir.ui.view'), ('res_id', '=', id)]
847         xmlid = imd.search_read(cr, uid, domain, ['module', 'name'])[0]
848         return '%s.%s' % (xmlid['module'], xmlid['name'])
849
850     def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
851         if isinstance(id_or_xml_id, list):
852             id_or_xml_id = id_or_xml_id[0]
853
854         if not context:
855             context = {}
856
857         if values is None:
858             values = dict()
859         qcontext = dict(
860             keep_query=keep_query,
861             request=request,
862             json=simplejson,
863             quote_plus=werkzeug.url_quote_plus,
864         )
865         qcontext.update(values)
866
867         def loader(name):
868             return self.read_template(cr, uid, name, context=context)
869
870         return self.pool[engine].render(cr, uid, id_or_xml_id, qcontext, loader=loader, context=context)
871
872     #------------------------------------------------------
873     # Misc
874     #------------------------------------------------------
875
876     def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
877         nodes=[]
878         nodes_name=[]
879         transitions=[]
880         start=[]
881         tres={}
882         labels={}
883         no_ancester=[]
884         blank_nodes = []
885
886         _Model_Obj = self.pool[model]
887         _Node_Obj = self.pool[node_obj]
888         _Arrow_Obj = self.pool[conn_obj]
889
890         for model_key,model_value in _Model_Obj._columns.items():
891                 if model_value._type=='one2many':
892                     if model_value._obj==node_obj:
893                         _Node_Field=model_key
894                         _Model_Field=model_value._fields_id
895                     flag=False
896                     for node_key,node_value in _Node_Obj._columns.items():
897                         if node_value._type=='one2many':
898                              if node_value._obj==conn_obj:
899                                  if src_node in _Arrow_Obj._columns and flag:
900                                     _Source_Field=node_key
901                                  if des_node in _Arrow_Obj._columns and not flag:
902                                     _Destination_Field=node_key
903                                     flag = True
904
905         datas = _Model_Obj.read(cr, uid, id, [],context)
906         for a in _Node_Obj.read(cr,uid,datas[_Node_Field],[]):
907             if a[_Source_Field] or a[_Destination_Field]:
908                 nodes_name.append((a['id'],a['name']))
909                 nodes.append(a['id'])
910             else:
911                 blank_nodes.append({'id': a['id'],'name':a['name']})
912
913             if a.has_key('flow_start') and a['flow_start']:
914                 start.append(a['id'])
915             else:
916                 if not a[_Source_Field]:
917                     no_ancester.append(a['id'])
918             for t in _Arrow_Obj.read(cr,uid, a[_Destination_Field],[]):
919                 transitions.append((a['id'], t[des_node][0]))
920                 tres[str(t['id'])] = (a['id'],t[des_node][0])
921                 label_string = ""
922                 if label:
923                     for lbl in eval(label):
924                         if t.has_key(tools.ustr(lbl)) and tools.ustr(t[lbl])=='False':
925                             label_string += ' '
926                         else:
927                             label_string = label_string + " " + tools.ustr(t[lbl])
928                 labels[str(t['id'])] = (a['id'],label_string)
929         g  = graph(nodes, transitions, no_ancester)
930         g.process(start)
931         g.scale(*scale)
932         result = g.result_get()
933         results = {}
934         for node in nodes_name:
935             results[str(node[0])] = result[node[0]]
936             results[str(node[0])]['name'] = node[1]
937         return {'nodes': results,
938                 'transitions': tres,
939                 'label' : labels,
940                 'blank_nodes': blank_nodes,
941                 'node_parent_field': _Model_Field,}
942
943     def _validate_custom_views(self, cr, uid, model):
944         """Validate architecture of custom views (= without xml id) for a given model.
945             This method is called at the end of registry update.
946         """
947         cr.execute("""SELECT max(v.id)
948                         FROM ir_ui_view v
949                    LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
950                        WHERE md.module IS NULL
951                          AND v.model = %s
952                     GROUP BY coalesce(v.inherit_id, v.id)
953                    """, (model,))
954
955         ids = map(itemgetter(0), cr.fetchall())
956         return self._check_xml(cr, uid, ids)
957
958 # vim:et: