1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
25 from dateutil.relativedelta import relativedelta
30 from operator import itemgetter
35 from lxml import etree
38 from openerp import tools
39 from openerp.http import request
40 from openerp.osv import fields, osv, orm
41 from openerp.tools import graph, SKIPPED_ELEMENT_TYPES
42 from openerp.tools.safe_eval import safe_eval as eval
43 from openerp.tools.view_validation import valid_view
44 from openerp.tools import misc
45 from openerp.tools.translate import _
47 _logger = logging.getLogger(__name__)
49 MOVABLE_BRANDING = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-xpath']
51 def keep_query(*args, **kw):
52 if not args and not kw:
55 query_params = frozenset(werkzeug.url_decode(request.httprequest.query_string).keys())
56 for keep_param in args:
57 for param in fnmatch.filter(query_params, keep_param):
58 if param not in params and param in request.params:
59 params[param] = request.params[param]
60 return werkzeug.urls.url_encode(params)
62 class view_custom(osv.osv):
63 _name = 'ir.ui.view.custom'
64 _order = 'create_date desc' # search(limit=1) should return the last customization
66 'ref_id': fields.many2one('ir.ui.view', 'Original View', select=True, required=True, ondelete='cascade'),
67 'user_id': fields.many2one('res.users', 'User', select=True, required=True, ondelete='cascade'),
68 'arch': fields.text('View Architecture', required=True),
71 def _auto_init(self, cr, context=None):
72 super(view_custom, self)._auto_init(cr, context)
73 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_custom_user_id_ref_id\'')
75 cr.execute('CREATE INDEX ir_ui_view_custom_user_id_ref_id ON ir_ui_view_custom (user_id, ref_id)')
80 def _get_model_data(self, cr, uid, ids, fname, args, context=None):
81 result = dict.fromkeys(ids, False)
82 IMD = self.pool['ir.model.data']
83 data_ids = IMD.search_read(cr, uid, [('res_id', 'in', ids), ('model', '=', 'ir.ui.view')], ['res_id'], context=context)
84 result.update(map(itemgetter('res_id', 'id'), data_ids))
87 def _views_from_model_data(self, cr, uid, ids, context=None):
88 IMD = self.pool['ir.model.data']
89 data_ids = IMD.search_read(cr, uid, [('id', 'in', ids), ('model', '=', 'ir.ui.view')], ['res_id'], context=context)
90 return map(itemgetter('res_id'), data_ids)
93 'name': fields.char('View Name', required=True),
94 'model': fields.char('Object', select=True),
95 'priority': fields.integer('Sequence', required=True),
96 'type': fields.selection([
100 ('calendar', 'Calendar'),
101 ('diagram','Diagram'),
103 ('kanban', 'Kanban'),
105 ('qweb', 'QWeb')], string='View Type'),
106 'arch': fields.text('View Architecture', required=True),
107 'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True),
108 'inherit_children_ids': fields.one2many('ir.ui.view','inherit_id', 'Inherit Views'),
109 'field_parent': fields.char('Child Field'),
110 'model_data_id': fields.function(_get_model_data, type='many2one', relation='ir.model.data', string="Model Data",
112 _name: (lambda s, c, u, i, ctx=None: i, None, 10),
113 'ir.model.data': (_views_from_model_data, ['model', 'res_id'], 10),
115 'xml_id': fields.function(osv.osv.get_xml_id, type='char', size=128, string="External ID",
116 help="ID of the view defined in xml file"),
117 'groups_id': fields.many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id',
118 string='Groups', help="If this field is empty, the view applies to all users. Otherwise, the view applies to the users of those groups only."),
119 'model_ids': fields.one2many('ir.model.data', 'res_id', domain=[('model','=','ir.ui.view')], auto_join=True),
124 _order = "priority,name"
126 # Holds the RNG schema
127 _relaxng_validator = None
130 if not self._relaxng_validator:
131 frng = tools.file_open(os.path.join('base','rng','view.rng'))
133 relaxng_doc = etree.parse(frng)
134 self._relaxng_validator = etree.RelaxNG(relaxng_doc)
136 _logger.exception('Failed to load RelaxNG XML schema for views validation')
139 return self._relaxng_validator
141 def _check_xml(self, cr, uid, ids, context=None):
144 context = dict(context, check_view_ids=ids)
146 # Sanity checks: the view should not break anything upon rendering!
147 # Any exception raised below will cause a transaction rollback.
148 for view in self.browse(cr, uid, ids, context):
149 view_def = self.read_combined(cr, uid, view.id, None, context=context)
150 view_arch_utf8 = view_def['arch']
151 if view.type != 'qweb':
152 view_doc = etree.fromstring(view_arch_utf8)
153 # verify that all fields used are valid, etc.
154 self.postprocess_and_fields(cr, uid, view.model, view_doc, view.id, context=context)
155 # RNG-based validation is not possible anymore with 7.0 forms
156 view_docs = [view_doc]
157 if view_docs[0].tag == 'data':
158 # A <data> element is a wrapper for multiple root nodes
159 view_docs = view_docs[0]
160 validator = self._relaxng()
161 for view_arch in view_docs:
162 if (view_arch.get('version') < '7.0') and validator and not validator.validate(view_arch):
163 for error in validator.error_log:
164 _logger.error(tools.ustr(error))
166 if not valid_view(view_arch):
171 (_check_xml, 'Invalid view definition', ['arch'])
174 def _auto_init(self, cr, context=None):
175 super(view, self)._auto_init(cr, context)
176 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_model_type_inherit_id\'')
177 if not cr.fetchone():
178 cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
180 def create(self, cr, uid, values, context=None):
181 if 'type' not in values:
182 if values.get('inherit_id'):
183 values['type'] = self.browse(cr, uid, values['inherit_id'], context).type
185 values['type'] = etree.fromstring(values['arch']).tag
187 if not values.get('name'):
188 values['name'] = "%s %s" % (values['model'], values['type'])
190 self.read_template.clear_cache(self)
191 return super(view, self).create(cr, uid, values, context)
193 def write(self, cr, uid, ids, vals, context=None):
194 if not isinstance(ids, (list, tuple)):
199 # drop the corresponding view customizations (used for dashboards for example), otherwise
200 # not all users would see the updated views
201 custom_view_ids = self.pool.get('ir.ui.view.custom').search(cr, uid, [('ref_id', 'in', ids)])
203 self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
205 self.read_template.clear_cache(self)
206 ret = super(view, self).write(cr, uid, ids, vals, context)
208 # if arch is modified views become noupdatable
209 if 'arch' in vals and not context.get('install_mode', False):
210 # TODO: should be doable in a read and a write
211 for view_ in self.browse(cr, uid, ids, context=context):
212 if view_.model_data_id:
213 self.pool.get('ir.model.data').write(cr, openerp.SUPERUSER_ID, view_.model_data_id.id, {'noupdate': True})
216 def copy(self, cr, uid, id, default=None, context=None):
222 return super(view, self).copy(cr, uid, id, default, context=context)
224 # default view selection
225 def default_view(self, cr, uid, model, view_type, context=None):
226 """ Fetches the default view for the provided (model, view_type) pair:
227 view with no parent (inherit_id=Fase) with the lowest priority.
230 :param int view_type:
231 :return: id of the default view of False if none found
235 ['model', '=', model],
236 ['type', '=', view_type],
237 ['inherit_id', '=', False],
239 ids = self.search(cr, uid, domain, limit=1, order='priority', context=context)
244 #------------------------------------------------------
245 # Inheritance mecanism
246 #------------------------------------------------------
247 def get_inheriting_views_arch(self, cr, uid, view_id, model, context=None):
248 """Retrieves the architecture of views that inherit from the given view, from the sets of
249 views that should currently be used in the system. During the module upgrade phase it
250 may happen that a view is present in the database but the fields it relies on are not
251 fully loaded yet. This method only considers views that belong to modules whose code
252 is already loaded. Custom views defined directly in the database are loaded only
253 after the module initialization phase is completely finished.
255 :param int view_id: id of the view whose inheriting views should be retrieved
256 :param str model: model identifier of the inheriting views.
257 :rtype: list of tuples
258 :return: [(view_arch,view_id), ...]
261 user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id)
263 check_view_ids = context and context.get('check_view_ids') or (0,)
264 conditions = [['inherit_id', '=', view_id], ['model', '=', model]]
266 # Module init currently in progress, only consider views from
267 # modules whose code is already loaded
270 ['model_ids.module', 'in', tuple(self.pool._init_modules)],
271 ['id', 'in', check_view_ids],
273 view_ids = self.search(cr, uid, conditions, context=context)
275 return [(view.arch, view.id)
276 for view in self.browse(cr, 1, view_ids, context)
277 if not (view.groups_id and user_groups.isdisjoint(view.groups_id))]
279 def raise_view_error(self, cr, uid, message, view_id, context=None):
280 view = self.browse(cr, uid, view_id, context)
282 message = ("%(msg)s\n\n" +
283 _("Error context:\nView `%(view_name)s`") +
284 "\n[view_id: %(viewid)s, xml_id: %(xmlid)s, "
285 "model: %(model)s, parent_id: %(parent)s]") % \
287 'view_name': view.name or not_avail,
288 'viewid': view_id or not_avail,
289 'xmlid': view.xml_id or not_avail,
290 'model': view.model or not_avail,
291 'parent': view.inherit_id.id or not_avail,
294 _logger.error(message)
295 raise AttributeError(message)
297 def locate_node(self, arch, spec):
298 """ Locate a node in a source (parent) architecture.
300 Given a complete source (parent) architecture (i.e. the field
301 `arch` in a view), and a 'spec' node (a node in an inheriting
302 view that specifies the location in the source view of what
303 should be changed), return (if it exists) the node in the
304 source view matching the specification.
306 :param arch: a parent architecture to modify
307 :param spec: a modifying node in an inheriting view
308 :return: a node in the source matching the spec
310 if spec.tag == 'xpath':
311 nodes = arch.xpath(spec.get('expr'))
312 return nodes[0] if nodes else None
313 elif spec.tag == 'field':
314 # Only compare the field name: a field can be only once in a given view
315 # at a given level (and for multilevel expressions, we should use xpath
316 # inheritance spec anyway).
317 for node in arch.iter('field'):
318 if node.get('name') == spec.get('name'):
322 for node in arch.iter(spec.tag):
323 if isinstance(node, SKIPPED_ELEMENT_TYPES):
325 if all(node.get(attr) == spec.get(attr) for attr in spec.attrib
326 if attr not in ('position','version')):
327 # Version spec should match parent's root element's version
328 if spec.get('version') and spec.get('version') != arch.get('version'):
333 def inherit_branding(self, specs_tree, view_id, root_id):
334 for node in specs_tree.iterchildren(tag=etree.Element):
335 xpath = node.getroottree().getpath(node)
336 if node.tag == 'data' or node.tag == 'xpath':
337 self.inherit_branding(node, view_id, root_id)
339 node.set('data-oe-id', str(view_id))
340 node.set('data-oe-source-id', str(root_id))
341 node.set('data-oe-xpath', xpath)
342 node.set('data-oe-model', 'ir.ui.view')
343 node.set('data-oe-field', 'arch')
347 def apply_inheritance_specs(self, cr, uid, source, specs_tree, inherit_id, context=None):
348 """ Apply an inheriting view (a descendant of the base view)
350 Apply to a source architecture all the spec nodes (i.e. nodes
351 describing where and what changes to apply to some parent
352 architecture) given by an inheriting view.
354 :param Element source: a parent architecture to modify
355 :param Elepect specs_tree: a modifying architecture in an inheriting view
356 :param inherit_id: the database id of specs_arch
357 :return: a modified source where the specs are applied
360 # Queue of specification nodes (i.e. nodes describing where and
361 # changes to apply to some parent architecture).
366 if isinstance(spec, SKIPPED_ELEMENT_TYPES):
368 if spec.tag == 'data':
369 specs += [c for c in spec]
371 node = self.locate_node(source, spec)
373 pos = spec.get('position', 'inside')
375 if node.getparent() is None:
376 source = copy.deepcopy(spec[0])
379 node.addprevious(child)
380 node.getparent().remove(node)
381 elif pos == 'attributes':
382 for child in spec.getiterator('attribute'):
383 attribute = (child.get('name'), child.text and child.text.encode('utf8') or None)
385 node.set(attribute[0], attribute[1])
386 elif attribute[0] in node.attrib:
387 del node.attrib[attribute[0]]
398 sib.addprevious(child)
399 elif pos == 'before':
400 node.addprevious(child)
402 self.raise_view_error(cr, uid, _("Invalid position attribute: '%s'") % pos, inherit_id, context=context)
405 ' %s="%s"' % (attr, spec.get(attr))
406 for attr in spec.attrib
407 if attr != 'position'
409 tag = "<%s%s>" % (spec.tag, attrs)
410 self.raise_view_error(cr, uid, _("Element '%s' cannot be located in parent view") % tag, inherit_id, context=context)
414 def apply_view_inheritance(self, cr, uid, source, source_id, model, root_id=None, context=None):
415 """ Apply all the (directly and indirectly) inheriting views.
417 :param source: a parent architecture to modify (with parent modifications already applied)
418 :param source_id: the database view_id of the parent view
419 :param model: the original model for which we create a view (not
420 necessarily the same as the source's model); only the inheriting
421 views with that specific model will be applied.
422 :return: a modified source where all the modifying architecture are applied
424 if context is None: context = {}
427 sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model, context=context)
428 for (specs, view_id) in sql_inherit:
429 specs_tree = etree.fromstring(specs.encode('utf-8'))
430 if context.get('inherit_branding'):
431 self.inherit_branding(specs_tree, view_id, root_id)
432 source = self.apply_inheritance_specs(cr, uid, source, specs_tree, view_id, context=context)
433 source = self.apply_view_inheritance(cr, uid, source, view_id, model, root_id=root_id, context=context)
436 def read_combined(self, cr, uid, view_id, fields=None, context=None):
438 Utility function to get a view combined with its inherited views.
440 * Gets the top of the view tree if a sub-view is requested
441 * Applies all inherited archs on the root view
442 * Returns the view with all requested fields
443 .. note:: ``arch`` is always added to the fields list even if not
444 requested (similar to ``id``)
446 if context is None: context = {}
448 # if view_id is not a root view, climb back to the top.
449 base = v = self.browse(cr, uid, view_id, context=context)
454 # arch and model fields are always returned
456 fields = list(set(fields) | set(['arch', 'model']))
459 [view] = self.read(cr, uid, [root_id], fields=fields, context=context)
460 arch_tree = etree.fromstring(view['arch'].encode('utf-8'))
462 if context.get('inherit_branding'):
463 arch_tree.attrib.update({
464 'data-oe-model': 'ir.ui.view',
465 'data-oe-id': str(root_id),
466 'data-oe-field': 'arch',
469 # and apply inheritance
470 arch = self.apply_view_inheritance(
471 cr, uid, arch_tree, root_id, base.model, context=context)
473 return dict(view, arch=etree.tostring(arch, encoding='utf-8'))
475 #------------------------------------------------------
476 # Postprocessing: translation, groups and modifiers
477 #------------------------------------------------------
479 # - split postprocess so that it can be used instead of translate_qweb
480 # - remove group processing from ir_qweb
481 #------------------------------------------------------
482 def postprocess(self, cr, user, model, node, view_id, in_tree_view, model_fields, context=None):
483 """Return the description of the fields in the node.
485 In a normal call to this method, node is a complete view architecture
486 but it is actually possible to give some sub-node (this is used so
487 that the method can call itself recursively).
489 Originally, the field descriptions are drawn from the node itself.
490 But there is now some code calling fields_get() in order to merge some
491 of those information in the architecture.
501 Model = self.pool.get(model)
503 self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model),
507 if isinstance(s, unicode):
508 return s.encode('utf8')
511 def check_group(node):
512 """Apply group restrictions, may be set at view level or model level::
513 * at view level this means the element should be made invisible to
514 people who are not members
515 * at model level (exclusively for fields, obviously), this means
516 the field should be completely removed from the view, as it is
517 completely unavailable for non-members
519 :return: True if field should be included in the result of fields_view_get
521 if node.tag == 'field' and node.get('name') in Model._all_columns:
522 column = Model._all_columns[node.get('name')].column
523 if column.groups and not self.user_has_groups(
524 cr, user, groups=column.groups, context=context):
525 node.getparent().remove(node)
526 fields.pop(node.get('name'), None)
527 # no point processing view-level ``groups`` anymore, return
529 if node.get('groups'):
530 can_see = self.user_has_groups(
531 cr, user, groups=node.get('groups'), context=context)
533 node.set('invisible', '1')
534 modifiers['invisible'] = True
535 if 'attrs' in node.attrib:
536 del(node.attrib['attrs']) #avoid making field visible later
537 del(node.attrib['groups'])
540 if node.tag in ('field', 'node', 'arrow'):
541 if node.get('object'):
547 xml += etree.tostring(f, encoding="utf-8")
549 new_xml = etree.fromstring(encode(xml))
551 ctx['base_model_name'] = model
552 xarch, xfields = self.postprocess_and_fields(cr, user, node.get('object'), new_xml, view_id, ctx)
557 attrs = {'views': views}
562 if node.get('name') in Model._columns:
563 column = Model._columns[node.get('name')]
565 column = Model._inherit_fields[node.get('name')][2]
573 if f.tag in ('form', 'tree', 'graph', 'kanban', 'calendar'):
576 ctx['base_model_name'] = model
577 xarch, xfields = self.postprocess_and_fields(cr, user, column._obj or None, f, view_id, ctx)
578 views[str(f.tag)] = {
582 attrs = {'views': views}
583 fields[node.get('name')] = attrs
585 field = model_fields.get(node.get('name'))
587 orm.transfer_field_to_modifiers(field, modifiers)
589 elif node.tag in ('form', 'tree'):
590 result = Model.view_header_get(cr, user, False, node.tag, context)
592 node.set('string', result)
593 in_tree_view = node.tag == 'tree'
595 elif node.tag == 'calendar':
596 for additional_field in ('date_start', 'date_delay', 'date_stop', 'color', 'all_day', 'attendee'):
597 if node.get(additional_field):
598 fields[node.get(additional_field)] = {}
600 if not check_group(node):
601 # node must be removed, no need to proceed further with its children
604 # The view architeture overrides the python model.
605 # Get the attrs before they are (possibly) deleted by check_group below
606 orm.transfer_node_to_modifiers(node, modifiers, context, in_tree_view)
608 # TODO remove attrs counterpart in modifiers when invisible is true ?
611 if 'lang' in context:
612 Translations = self.pool['ir.translation']
613 if node.text and node.text.strip():
614 trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.text.strip())
616 node.text = node.text.replace(node.text.strip(), trans)
617 if node.tail and node.tail.strip():
618 trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.tail.strip())
620 node.tail = node.tail.replace(node.tail.strip(), trans)
622 if node.get('string') and not result:
623 trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.get('string'))
624 if trans == node.get('string') and ('base_model_name' in context):
625 # If translation is same as source, perhaps we'd have more luck with the alternative model name
626 # (in case we are in a mixed situation, such as an inherited view where parent_view.model != model
627 trans = Translations._get_source(cr, user, context['base_model_name'], 'view', context['lang'], node.get('string'))
629 node.set('string', trans)
631 for attr_name in ('confirm', 'sum', 'avg', 'help', 'placeholder'):
632 attr_value = node.get(attr_name)
634 trans = Translations._get_source(cr, user, model, 'view', context['lang'], attr_value)
636 node.set(attr_name, trans)
639 if children or (node.tag == 'field' and f.tag in ('filter','separator')):
640 fields.update(self.postprocess(cr, user, model, f, view_id, in_tree_view, model_fields, context))
642 orm.transfer_modifiers_to_node(modifiers, node)
645 def _disable_workflow_buttons(self, cr, user, model, node):
646 """ Set the buttons in node to readonly if the user can't activate them. """
647 if model is None or user == 1:
648 # admin user can always activate workflow buttons
651 # TODO handle the case of more than one workflow for a model or multiple
652 # transitions with different groups and same signal
653 usersobj = self.pool.get('res.users')
654 buttons = (n for n in node.getiterator('button') if n.get('type') != 'object')
655 for button in buttons:
656 user_groups = usersobj.read(cr, user, [user], ['groups_id'])[0]['groups_id']
657 cr.execute("""SELECT DISTINCT t.group_id
659 INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
660 INNER JOIN wkf_transition t ON (t.act_to = a.id)
663 AND t.group_id is NOT NULL
664 """, (model, button.get('name')))
665 group_ids = [x[0] for x in cr.fetchall() if x[0]]
666 can_click = not group_ids or bool(set(user_groups).intersection(group_ids))
667 button.set('readonly', str(int(not can_click)))
670 def postprocess_and_fields(self, cr, user, model, node, view_id, context=None):
671 """ Return an architecture and a description of all the fields.
673 The field description combines the result of fields_get() and
676 :param node: the architecture as as an etree
677 :return: a tuple (arch, fields) where arch is the given node as a
678 string and fields is the description of all the fields.
682 Model = self.pool.get(model)
684 self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model), view_id, context)
686 if node.tag == 'diagram':
687 if node.getchildren()[0].tag == 'node':
688 node_model = self.pool[node.getchildren()[0].get('object')]
689 node_fields = node_model.fields_get(cr, user, None, context)
690 fields.update(node_fields)
691 if not node.get("create") and not node_model.check_access_rights(cr, user, 'create', raise_exception=False):
692 node.set("create", 'false')
693 if node.getchildren()[1].tag == 'arrow':
694 arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context)
695 fields.update(arrow_fields)
697 fields = Model.fields_get(cr, user, None, context)
699 fields_def = self.postprocess(cr, user, model, node, view_id, False, fields, context=context)
700 node = self._disable_workflow_buttons(cr, user, model, node)
701 if node.tag in ('kanban', 'tree', 'form', 'gantt'):
702 for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')):
703 if not node.get(action) and not Model.check_access_rights(cr, user, operation, raise_exception=False):
704 node.set(action, 'false')
705 arch = etree.tostring(node, encoding="utf-8").replace('\t', '')
706 for k in fields.keys():
707 if k not in fields_def:
709 for field in fields_def:
711 # sometime, the view may contain the (invisible) field 'id' needed for a domain (when 2 objects have cross references)
712 fields['id'] = {'readonly': True, 'type': 'integer', 'string': 'ID'}
713 elif field in fields:
714 fields[field].update(fields_def[field])
716 message = _("Field `%(field_name)s` does not exist") % \
717 dict(field_name=field)
718 self.raise_view_error(cr, user, message, view_id, context)
721 #------------------------------------------------------
722 # QWeb template views
723 #------------------------------------------------------
724 @tools.ormcache_context(accepted_keys=('lang','inherit_branding', 'editable', 'translatable'))
725 def read_template(self, cr, uid, xml_id, context=None):
726 if '.' not in xml_id:
727 raise ValueError('Invalid template id: %r' % (xml_id,))
729 view_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xml_id, raise_if_not_found=True)
730 arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch']
731 arch_tree = etree.fromstring(arch)
733 if 'lang' in context:
734 arch_tree = self.translate_qweb(cr, uid, view_id, arch_tree, context['lang'], context)
736 self.distribute_branding(arch_tree)
737 root = etree.Element('templates')
738 root.append(arch_tree)
739 arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
742 def clear_cache(self):
743 self.read_template.clear_cache(self)
745 def _contains_branded(self, node):
746 return node.tag == 't'\
747 or 't-raw' in node.attrib\
748 or any(self.is_node_branded(child) for child in node.iterdescendants())
750 def _pop_view_branding(self, element):
751 distributed_branding = dict(
752 (attribute, element.attrib.pop(attribute))
753 for attribute in MOVABLE_BRANDING
754 if element.get(attribute))
755 return distributed_branding
757 def distribute_branding(self, e, branding=None, parent_xpath='',
758 index_map=misc.ConstantMapping(1)):
759 if e.get('t-ignore') or e.tag == 'head':
760 # remove any view branding possibly injected by inheritance
761 attrs = set(MOVABLE_BRANDING)
762 for descendant in e.iterdescendants(tag=etree.Element):
763 if not attrs.intersection(descendant.attrib): continue
764 self._pop_view_branding(descendant)
765 # TODO: find a better name and check if we have a string to boolean helper
768 node_path = e.get('data-oe-xpath')
769 if node_path is None:
770 node_path = "%s/%s[%d]" % (parent_xpath, e.tag, index_map[e.tag])
771 if branding and not (e.get('data-oe-model') or e.get('t-field')):
772 e.attrib.update(branding)
773 e.set('data-oe-xpath', node_path)
774 if not e.get('data-oe-model'): return
776 if set(('t-esc', 't-escf', 't-raw', 't-rawf')).intersection(e.attrib):
777 # nodes which fully generate their content and have no reason to
778 # be branded because they can not sensibly be edited
779 self._pop_view_branding(e)
780 elif self._contains_branded(e):
781 # if a branded element contains branded elements distribute own
782 # branding to children unless it's t-raw, then just remove branding
784 distributed_branding = self._pop_view_branding(e)
786 if 't-raw' not in e.attrib:
787 # TODO: collections.Counter if remove p2.6 compat
788 # running index by tag type, for XPath query generation
789 indexes = collections.defaultdict(lambda: 0)
790 for child in e.iterchildren(tag=etree.Element):
791 if child.get('data-oe-xpath'):
792 # injected by view inheritance, skip otherwise
793 # generated xpath is incorrect
794 self.distribute_branding(child)
796 indexes[child.tag] += 1
797 self.distribute_branding(
798 child, distributed_branding,
799 parent_xpath=node_path, index_map=indexes)
801 def is_node_branded(self, node):
802 """ Finds out whether a node is branded or qweb-active (bears a
803 @data-oe-model or a @t-* *which is not t-field* as t-field does not
806 :param node: an etree-compatible element to test
807 :type node: etree._Element
811 (attr == 'data-oe-model' or (attr != 't-field' and attr.startswith('t-')))
812 for attr in node.attrib
815 def translate_qweb(self, cr, uid, id_, arch, lang, context=None):
816 # TODO: this should be moved in a place before inheritance is applied
817 # but process() is only called on fields_view_get()
818 Translations = self.pool['ir.translation']
819 h = HTMLParser.HTMLParser()
821 if not text or not text.strip():
823 text = h.unescape(text.strip())
824 if len(text) < 2 or (text.startswith('<!') and text.endswith('>')):
826 return Translations._get_source(cr, uid, 'website', 'view', lang, text, id_)
828 if arch.tag not in ['script']:
829 text = get_trans(arch.text)
831 arch.text = arch.text.replace(arch.text.strip(), text)
832 tail = get_trans(arch.tail)
834 arch.tail = arch.tail.replace(arch.tail.strip(), tail)
836 for attr_name in ('title', 'alt', 'placeholder'):
837 attr = get_trans(arch.get(attr_name))
839 arch.set(attr_name, attr)
840 for node in arch.iterchildren("*"):
841 self.translate_qweb(cr, uid, id_, node, lang, context)
844 @openerp.tools.ormcache()
845 def get_view_xmlid(self, cr, uid, id):
846 imd = self.pool['ir.model.data']
847 domain = [('model', '=', 'ir.ui.view'), ('res_id', '=', id)]
848 xmlid = imd.search_read(cr, uid, domain, ['module', 'name'])[0]
849 return '%s.%s' % (xmlid['module'], xmlid['name'])
851 def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
852 if isinstance(id_or_xml_id, list):
853 id_or_xml_id = id_or_xml_id[0]
855 if isinstance(tname, (int, long)):
856 tname = self.get_view_xmlid(cr, uid, tname)
864 keep_query=keep_query,
867 quote_plus=werkzeug.url_quote_plus,
870 relativedelta=relativedelta,
872 qcontext.update(values)
875 return self.read_template(cr, uid, name, context=context)
877 return self.pool[engine].render(cr, uid, tname, qcontext, loader=loader, context=context)
879 #------------------------------------------------------
881 #------------------------------------------------------
883 def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
893 _Model_Obj = self.pool[model]
894 _Node_Obj = self.pool[node_obj]
895 _Arrow_Obj = self.pool[conn_obj]
897 for model_key,model_value in _Model_Obj._columns.items():
898 if model_value._type=='one2many':
899 if model_value._obj==node_obj:
900 _Node_Field=model_key
901 _Model_Field=model_value._fields_id
903 for node_key,node_value in _Node_Obj._columns.items():
904 if node_value._type=='one2many':
905 if node_value._obj==conn_obj:
906 if src_node in _Arrow_Obj._columns and flag:
907 _Source_Field=node_key
908 if des_node in _Arrow_Obj._columns and not flag:
909 _Destination_Field=node_key
912 datas = _Model_Obj.read(cr, uid, id, [],context)
913 for a in _Node_Obj.read(cr,uid,datas[_Node_Field],[]):
914 if a[_Source_Field] or a[_Destination_Field]:
915 nodes_name.append((a['id'],a['name']))
916 nodes.append(a['id'])
918 blank_nodes.append({'id': a['id'],'name':a['name']})
920 if a.has_key('flow_start') and a['flow_start']:
921 start.append(a['id'])
923 if not a[_Source_Field]:
924 no_ancester.append(a['id'])
925 for t in _Arrow_Obj.read(cr,uid, a[_Destination_Field],[]):
926 transitions.append((a['id'], t[des_node][0]))
927 tres[str(t['id'])] = (a['id'],t[des_node][0])
930 for lbl in eval(label):
931 if t.has_key(tools.ustr(lbl)) and tools.ustr(t[lbl])=='False':
934 label_string = label_string + " " + tools.ustr(t[lbl])
935 labels[str(t['id'])] = (a['id'],label_string)
936 g = graph(nodes, transitions, no_ancester)
939 result = g.result_get()
941 for node in nodes_name:
942 results[str(node[0])] = result[node[0]]
943 results[str(node[0])]['name'] = node[1]
944 return {'nodes': results,
947 'blank_nodes': blank_nodes,
948 'node_parent_field': _Model_Field,}
950 def _validate_custom_views(self, cr, uid, model):
951 """Validate architecture of custom views (= without xml id) for a given model.
952 This method is called at the end of registry update.
954 cr.execute("""SELECT max(v.id)
956 LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
957 WHERE md.module IS NULL
959 GROUP BY coalesce(v.inherit_id, v.id)
962 ids = map(itemgetter(0), cr.fetchall())
963 return self._check_xml(cr, uid, ids)