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', 'data-oe-source-id']
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 name_get(self, cr, uid, ids, context=None):
72 return [(rec.id, rec.user_id.name) for rec in self.browse(cr, uid, ids, context=context)]
74 def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
78 ids = self.search(cr, user, [('user_id', operator, name)] + args, limit=limit)
79 return self.name_get(cr, user, ids, context=context)
80 return super(view_custom, self).name_search(cr, user, name, args=args, operator=operator, context=context, limit=limit)
83 def _auto_init(self, cr, context=None):
84 super(view_custom, self)._auto_init(cr, context)
85 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_custom_user_id_ref_id\'')
87 cr.execute('CREATE INDEX ir_ui_view_custom_user_id_ref_id ON ir_ui_view_custom (user_id, ref_id)')
89 def _hasclass(context, *cls):
90 """ Checks if the context node has all the classes passed as arguments
92 node_classes = set(context.context_node.attrib.get('class', '').split())
94 return node_classes.issuperset(cls)
96 xpath_utils = etree.FunctionNamespace(None)
97 xpath_utils['hasclass'] = _hasclass
102 def _get_model_data(self, cr, uid, ids, fname, args, context=None):
103 result = dict.fromkeys(ids, False)
104 IMD = self.pool['ir.model.data']
105 data_ids = IMD.search_read(cr, uid, [('res_id', 'in', ids), ('model', '=', 'ir.ui.view')], ['res_id'], context=context)
106 result.update(map(itemgetter('res_id', 'id'), data_ids))
109 def _views_from_model_data(self, cr, uid, ids, context=None):
110 IMD = self.pool['ir.model.data']
111 data_ids = IMD.search_read(cr, uid, [('id', 'in', ids), ('model', '=', 'ir.ui.view')], ['res_id'], context=context)
112 return map(itemgetter('res_id'), data_ids)
115 'name': fields.char('View Name', required=True),
116 'model': fields.char('Object', select=True),
117 'priority': fields.integer('Sequence', required=True),
118 'type': fields.selection([
122 ('calendar', 'Calendar'),
123 ('diagram','Diagram'),
125 ('kanban', 'Kanban'),
127 ('qweb', 'QWeb')], string='View Type'),
128 'arch': fields.text('View Architecture', required=True),
129 'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True),
130 'inherit_children_ids': fields.one2many('ir.ui.view','inherit_id', 'Inherit Views'),
131 'field_parent': fields.char('Child Field'),
132 'model_data_id': fields.function(_get_model_data, type='many2one', relation='ir.model.data', string="Model Data",
134 _name: (lambda s, c, u, i, ctx=None: i, None, 10),
135 'ir.model.data': (_views_from_model_data, ['model', 'res_id'], 10),
137 'xml_id': fields.function(osv.osv.get_xml_id, type='char', size=128, string="External ID",
138 help="ID of the view defined in xml file"),
139 'groups_id': fields.many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id',
140 string='Groups', help="If this field is empty, the view applies to all users. Otherwise, the view applies to the users of those groups only."),
141 'model_ids': fields.one2many('ir.model.data', 'res_id', domain=[('model','=','ir.ui.view')], auto_join=True),
142 'create_date': fields.datetime('Create Date', readonly=True),
143 'write_date': fields.datetime('Last Modification Date', readonly=True),
145 'mode': fields.selection(
146 [('primary', "Base view"), ('extension', "Extension View")],
147 string="View inheritance mode", required=True,
148 help="""Only applies if this view inherits from an other one (inherit_id is not False/Null).
150 * if extension (default), if this view is requested the closest primary view
151 is looked up (via inherit_id), then all views inheriting from it with this
152 view's model are applied
153 * if primary, the closest primary view is fully resolved (even if it uses a
154 different model than this one), then this view's inheritance specs
155 (<xpath/>) are applied, and the result is used as if it were this view's
158 'application': fields.selection([
159 ('always', "Always applied"),
160 ('enabled', "Optional, enabled"),
161 ('disabled', "Optional, disabled"),
163 required=True, string="Application status",
164 help="""If this view is inherited,
165 * if always, the view always extends its parent
166 * if enabled, the view currently extends its parent but can be disabled
167 * if disabled, the view currently does not extend its parent but can be enabled
172 'application': 'always',
175 _order = "priority,name"
177 # Holds the RNG schema
178 _relaxng_validator = None
181 if not self._relaxng_validator:
182 frng = tools.file_open(os.path.join('base','rng','view.rng'))
184 relaxng_doc = etree.parse(frng)
185 self._relaxng_validator = etree.RelaxNG(relaxng_doc)
187 _logger.exception('Failed to load RelaxNG XML schema for views validation')
190 return self._relaxng_validator
192 def _check_xml(self, cr, uid, ids, context=None):
195 context = dict(context, check_view_ids=ids)
197 # Sanity checks: the view should not break anything upon rendering!
198 # Any exception raised below will cause a transaction rollback.
199 for view in self.browse(cr, uid, ids, context):
200 view_def = self.read_combined(cr, uid, view.id, None, context=context)
201 view_arch_utf8 = view_def['arch']
202 if view.type != 'qweb':
203 view_doc = etree.fromstring(view_arch_utf8)
204 # verify that all fields used are valid, etc.
205 self.postprocess_and_fields(cr, uid, view.model, view_doc, view.id, context=context)
206 # RNG-based validation is not possible anymore with 7.0 forms
207 view_docs = [view_doc]
208 if view_docs[0].tag == 'data':
209 # A <data> element is a wrapper for multiple root nodes
210 view_docs = view_docs[0]
211 validator = self._relaxng()
212 for view_arch in view_docs:
213 if (view_arch.get('version') < '7.0') and validator and not validator.validate(view_arch):
214 for error in validator.error_log:
215 _logger.error(tools.ustr(error))
217 if not valid_view(view_arch):
223 "CHECK (mode != 'extension' OR inherit_id IS NOT NULL)",
224 "Invalid inheritance mode: if the mode is 'extension', the view must"
225 " extend an other view"),
228 (_check_xml, 'Invalid view definition', ['arch']),
231 def _auto_init(self, cr, context=None):
232 super(view, self)._auto_init(cr, context)
233 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_model_type_inherit_id\'')
234 if not cr.fetchone():
235 cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
237 def _compute_defaults(self, cr, uid, values, context=None):
238 if 'inherit_id' in values:
240 'mode', 'extension' if values['inherit_id'] else 'primary')
243 def create(self, cr, uid, values, context=None):
244 if 'type' not in values:
245 if values.get('inherit_id'):
246 values['type'] = self.browse(cr, uid, values['inherit_id'], context).type
248 values['type'] = etree.fromstring(values['arch']).tag
250 if not values.get('name'):
251 values['name'] = "%s %s" % (values.get('model'), values['type'])
253 self.read_template.clear_cache(self)
254 return super(view, self).create(
256 self._compute_defaults(cr, uid, values, context=context),
259 def write(self, cr, uid, ids, vals, context=None):
260 if not isinstance(ids, (list, tuple)):
265 # drop the corresponding view customizations (used for dashboards for example), otherwise
266 # not all users would see the updated views
267 custom_view_ids = self.pool.get('ir.ui.view.custom').search(cr, uid, [('ref_id', 'in', ids)])
269 self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
271 if vals.get('application') == 'disabled':
272 from_always = self.search(
273 cr, uid, [('id', 'in', ids), ('application', '=', 'always')], context=context)
276 "Can't disable views %s marked as always applied" % (
277 ', '.join(map(str, from_always))))
279 self.read_template.clear_cache(self)
280 ret = super(view, self).write(
282 self._compute_defaults(cr, uid, vals, context=context),
286 def toggle(self, cr, uid, ids, context=None):
287 """ Switches between enabled and disabled application statuses
289 for view in self.browse(cr, uid, ids, context=context):
290 if view.application == 'enabled':
291 view.write({'application': 'disabled'})
292 elif view.application == 'disabled':
293 view.write({'application': 'enabled'})
295 raise ValueError(_("Can't toggle view %d with application %r") % (
301 def copy(self, cr, uid, id, default=None, context=None):
307 return super(view, self).copy(cr, uid, id, default, context=context)
309 # default view selection
310 def default_view(self, cr, uid, model, view_type, context=None):
311 """ Fetches the default view for the provided (model, view_type) pair:
312 primary view with the lowest priority.
315 :param int view_type:
316 :return: id of the default view of False if none found
320 ['model', '=', model],
321 ['type', '=', view_type],
322 ['mode', '=', 'primary'],
324 ids = self.search(cr, uid, domain, limit=1, context=context)
329 #------------------------------------------------------
330 # Inheritance mecanism
331 #------------------------------------------------------
332 def get_inheriting_views_arch(self, cr, uid, view_id, model, context=None):
333 """Retrieves the architecture of views that inherit from the given view, from the sets of
334 views that should currently be used in the system. During the module upgrade phase it
335 may happen that a view is present in the database but the fields it relies on are not
336 fully loaded yet. This method only considers views that belong to modules whose code
337 is already loaded. Custom views defined directly in the database are loaded only
338 after the module initialization phase is completely finished.
340 :param int view_id: id of the view whose inheriting views should be retrieved
341 :param str model: model identifier of the inheriting views.
342 :rtype: list of tuples
343 :return: [(view_arch,view_id), ...]
346 user = self.pool['res.users'].browse(cr, 1, uid, context=context)
347 user_groups = frozenset(user.groups_id or ())
350 ['inherit_id', '=', view_id],
351 ['model', '=', model],
352 ['mode', '=', 'extension'],
353 ['application', 'in', ['always', 'enabled']],
356 # Module init currently in progress, only consider views from
357 # modules whose code is already loaded
360 ['model_ids.module', 'in', tuple(self.pool._init_modules)],
361 ['id', 'in', context and context.get('check_view_ids') or (0,)],
363 view_ids = self.search(cr, uid, conditions, context=context)
365 return [(view.arch, view.id)
366 for view in self.browse(cr, 1, view_ids, context)
367 if not (view.groups_id and user_groups.isdisjoint(view.groups_id))]
369 def raise_view_error(self, cr, uid, message, view_id, context=None):
370 view = self.browse(cr, uid, view_id, context)
372 message = ("%(msg)s\n\n" +
373 _("Error context:\nView `%(view_name)s`") +
374 "\n[view_id: %(viewid)s, xml_id: %(xmlid)s, "
375 "model: %(model)s, parent_id: %(parent)s]") % \
377 'view_name': view.name or not_avail,
378 'viewid': view_id or not_avail,
379 'xmlid': view.xml_id or not_avail,
380 'model': view.model or not_avail,
381 'parent': view.inherit_id.id or not_avail,
384 _logger.error(message)
385 raise AttributeError(message)
387 def locate_node(self, arch, spec):
388 """ Locate a node in a source (parent) architecture.
390 Given a complete source (parent) architecture (i.e. the field
391 `arch` in a view), and a 'spec' node (a node in an inheriting
392 view that specifies the location in the source view of what
393 should be changed), return (if it exists) the node in the
394 source view matching the specification.
396 :param arch: a parent architecture to modify
397 :param spec: a modifying node in an inheriting view
398 :return: a node in the source matching the spec
400 if spec.tag == 'xpath':
401 nodes = arch.xpath(spec.get('expr'))
402 return nodes[0] if nodes else None
403 elif spec.tag == 'field':
404 # Only compare the field name: a field can be only once in a given view
405 # at a given level (and for multilevel expressions, we should use xpath
406 # inheritance spec anyway).
407 for node in arch.iter('field'):
408 if node.get('name') == spec.get('name'):
412 for node in arch.iter(spec.tag):
413 if isinstance(node, SKIPPED_ELEMENT_TYPES):
415 if all(node.get(attr) == spec.get(attr) for attr in spec.attrib
416 if attr not in ('position','version')):
417 # Version spec should match parent's root element's version
418 if spec.get('version') and spec.get('version') != arch.get('version'):
423 def inherit_branding(self, specs_tree, view_id, root_id):
424 for node in specs_tree.iterchildren(tag=etree.Element):
425 xpath = node.getroottree().getpath(node)
426 if node.tag == 'data' or node.tag == 'xpath':
427 self.inherit_branding(node, view_id, root_id)
429 node.set('data-oe-id', str(view_id))
430 node.set('data-oe-source-id', str(root_id))
431 node.set('data-oe-xpath', xpath)
432 node.set('data-oe-model', 'ir.ui.view')
433 node.set('data-oe-field', 'arch')
437 def apply_inheritance_specs(self, cr, uid, source, specs_tree, inherit_id, context=None):
438 """ Apply an inheriting view (a descendant of the base view)
440 Apply to a source architecture all the spec nodes (i.e. nodes
441 describing where and what changes to apply to some parent
442 architecture) given by an inheriting view.
444 :param Element source: a parent architecture to modify
445 :param Elepect specs_tree: a modifying architecture in an inheriting view
446 :param inherit_id: the database id of specs_arch
447 :return: a modified source where the specs are applied
450 # Queue of specification nodes (i.e. nodes describing where and
451 # changes to apply to some parent architecture).
456 if isinstance(spec, SKIPPED_ELEMENT_TYPES):
458 if spec.tag == 'data':
459 specs += [c for c in spec]
461 node = self.locate_node(source, spec)
463 pos = spec.get('position', 'inside')
465 if node.getparent() is None:
466 source = copy.deepcopy(spec[0])
469 node.addprevious(child)
470 node.getparent().remove(node)
471 elif pos == 'attributes':
472 for child in spec.getiterator('attribute'):
473 attribute = (child.get('name'), child.text or None)
475 node.set(attribute[0], attribute[1])
476 elif attribute[0] in node.attrib:
477 del node.attrib[attribute[0]]
488 sib.addprevious(child)
489 elif pos == 'before':
490 node.addprevious(child)
492 self.raise_view_error(cr, uid, _("Invalid position attribute: '%s'") % pos, inherit_id, context=context)
495 ' %s="%s"' % (attr, spec.get(attr))
496 for attr in spec.attrib
497 if attr != 'position'
499 tag = "<%s%s>" % (spec.tag, attrs)
500 self.raise_view_error(cr, uid, _("Element '%s' cannot be located in parent view") % tag, inherit_id, context=context)
504 def apply_view_inheritance(self, cr, uid, source, source_id, model, root_id=None, context=None):
505 """ Apply all the (directly and indirectly) inheriting views.
507 :param source: a parent architecture to modify (with parent modifications already applied)
508 :param source_id: the database view_id of the parent view
509 :param model: the original model for which we create a view (not
510 necessarily the same as the source's model); only the inheriting
511 views with that specific model will be applied.
512 :return: a modified source where all the modifying architecture are applied
514 if context is None: context = {}
517 sql_inherit = self.pool['ir.ui.view'].get_inheriting_views_arch(cr, uid, source_id, model, context=context)
518 for (specs, view_id) in sql_inherit:
519 specs_tree = etree.fromstring(specs.encode('utf-8'))
520 if context.get('inherit_branding'):
521 self.inherit_branding(specs_tree, view_id, root_id)
522 source = self.apply_inheritance_specs(cr, uid, source, specs_tree, view_id, context=context)
523 source = self.apply_view_inheritance(cr, uid, source, view_id, model, root_id=root_id, context=context)
526 def read_combined(self, cr, uid, view_id, fields=None, context=None):
528 Utility function to get a view combined with its inherited views.
530 * Gets the top of the view tree if a sub-view is requested
531 * Applies all inherited archs on the root view
532 * Returns the view with all requested fields
533 .. note:: ``arch`` is always added to the fields list even if not
534 requested (similar to ``id``)
536 if context is None: context = {}
538 # if view_id is not a root view, climb back to the top.
539 base = v = self.browse(cr, uid, view_id, context=context)
540 while v.mode != 'primary':
544 # arch and model fields are always returned
546 fields = list(set(fields) | set(['arch', 'model']))
549 [view] = self.read(cr, uid, [root_id], fields=fields, context=context)
550 view_arch = etree.fromstring(view['arch'].encode('utf-8'))
552 arch_tree = view_arch
554 parent_view = self.read_combined(
555 cr, uid, v.inherit_id.id, fields=fields, context=context)
556 arch_tree = etree.fromstring(parent_view['arch'])
557 self.apply_inheritance_specs(
558 cr, uid, arch_tree, view_arch, parent_view['id'], context=context)
561 if context.get('inherit_branding'):
562 arch_tree.attrib.update({
563 'data-oe-model': 'ir.ui.view',
564 'data-oe-id': str(root_id),
565 'data-oe-field': 'arch',
568 # and apply inheritance
569 arch = self.apply_view_inheritance(
570 cr, uid, arch_tree, root_id, base.model, context=context)
572 return dict(view, arch=etree.tostring(arch, encoding='utf-8'))
574 #------------------------------------------------------
575 # Postprocessing: translation, groups and modifiers
576 #------------------------------------------------------
578 # - split postprocess so that it can be used instead of translate_qweb
579 # - remove group processing from ir_qweb
580 #------------------------------------------------------
581 def postprocess(self, cr, user, model, node, view_id, in_tree_view, model_fields, context=None):
582 """Return the description of the fields in the node.
584 In a normal call to this method, node is a complete view architecture
585 but it is actually possible to give some sub-node (this is used so
586 that the method can call itself recursively).
588 Originally, the field descriptions are drawn from the node itself.
589 But there is now some code calling fields_get() in order to merge some
590 of those information in the architecture.
600 Model = self.pool.get(model)
602 self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model),
606 if isinstance(s, unicode):
607 return s.encode('utf8')
610 def check_group(node):
611 """Apply group restrictions, may be set at view level or model level::
612 * at view level this means the element should be made invisible to
613 people who are not members
614 * at model level (exclusively for fields, obviously), this means
615 the field should be completely removed from the view, as it is
616 completely unavailable for non-members
618 :return: True if field should be included in the result of fields_view_get
620 if node.tag == 'field' and node.get('name') in Model._all_columns:
621 column = Model._all_columns[node.get('name')].column
622 if column.groups and not self.user_has_groups(
623 cr, user, groups=column.groups, context=context):
624 node.getparent().remove(node)
625 fields.pop(node.get('name'), None)
626 # no point processing view-level ``groups`` anymore, return
628 if node.get('groups'):
629 can_see = self.user_has_groups(
630 cr, user, groups=node.get('groups'), context=context)
632 node.set('invisible', '1')
633 modifiers['invisible'] = True
634 if 'attrs' in node.attrib:
635 del(node.attrib['attrs']) #avoid making field visible later
636 del(node.attrib['groups'])
639 if node.tag in ('field', 'node', 'arrow'):
640 if node.get('object'):
646 xml += etree.tostring(f, encoding="utf-8")
648 new_xml = etree.fromstring(encode(xml))
650 ctx['base_model_name'] = model
651 xarch, xfields = self.postprocess_and_fields(cr, user, node.get('object'), new_xml, view_id, ctx)
656 attrs = {'views': views}
661 if node.get('name') in Model._columns:
662 column = Model._columns[node.get('name')]
664 column = Model._inherit_fields[node.get('name')][2]
672 if f.tag in ('form', 'tree', 'graph', 'kanban', 'calendar'):
675 ctx['base_model_name'] = model
676 xarch, xfields = self.postprocess_and_fields(cr, user, column._obj or None, f, view_id, ctx)
677 views[str(f.tag)] = {
681 attrs = {'views': views}
682 fields[node.get('name')] = attrs
684 field = model_fields.get(node.get('name'))
686 orm.transfer_field_to_modifiers(field, modifiers)
688 elif node.tag in ('form', 'tree'):
689 result = Model.view_header_get(cr, user, False, node.tag, context)
691 node.set('string', result)
692 in_tree_view = node.tag == 'tree'
694 elif node.tag == 'calendar':
695 for additional_field in ('date_start', 'date_delay', 'date_stop', 'color', 'all_day', 'attendee'):
696 if node.get(additional_field):
697 fields[node.get(additional_field)] = {}
699 if not check_group(node):
700 # node must be removed, no need to proceed further with its children
703 # The view architeture overrides the python model.
704 # Get the attrs before they are (possibly) deleted by check_group below
705 orm.transfer_node_to_modifiers(node, modifiers, context, in_tree_view)
707 # TODO remove attrs counterpart in modifiers when invisible is true ?
710 if 'lang' in context:
711 Translations = self.pool['ir.translation']
712 if node.text and node.text.strip():
713 trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.text.strip())
715 node.text = node.text.replace(node.text.strip(), trans)
716 if node.tail and node.tail.strip():
717 trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.tail.strip())
719 node.tail = node.tail.replace(node.tail.strip(), trans)
721 if node.get('string') and not result:
722 trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.get('string'))
723 if trans == node.get('string') and ('base_model_name' in context):
724 # If translation is same as source, perhaps we'd have more luck with the alternative model name
725 # (in case we are in a mixed situation, such as an inherited view where parent_view.model != model
726 trans = Translations._get_source(cr, user, context['base_model_name'], 'view', context['lang'], node.get('string'))
728 node.set('string', trans)
730 for attr_name in ('confirm', 'sum', 'avg', 'help', 'placeholder'):
731 attr_value = node.get(attr_name)
733 trans = Translations._get_source(cr, user, model, 'view', context['lang'], attr_value)
735 node.set(attr_name, trans)
738 if children or (node.tag == 'field' and f.tag in ('filter','separator')):
739 fields.update(self.postprocess(cr, user, model, f, view_id, in_tree_view, model_fields, context))
741 orm.transfer_modifiers_to_node(modifiers, node)
744 def _disable_workflow_buttons(self, cr, user, model, node):
745 """ Set the buttons in node to readonly if the user can't activate them. """
746 if model is None or user == 1:
747 # admin user can always activate workflow buttons
750 # TODO handle the case of more than one workflow for a model or multiple
751 # transitions with different groups and same signal
752 usersobj = self.pool.get('res.users')
753 buttons = (n for n in node.getiterator('button') if n.get('type') != 'object')
754 for button in buttons:
755 user_groups = usersobj.read(cr, user, [user], ['groups_id'])[0]['groups_id']
756 cr.execute("""SELECT DISTINCT t.group_id
758 INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
759 INNER JOIN wkf_transition t ON (t.act_to = a.id)
762 AND t.group_id is NOT NULL
763 """, (model, button.get('name')))
764 group_ids = [x[0] for x in cr.fetchall() if x[0]]
765 can_click = not group_ids or bool(set(user_groups).intersection(group_ids))
766 button.set('readonly', str(int(not can_click)))
769 def postprocess_and_fields(self, cr, user, model, node, view_id, context=None):
770 """ Return an architecture and a description of all the fields.
772 The field description combines the result of fields_get() and
775 :param node: the architecture as as an etree
776 :return: a tuple (arch, fields) where arch is the given node as a
777 string and fields is the description of all the fields.
781 Model = self.pool.get(model)
783 self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model), view_id, context)
785 if node.tag == 'diagram':
786 if node.getchildren()[0].tag == 'node':
787 node_model = self.pool[node.getchildren()[0].get('object')]
788 node_fields = node_model.fields_get(cr, user, None, context)
789 fields.update(node_fields)
790 if not node.get("create") and not node_model.check_access_rights(cr, user, 'create', raise_exception=False):
791 node.set("create", 'false')
792 if node.getchildren()[1].tag == 'arrow':
793 arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context)
794 fields.update(arrow_fields)
796 fields = Model.fields_get(cr, user, None, context)
798 fields_def = self.postprocess(cr, user, model, node, view_id, False, fields, context=context)
799 node = self._disable_workflow_buttons(cr, user, model, node)
800 if node.tag in ('kanban', 'tree', 'form', 'gantt'):
801 for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')):
802 if not node.get(action) and not Model.check_access_rights(cr, user, operation, raise_exception=False):
803 node.set(action, 'false')
804 if node.tag in ('kanban'):
805 group_by_field = node.get('default_group_by')
806 if group_by_field and Model._all_columns.get(group_by_field):
807 group_by_column = Model._all_columns[group_by_field].column
808 if group_by_column._type == 'many2one':
809 group_by_model = Model.pool.get(group_by_column._obj)
810 for action, operation in (('group_create', 'create'), ('group_delete', 'unlink'), ('group_edit', 'write')):
811 if not node.get(action) and not group_by_model.check_access_rights(cr, user, operation, raise_exception=False):
812 node.set(action, 'false')
814 arch = etree.tostring(node, encoding="utf-8").replace('\t', '')
815 for k in fields.keys():
816 if k not in fields_def:
818 for field in fields_def:
820 # sometime, the view may contain the (invisible) field 'id' needed for a domain (when 2 objects have cross references)
821 fields['id'] = {'readonly': True, 'type': 'integer', 'string': 'ID'}
822 elif field in fields:
823 fields[field].update(fields_def[field])
825 message = _("Field `%(field_name)s` does not exist") % \
826 dict(field_name=field)
827 self.raise_view_error(cr, user, message, view_id, context)
830 #------------------------------------------------------
831 # QWeb template views
832 #------------------------------------------------------
833 @tools.ormcache_context(accepted_keys=('lang','inherit_branding', 'editable', 'translatable'))
834 def read_template(self, cr, uid, xml_id, context=None):
835 if isinstance(xml_id, (int, long)):
838 if '.' not in xml_id:
839 raise ValueError('Invalid template id: %r' % (xml_id,))
840 view_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xml_id, raise_if_not_found=True)
842 arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch']
843 arch_tree = etree.fromstring(arch)
845 if 'lang' in context:
846 arch_tree = self.translate_qweb(cr, uid, view_id, arch_tree, context['lang'], context)
848 self.distribute_branding(arch_tree)
849 root = etree.Element('templates')
850 root.append(arch_tree)
851 arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
854 def clear_cache(self):
855 self.read_template.clear_cache(self)
857 def _contains_branded(self, node):
858 return node.tag == 't'\
859 or 't-raw' in node.attrib\
860 or any(self.is_node_branded(child) for child in node.iterdescendants())
862 def _pop_view_branding(self, element):
863 distributed_branding = dict(
864 (attribute, element.attrib.pop(attribute))
865 for attribute in MOVABLE_BRANDING
866 if element.get(attribute))
867 return distributed_branding
869 def distribute_branding(self, e, branding=None, parent_xpath='',
870 index_map=misc.ConstantMapping(1)):
871 if e.get('t-ignore') or e.tag == 'head':
872 # remove any view branding possibly injected by inheritance
873 attrs = set(MOVABLE_BRANDING)
874 for descendant in e.iterdescendants(tag=etree.Element):
875 if not attrs.intersection(descendant.attrib): continue
876 self._pop_view_branding(descendant)
877 # TODO: find a better name and check if we have a string to boolean helper
880 node_path = e.get('data-oe-xpath')
881 if node_path is None:
882 node_path = "%s/%s[%d]" % (parent_xpath, e.tag, index_map[e.tag])
883 if branding and not (e.get('data-oe-model') or e.get('t-field')):
884 e.attrib.update(branding)
885 e.set('data-oe-xpath', node_path)
886 if not e.get('data-oe-model'): return
888 if set(('t-esc', 't-escf', 't-raw', 't-rawf')).intersection(e.attrib):
889 # nodes which fully generate their content and have no reason to
890 # be branded because they can not sensibly be edited
891 self._pop_view_branding(e)
892 elif self._contains_branded(e):
893 # if a branded element contains branded elements distribute own
894 # branding to children unless it's t-raw, then just remove branding
896 distributed_branding = self._pop_view_branding(e)
898 if 't-raw' not in e.attrib:
899 # TODO: collections.Counter if remove p2.6 compat
900 # running index by tag type, for XPath query generation
901 indexes = collections.defaultdict(lambda: 0)
902 for child in e.iterchildren(tag=etree.Element):
903 if child.get('data-oe-xpath'):
904 # injected by view inheritance, skip otherwise
905 # generated xpath is incorrect
906 self.distribute_branding(child)
908 indexes[child.tag] += 1
909 self.distribute_branding(
910 child, distributed_branding,
911 parent_xpath=node_path, index_map=indexes)
913 def is_node_branded(self, node):
914 """ Finds out whether a node is branded or qweb-active (bears a
915 @data-oe-model or a @t-* *which is not t-field* as t-field does not
918 :param node: an etree-compatible element to test
919 :type node: etree._Element
923 (attr == 'data-oe-model' or (attr != 't-field' and attr.startswith('t-')))
924 for attr in node.attrib
927 def translate_qweb(self, cr, uid, id_, arch, lang, context=None):
928 # TODO: this should be moved in a place before inheritance is applied
929 # but process() is only called on fields_view_get()
930 Translations = self.pool['ir.translation']
931 h = HTMLParser.HTMLParser()
933 if not text or not text.strip():
935 text = h.unescape(text.strip())
936 if len(text) < 2 or (text.startswith('<!') and text.endswith('>')):
938 return Translations._get_source(cr, uid, 'website', 'view', lang, text, id_)
940 if arch.tag not in ['script']:
941 text = get_trans(arch.text)
943 arch.text = arch.text.replace(arch.text.strip(), text)
944 tail = get_trans(arch.tail)
946 arch.tail = arch.tail.replace(arch.tail.strip(), tail)
948 for attr_name in ('title', 'alt', 'placeholder'):
949 attr = get_trans(arch.get(attr_name))
951 arch.set(attr_name, attr)
952 for node in arch.iterchildren("*"):
953 self.translate_qweb(cr, uid, id_, node, lang, context)
956 @openerp.tools.ormcache()
957 def get_view_xmlid(self, cr, uid, id):
958 imd = self.pool['ir.model.data']
959 domain = [('model', '=', 'ir.ui.view'), ('res_id', '=', id)]
960 xmlid = imd.search_read(cr, uid, domain, ['module', 'name'])[0]
961 return '%s.%s' % (xmlid['module'], xmlid['name'])
963 def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
964 if isinstance(id_or_xml_id, list):
965 id_or_xml_id = id_or_xml_id[0]
973 keep_query=keep_query,
974 request=request, # might be unbound if we're not in an httprequest context
975 debug=request.debug if request else False,
977 quote_plus=werkzeug.url_quote_plus,
980 relativedelta=relativedelta,
982 qcontext.update(values)
984 # TODO: remove this as soon as the following branch is merged
985 # lp:~openerp-dev/openerp-web/trunk-module-closure-style-msh
986 from openerp.addons.web.controllers.main import module_boot
987 qcontext['modules'] = simplejson.dumps(module_boot()) if request else None
990 return self.read_template(cr, uid, name, context=context)
992 return self.pool[engine].render(cr, uid, id_or_xml_id, qcontext, loader=loader, context=context)
994 #------------------------------------------------------
996 #------------------------------------------------------
998 def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
1008 _Model_Obj = self.pool[model]
1009 _Node_Obj = self.pool[node_obj]
1010 _Arrow_Obj = self.pool[conn_obj]
1012 for model_key,model_value in _Model_Obj._columns.items():
1013 if model_value._type=='one2many':
1014 if model_value._obj==node_obj:
1015 _Node_Field=model_key
1016 _Model_Field=model_value._fields_id
1018 for node_key,node_value in _Node_Obj._columns.items():
1019 if node_value._type=='one2many':
1020 if node_value._obj==conn_obj:
1021 if src_node in _Arrow_Obj._columns and flag:
1022 _Source_Field=node_key
1023 if des_node in _Arrow_Obj._columns and not flag:
1024 _Destination_Field=node_key
1027 datas = _Model_Obj.read(cr, uid, id, [],context)
1028 for a in _Node_Obj.read(cr,uid,datas[_Node_Field],[]):
1029 if a[_Source_Field] or a[_Destination_Field]:
1030 nodes_name.append((a['id'],a['name']))
1031 nodes.append(a['id'])
1033 blank_nodes.append({'id': a['id'],'name':a['name']})
1035 if a.has_key('flow_start') and a['flow_start']:
1036 start.append(a['id'])
1038 if not a[_Source_Field]:
1039 no_ancester.append(a['id'])
1040 for t in _Arrow_Obj.read(cr,uid, a[_Destination_Field],[]):
1041 transitions.append((a['id'], t[des_node][0]))
1042 tres[str(t['id'])] = (a['id'],t[des_node][0])
1045 for lbl in eval(label):
1046 if t.has_key(tools.ustr(lbl)) and tools.ustr(t[lbl])=='False':
1049 label_string = label_string + " " + tools.ustr(t[lbl])
1050 labels[str(t['id'])] = (a['id'],label_string)
1051 g = graph(nodes, transitions, no_ancester)
1054 result = g.result_get()
1056 for node in nodes_name:
1057 results[str(node[0])] = result[node[0]]
1058 results[str(node[0])]['name'] = node[1]
1059 return {'nodes': results,
1060 'transitions': tres,
1062 'blank_nodes': blank_nodes,
1063 'node_parent_field': _Model_Field,}
1065 def _validate_custom_views(self, cr, uid, model):
1066 """Validate architecture of custom views (= without xml id) for a given model.
1067 This method is called at the end of registry update.
1069 cr.execute("""SELECT max(v.id)
1071 LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
1072 WHERE md.module IS NULL
1074 GROUP BY coalesce(v.inherit_id, v.id)
1077 ids = map(itemgetter(0), cr.fetchall())
1078 return self._check_xml(cr, uid, ids)
1080 def _validate_module_views(self, cr, uid, module):
1081 """Validate architecture of all the views of a given module"""
1082 assert not self.pool._init or module in self.pool._init_modules
1086 # only validate the views that are still existing...
1087 xmlid_filter = "AND md.name IN %s"
1088 names = tuple(name for (xmod, name), (model, res_id) in self.pool.model_data_reference_ids.items() if xmod == module and model == self._name)
1090 # no views for this module, nothing to validate
1093 cr.execute("""SELECT max(v.id)
1095 LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
1096 WHERE md.module = %s
1098 GROUP BY coalesce(v.inherit_id, v.id)
1099 """.format(xmlid_filter), params)
1101 for vid, in cr.fetchall():
1102 if not self._check_xml(cr, uid, [vid]):
1103 self.raise_view_error(cr, uid, "Can't validate view", vid)