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