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