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