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