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