[FIX] branding distribution to generate correct xpath even after injection of inherit...
[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, source_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, source_id)
324             else:
325                 node.set('data-oe-id', str(view_id))
326                 node.set('data-oe-source-id', str(source_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, 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         sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model, context=context)
412         for (specs, view_id) in sql_inherit:
413             specs_tree = etree.fromstring(specs.encode('utf-8'))
414             if context.get('inherit_branding'):
415                 self.inherit_branding(specs_tree, view_id, source_id)
416             source = self.apply_inheritance_specs(cr, uid, source, specs_tree, view_id, context=context)
417             source = self.apply_view_inheritance(cr, uid, source, view_id, model, context=context)
418         return source
419
420     def read_combined(self, cr, uid, view_id, fields=None, context=None):
421         """
422         Utility function to get a view combined with its inherited views.
423
424         * Gets the top of the view tree if a sub-view is requested
425         * Applies all inherited archs on the root view
426         * Returns the view with all requested fields
427           .. note:: ``arch`` is always added to the fields list even if not
428                     requested (similar to ``id``)
429         """
430         if context is None: context = {}
431
432         # if view_id is not a root view, climb back to the top.
433         base = v = self.browse(cr, uid, view_id, context=context)
434         while v.inherit_id:
435             v = v.inherit_id
436         root_id = v.id
437
438         # arch and model fields are always returned
439         if fields:
440             fields = list(set(fields) | set(['arch', 'model']))
441
442         # read the view arch
443         [view] = self.read(cr, uid, [root_id], fields=fields, context=context)
444         arch_tree = etree.fromstring(view['arch'].encode('utf-8'))
445
446         if context.get('inherit_branding'):
447             arch_tree.attrib.update({
448                 'data-oe-model': 'ir.ui.view',
449                 'data-oe-id': str(root_id),
450                 'data-oe-field': 'arch',
451             })
452
453         # and apply inheritance
454         arch = self.apply_view_inheritance(
455             cr, uid, arch_tree, root_id, base.model, context=context)
456
457         return dict(view, arch=etree.tostring(arch, encoding='utf-8'))
458
459     #------------------------------------------------------
460     # Postprocessing: translation, groups and modifiers
461     #------------------------------------------------------
462     # TODO: 
463     # - split postprocess so that it can be used instead of translate_qweb
464     # - remove group processing from ir_qweb
465     #------------------------------------------------------
466     def postprocess(self, cr, user, model, node, view_id, in_tree_view, model_fields, context=None):
467         """Return the description of the fields in the node.
468
469         In a normal call to this method, node is a complete view architecture
470         but it is actually possible to give some sub-node (this is used so
471         that the method can call itself recursively).
472
473         Originally, the field descriptions are drawn from the node itself.
474         But there is now some code calling fields_get() in order to merge some
475         of those information in the architecture.
476
477         """
478         if context is None:
479             context = {}
480         result = False
481         fields = {}
482         children = True
483
484         modifiers = {}
485         Model = self.pool.get(model)
486         if not Model:
487             self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model),
488                                   view_id, context)
489
490         def encode(s):
491             if isinstance(s, unicode):
492                 return s.encode('utf8')
493             return s
494
495         def check_group(node):
496             """Apply group restrictions,  may be set at view level or model level::
497                * at view level this means the element should be made invisible to
498                  people who are not members
499                * at model level (exclusively for fields, obviously), this means
500                  the field should be completely removed from the view, as it is
501                  completely unavailable for non-members
502
503                :return: True if field should be included in the result of fields_view_get
504             """
505             if node.tag == 'field' and node.get('name') in Model._all_columns:
506                 column = Model._all_columns[node.get('name')].column
507                 if column.groups and not self.user_has_groups(
508                         cr, user, groups=column.groups, context=context):
509                     node.getparent().remove(node)
510                     fields.pop(node.get('name'), None)
511                     # no point processing view-level ``groups`` anymore, return
512                     return False
513             if node.get('groups'):
514                 can_see = self.user_has_groups(
515                     cr, user, groups=node.get('groups'), context=context)
516                 if not can_see:
517                     node.set('invisible', '1')
518                     modifiers['invisible'] = True
519                     if 'attrs' in node.attrib:
520                         del(node.attrib['attrs']) #avoid making field visible later
521                 del(node.attrib['groups'])
522             return True
523
524         if node.tag in ('field', 'node', 'arrow'):
525             if node.get('object'):
526                 attrs = {}
527                 views = {}
528                 xml = "<form>"
529                 for f in node:
530                     if f.tag == 'field':
531                         xml += etree.tostring(f, encoding="utf-8")
532                 xml += "</form>"
533                 new_xml = etree.fromstring(encode(xml))
534                 ctx = context.copy()
535                 ctx['base_model_name'] = model
536                 xarch, xfields = self.postprocess_and_fields(cr, user, node.get('object'), new_xml, view_id, ctx)
537                 views['form'] = {
538                     'arch': xarch,
539                     'fields': xfields
540                 }
541                 attrs = {'views': views}
542                 fields = xfields
543             if node.get('name'):
544                 attrs = {}
545                 try:
546                     if node.get('name') in Model._columns:
547                         column = Model._columns[node.get('name')]
548                     else:
549                         column = Model._inherit_fields[node.get('name')][2]
550                 except Exception:
551                     column = False
552
553                 if column:
554                     children = False
555                     views = {}
556                     for f in node:
557                         if f.tag in ('form', 'tree', 'graph', 'kanban', 'calendar'):
558                             node.remove(f)
559                             ctx = context.copy()
560                             ctx['base_model_name'] = model
561                             xarch, xfields = self.postprocess_and_fields(cr, user, column._obj or None, f, view_id, ctx)
562                             views[str(f.tag)] = {
563                                 'arch': xarch,
564                                 'fields': xfields
565                             }
566                     attrs = {'views': views}
567                 fields[node.get('name')] = attrs
568
569                 field = model_fields.get(node.get('name'))
570                 if field:
571                     orm.transfer_field_to_modifiers(field, modifiers)
572
573         elif node.tag in ('form', 'tree'):
574             result = Model.view_header_get(cr, user, False, node.tag, context)
575             if result:
576                 node.set('string', result)
577             in_tree_view = node.tag == 'tree'
578
579         elif node.tag == 'calendar':
580             for additional_field in ('date_start', 'date_delay', 'date_stop', 'color', 'all_day', 'attendee'):
581                 if node.get(additional_field):
582                     fields[node.get(additional_field)] = {}
583
584         if not check_group(node):
585             # node must be removed, no need to proceed further with its children
586             return fields
587
588         # The view architeture overrides the python model.
589         # Get the attrs before they are (possibly) deleted by check_group below
590         orm.transfer_node_to_modifiers(node, modifiers, context, in_tree_view)
591
592         # TODO remove attrs counterpart in modifiers when invisible is true ?
593
594         # translate view
595         if 'lang' in context:
596             Translations = self.pool['ir.translation']
597             if node.text and node.text.strip():
598                 trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.text.strip())
599                 if trans:
600                     node.text = node.text.replace(node.text.strip(), trans)
601             if node.tail and node.tail.strip():
602                 trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.tail.strip())
603                 if trans:
604                     node.tail =  node.tail.replace(node.tail.strip(), trans)
605
606             if node.get('string') and not result:
607                 trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.get('string'))
608                 if trans == node.get('string') and ('base_model_name' in context):
609                     # If translation is same as source, perhaps we'd have more luck with the alternative model name
610                     # (in case we are in a mixed situation, such as an inherited view where parent_view.model != model
611                     trans = Translations._get_source(cr, user, context['base_model_name'], 'view', context['lang'], node.get('string'))
612                 if trans:
613                     node.set('string', trans)
614
615             for attr_name in ('confirm', 'sum', 'avg', 'help', 'placeholder'):
616                 attr_value = node.get(attr_name)
617                 if attr_value:
618                     trans = Translations._get_source(cr, user, model, 'view', context['lang'], attr_value)
619                     if trans:
620                         node.set(attr_name, trans)
621
622         for f in node:
623             if children or (node.tag == 'field' and f.tag in ('filter','separator')):
624                 fields.update(self.postprocess(cr, user, model, f, view_id, in_tree_view, model_fields, context))
625
626         orm.transfer_modifiers_to_node(modifiers, node)
627         return fields
628
629     def _disable_workflow_buttons(self, cr, user, model, node):
630         """ Set the buttons in node to readonly if the user can't activate them. """
631         if model is None or user == 1:
632             # admin user can always activate workflow buttons
633             return node
634
635         # TODO handle the case of more than one workflow for a model or multiple
636         # transitions with different groups and same signal
637         usersobj = self.pool.get('res.users')
638         buttons = (n for n in node.getiterator('button') if n.get('type') != 'object')
639         for button in buttons:
640             user_groups = usersobj.read(cr, user, [user], ['groups_id'])[0]['groups_id']
641             cr.execute("""SELECT DISTINCT t.group_id
642                         FROM wkf
643                   INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
644                   INNER JOIN wkf_transition t ON (t.act_to = a.id)
645                        WHERE wkf.osv = %s
646                          AND t.signal = %s
647                          AND t.group_id is NOT NULL
648                    """, (model, button.get('name')))
649             group_ids = [x[0] for x in cr.fetchall() if x[0]]
650             can_click = not group_ids or bool(set(user_groups).intersection(group_ids))
651             button.set('readonly', str(int(not can_click)))
652         return node
653
654     def postprocess_and_fields(self, cr, user, model, node, view_id, context=None):
655         """ Return an architecture and a description of all the fields.
656
657         The field description combines the result of fields_get() and
658         postprocess().
659
660         :param node: the architecture as as an etree
661         :return: a tuple (arch, fields) where arch is the given node as a
662             string and fields is the description of all the fields.
663
664         """
665         fields = {}
666         Model = self.pool.get(model)
667         if not Model:
668             self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model), view_id, context)
669
670         if node.tag == 'diagram':
671             if node.getchildren()[0].tag == 'node':
672                 node_model = self.pool[node.getchildren()[0].get('object')]
673                 node_fields = node_model.fields_get(cr, user, None, context)
674                 fields.update(node_fields)
675                 if not node.get("create") and not node_model.check_access_rights(cr, user, 'create', raise_exception=False):
676                     node.set("create", 'false')
677             if node.getchildren()[1].tag == 'arrow':
678                 arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context)
679                 fields.update(arrow_fields)
680         else:
681             fields = Model.fields_get(cr, user, None, context)
682
683         fields_def = self.postprocess(cr, user, model, node, view_id, False, fields, context=context)
684         node = self._disable_workflow_buttons(cr, user, model, node)
685         if node.tag in ('kanban', 'tree', 'form', 'gantt'):
686             for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')):
687                 if not node.get(action) and not Model.check_access_rights(cr, user, operation, raise_exception=False):
688                     node.set(action, 'false')
689         arch = etree.tostring(node, encoding="utf-8").replace('\t', '')
690         for k in fields.keys():
691             if k not in fields_def:
692                 del fields[k]
693         for field in fields_def:
694             if field == 'id':
695                 # sometime, the view may contain the (invisible) field 'id' needed for a domain (when 2 objects have cross references)
696                 fields['id'] = {'readonly': True, 'type': 'integer', 'string': 'ID'}
697             elif field in fields:
698                 fields[field].update(fields_def[field])
699             else:
700                 message = _("Field `%(field_name)s` does not exist") % \
701                                 dict(field_name=field)
702                 self.raise_view_error(cr, user, message, view_id, context)
703         return arch, fields
704
705     #------------------------------------------------------
706     # QWeb template views
707     #------------------------------------------------------
708     @tools.ormcache_context(accepted_keys=('lang','inherit_branding', 'editable', 'translatable'))
709     def read_template(self, cr, uid, xml_id, context=None):
710         if '.' not in xml_id:
711             raise ValueError('Invalid template id: %r' % (xml_id,))
712
713         view_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xml_id, raise_if_not_found=True)
714         arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch']
715         arch_tree = etree.fromstring(arch)
716
717         if 'lang' in context:
718             arch_tree = self.translate_qweb(cr, uid, view_id, arch_tree, context['lang'], context)
719
720         self.distribute_branding(arch_tree)
721         root = etree.Element('templates')
722         root.append(arch_tree)
723         arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
724         return arch
725
726     def clear_cache(self):
727         self.read_template.clear_cache(self)
728
729     def distribute_branding(self, e, branding=None, parent_xpath='',
730                             index_map=misc.ConstantMapping(1)):
731         if e.get('t-ignore') or e.tag == 'head':
732             # TODO: find a better name and check if we have a string to boolean helper
733             return
734
735         node_path = e.get('data-oe-xpath')
736         if node_path is None:
737             node_path = "%s/%s[%d]" % (parent_xpath, e.tag, index_map[e.tag])
738         if branding and not (e.get('data-oe-model') or e.get('t-field')):
739             e.attrib.update(branding)
740             e.set('data-oe-xpath', node_path)
741         if not e.get('data-oe-model'): return
742
743         # if a branded element contains branded elements distribute own
744         # branding to children unless it's t-raw, then just remove branding
745         # on current element
746         if e.tag == 't' or 't-raw' in e.attrib or \
747                 any(self.is_node_branded(child) for child in e.iterdescendants()):
748             distributed_branding = dict(
749                 (attribute, e.attrib.pop(attribute))
750                 for attribute in MOVABLE_BRANDING
751                 if e.get(attribute))
752
753             if 't-raw' not in e.attrib:
754                 # TODO: collections.Counter if remove p2.6 compat
755                 # running index by tag type, for XPath query generation
756                 indexes = collections.defaultdict(lambda: 0)
757                 for child in e.iterchildren(tag=etree.Element):
758                     if child.get('data-oe-xpath'):
759                         # injected by view inheritance, skip otherwise
760                         # generated xpath is incorrect
761                         continue
762                     indexes[child.tag] += 1
763                     self.distribute_branding(child, distributed_branding,
764                                              parent_xpath=node_path,
765                                              index_map=indexes)
766
767     def is_node_branded(self, node):
768         """ Finds out whether a node is branded or qweb-active (bears a
769         @data-oe-model or a @t-* *which is not t-field* as t-field does not
770         section out views)
771
772         :param node: an etree-compatible element to test
773         :type node: etree._Element
774         :rtype: boolean
775         """
776         return any(
777             (attr == 'data-oe-model' or (attr != 't-field' and attr.startswith('t-')))
778             for attr in node.attrib
779         )
780
781     def translate_qweb(self, cr, uid, id_, arch, lang, context=None):
782         # TODO: this should be moved in a place before inheritance is applied
783         #       but process() is only called on fields_view_get()
784         Translations = self.pool['ir.translation']
785         h = HTMLParser.HTMLParser()
786         def get_trans(text):
787             if not text or not text.strip():
788                 return None
789             text = h.unescape(text.strip())
790             if len(text) < 2 or (text.startswith('<!') and text.endswith('>')):
791                 return None
792             return Translations._get_source(cr, uid, 'website', 'view', lang, text, id_)
793
794         if arch.tag not in ['script']:
795             text = get_trans(arch.text)
796             if text:
797                 arch.text = arch.text.replace(arch.text.strip(), text)
798             tail = get_trans(arch.tail)
799             if tail:
800                 arch.tail = arch.tail.replace(arch.tail.strip(), tail)
801
802             for attr_name in ('title', 'alt', 'placeholder'):
803                 attr = get_trans(arch.get(attr_name))
804                 if attr:
805                     arch.set(attr_name, attr)
806             for node in arch.iterchildren("*"):
807                 self.translate_qweb(cr, uid, id_, node, lang, context)
808         return arch
809
810     @openerp.tools.ormcache()
811     def get_view_xmlid(self, cr, uid, id):
812         imd = self.pool['ir.model.data']
813         domain = [('model', '=', 'ir.ui.view'), ('res_id', '=', id)]
814         xmlid = imd.search_read(cr, uid, domain, ['module', 'name'])[0]
815         return '%s.%s' % (xmlid['module'], xmlid['name'])
816
817     def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
818         if isinstance(id_or_xml_id, list):
819             id_or_xml_id = id_or_xml_id[0]
820         tname = id_or_xml_id
821         if isinstance(tname, (int, long)):
822             tname = self.get_view_xmlid(cr, uid, tname)
823
824         if not context:
825             context = {}
826
827         if values is None:
828             values = dict()
829         qcontext = dict(
830             keep_query=keep_query,
831             request=request,
832             json=simplejson,
833             quote_plus=werkzeug.url_quote_plus,
834         )
835         qcontext.update(values)
836
837         def loader(name):
838             return self.read_template(cr, uid, name, context=context)
839
840         return self.pool[engine].render(cr, uid, tname, qcontext, loader=loader, context=context)
841
842     #------------------------------------------------------
843     # Misc
844     #------------------------------------------------------
845
846     def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
847         nodes=[]
848         nodes_name=[]
849         transitions=[]
850         start=[]
851         tres={}
852         labels={}
853         no_ancester=[]
854         blank_nodes = []
855
856         _Model_Obj = self.pool[model]
857         _Node_Obj = self.pool[node_obj]
858         _Arrow_Obj = self.pool[conn_obj]
859
860         for model_key,model_value in _Model_Obj._columns.items():
861                 if model_value._type=='one2many':
862                     if model_value._obj==node_obj:
863                         _Node_Field=model_key
864                         _Model_Field=model_value._fields_id
865                     flag=False
866                     for node_key,node_value in _Node_Obj._columns.items():
867                         if node_value._type=='one2many':
868                              if node_value._obj==conn_obj:
869                                  if src_node in _Arrow_Obj._columns and flag:
870                                     _Source_Field=node_key
871                                  if des_node in _Arrow_Obj._columns and not flag:
872                                     _Destination_Field=node_key
873                                     flag = True
874
875         datas = _Model_Obj.read(cr, uid, id, [],context)
876         for a in _Node_Obj.read(cr,uid,datas[_Node_Field],[]):
877             if a[_Source_Field] or a[_Destination_Field]:
878                 nodes_name.append((a['id'],a['name']))
879                 nodes.append(a['id'])
880             else:
881                 blank_nodes.append({'id': a['id'],'name':a['name']})
882
883             if a.has_key('flow_start') and a['flow_start']:
884                 start.append(a['id'])
885             else:
886                 if not a[_Source_Field]:
887                     no_ancester.append(a['id'])
888             for t in _Arrow_Obj.read(cr,uid, a[_Destination_Field],[]):
889                 transitions.append((a['id'], t[des_node][0]))
890                 tres[str(t['id'])] = (a['id'],t[des_node][0])
891                 label_string = ""
892                 if label:
893                     for lbl in eval(label):
894                         if t.has_key(tools.ustr(lbl)) and tools.ustr(t[lbl])=='False':
895                             label_string += ' '
896                         else:
897                             label_string = label_string + " " + tools.ustr(t[lbl])
898                 labels[str(t['id'])] = (a['id'],label_string)
899         g  = graph(nodes, transitions, no_ancester)
900         g.process(start)
901         g.scale(*scale)
902         result = g.result_get()
903         results = {}
904         for node in nodes_name:
905             results[str(node[0])] = result[node[0]]
906             results[str(node[0])]['name'] = node[1]
907         return {'nodes': results,
908                 'transitions': tres,
909                 'label' : labels,
910                 'blank_nodes': blank_nodes,
911                 'node_parent_field': _Model_Field,}
912
913     def _validate_custom_views(self, cr, uid, model):
914         """Validate architecture of custom views (= without xml id) for a given model.
915             This method is called at the end of registry update.
916         """
917         cr.execute("""SELECT max(v.id)
918                         FROM ir_ui_view v
919                    LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
920                        WHERE md.module IS NULL
921                          AND v.model = %s
922                     GROUP BY coalesce(v.inherit_id, v.id)
923                    """, (model,))
924
925         ids = map(itemgetter(0), cr.fetchall())
926         return self._check_xml(cr, uid, ids)
927
928 # vim:et: