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