1 # -*- coding: utf-8 -*-
5 from xml.etree import ElementTree
6 from cStringIO import StringIO
12 import openerpweb.nonliterals
16 # Should move to openerpweb.Xml2Json
19 # Simple and straightforward XML-to-JSON converter in Python
22 # URL: http://code.google.com/p/xml2json-direct/
24 def convert_to_json(s):
25 return simplejson.dumps(
26 Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
29 def convert_to_structure(s):
30 root = ElementTree.fromstring(s)
31 return Xml2Json.convert_element(root)
34 def convert_element(el, skip_whitespaces=True):
37 ns, name = el.tag.rsplit("}", 1)
39 res["namespace"] = ns[1:]
43 for k, v in el.items():
46 if el.text and (not skip_whitespaces or el.text.strip() != ''):
49 kids.append(Xml2Json.convert_element(kid))
50 if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
52 res["children"] = kids
55 #----------------------------------------------------------
56 # OpenERP Web base Controllers
57 #----------------------------------------------------------
59 class Session(openerpweb.Controller):
60 _cp_path = "/base/session"
62 def manifest_glob(self, addons, key):
65 globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
68 resource_path[len(openerpweb.path_addons):]
69 for pattern in globlist
70 for resource_path in glob.glob(os.path.join(
71 openerpweb.path_addons, addon, pattern))
75 def concat_files(self, file_list):
76 """ Concatenate file content
77 return (concat,timestamp)
78 concat: concatenation of file content
79 timestamp: max(os.path.getmtime of file_list)
81 root = openerpweb.path_root
85 fname = os.path.join(root, i)
86 ftime = os.path.getmtime(fname)
87 if ftime > files_timestamp:
88 files_timestamp = ftime
89 files_content = open(fname).read()
90 files_concat = "".join(files_content)
93 @openerpweb.jsonrequest
94 def login(self, req, db, login, password):
95 req.session.login(db, login, password)
98 "session_id": req.session_id,
99 "uid": req.session._uid,
102 @openerpweb.jsonrequest
103 def sc_list(self, req):
104 return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
105 req.session.eval_context(req.context))
107 @openerpweb.jsonrequest
108 def get_databases_list(self, req):
109 proxy = req.session.proxy("db")
112 return {"db_list": dbs}
114 @openerpweb.jsonrequest
115 def get_lang_list(self, req):
116 lang_list = [('en_US', 'English (US)')]
118 lang_list = lang_list + (req.session.proxy("db").list_lang() or [])
121 return {"lang_list": lang_list}
123 @openerpweb.jsonrequest
124 def drop_db(self, req, **kw):
127 password = kw.get('password')
129 res = req.session.proxy("db").drop(password, db)
135 @openerpweb.jsonrequest
136 def modules(self, req):
137 return {"modules": [name
138 for name, manifest in openerpweb.addons_manifest.iteritems()
139 if manifest.get('active', True)]}
141 @openerpweb.jsonrequest
142 def csslist(self, req, mods='base'):
143 return {'files': self.manifest_glob(mods.split(','), 'css')}
145 @openerpweb.jsonrequest
146 def jslist(self, req, mods='base'):
147 return {'files': self.manifest_glob(mods.split(','), 'js')}
149 def css(self, req, mods='base,base_hello'):
150 files = self.manifest_glob(mods.split(','), 'css')
151 concat = self.concat_files(files)[0]
152 # TODO request set the Date of last modif and Etag
156 def js(self, req, mods='base,base_hello'):
157 files = self.manifest_glob(mods.split(','), 'js')
158 concat = self.concat_files(files)[0]
159 # TODO request set the Date of last modif and Etag
163 @openerpweb.jsonrequest
164 def eval_domain_and_context(self, req, contexts, domains,
166 """ Evaluates sequences of domains and contexts, composing them into
167 a single context, domain or group_by sequence.
169 :param list contexts: list of contexts to merge together. Contexts are
170 evaluated in sequence, all previous contexts
171 are part of their own evaluation context
172 (starting at the session context).
173 :param list domains: list of domains to merge together. Domains are
174 evaluated in sequence and appended to one another
175 (implicit AND), their evaluation domain is the
176 result of merging all contexts.
177 :param list group_by_seq: list of domains (which may be in a different
178 order than the ``contexts`` parameter),
179 evaluated in sequence, their ``'group_by'``
180 key is extracted if they have one.
185 the global context created by merging all of
189 the concatenation of all domains
192 a list of fields to group by, potentially empty (in which case
193 no group by should be performed)
195 context, domain = eval_context_and_domain(req.session,
196 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
197 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
199 group_by_sequence = []
200 for candidate in (group_by_seq or []):
201 ctx = req.session.eval_context(candidate, context)
202 group_by = ctx.get('group_by')
205 elif isinstance(group_by, basestring):
206 group_by_sequence.append(group_by)
208 group_by_sequence.extend(group_by)
213 'group_by': group_by_sequence
216 @openerpweb.jsonrequest
217 def save_session_action(self, req, the_action):
219 This method store an action object in the session object and returns an integer
220 identifying that action. The method get_session_action() can be used to get
223 :param the_action: The action to save in the session.
224 :type the_action: anything
225 :return: A key identifying the saved action.
228 saved_actions = cherrypy.session.get('saved_actions')
229 if not saved_actions:
230 saved_actions = {"next":0, "actions":{}}
231 cherrypy.session['saved_actions'] = saved_actions
232 # we don't allow more than 10 stored actions
233 if len(saved_actions["actions"]) >= 10:
234 del saved_actions["actions"][min(saved_actions["actions"].keys())]
235 key = saved_actions["next"]
236 saved_actions["actions"][key] = the_action
237 saved_actions["next"] = key + 1
240 @openerpweb.jsonrequest
241 def get_session_action(self, req, key):
243 Gets back a previously saved action. This method can return None if the action
244 was saved since too much time (this case should be handled in a smart way).
246 :param key: The key given by save_session_action()
248 :return: The saved action or None.
251 saved_actions = cherrypy.session.get('saved_actions')
252 if not saved_actions:
254 return saved_actions["actions"].get(key)
256 def eval_context_and_domain(session, context, domain=None):
257 e_context = session.eval_context(context)
258 # should we give the evaluated context as an evaluation context to the domain?
259 e_domain = session.eval_domain(domain or [])
261 return e_context, e_domain
263 def load_actions_from_ir_values(req, key, key2, models, meta, context):
264 Values = req.session.model('ir.values')
265 actions = Values.get(key, key2, models, meta, context)
267 return [(id, name, clean_action(action, req.session))
268 for id, name, action in actions]
270 def clean_action(action, session):
271 if action['type'] != 'ir.actions.act_window':
273 # values come from the server, we can just eval them
274 if isinstance(action.get('context', None), basestring):
275 action['context'] = eval(
277 session.evaluation_context()) or {}
279 if isinstance(action.get('domain', None), basestring):
280 action['domain'] = eval(
282 session.evaluation_context(
283 action['context'])) or []
284 if 'flags' not in action:
285 # Set empty flags dictionary for web client.
286 action['flags'] = dict()
287 return fix_view_modes(action)
289 def generate_views(action):
291 While the server generates a sequence called "views" computing dependencies
292 between a bunch of stuff for views coming directly from the database
293 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
294 to return custom view dictionaries generated on the fly.
296 In that case, there is no ``views`` key available on the action.
298 Since the web client relies on ``action['views']``, generate it here from
299 ``view_mode`` and ``view_id``.
301 Currently handles two different cases:
303 * no view_id, multiple view_mode
304 * single view_id, single view_mode
306 :param dict action: action descriptor dictionary to generate a views key for
308 view_id = action.get('view_id', False)
309 if isinstance(view_id, (list, tuple)):
312 # providing at least one view mode is a requirement, not an option
313 view_modes = action['view_mode'].split(',')
315 if len(view_modes) > 1:
317 raise ValueError('Non-db action dictionaries should provide '
318 'either multiple view modes or a single view '
319 'mode and an optional view id.\n\n Got view '
320 'modes %r and view id %r for action %r' % (
321 view_modes, view_id, action))
322 action['views'] = [(False, mode) for mode in view_modes]
324 action['views'] = [(view_id, view_modes[0])]
327 def fix_view_modes(action):
328 """ For historical reasons, OpenERP has weird dealings in relation to
329 view_mode and the view_type attribute (on window actions):
331 * one of the view modes is ``tree``, which stands for both list views
333 * the choice is made by checking ``view_type``, which is either
334 ``form`` for a list view or ``tree`` for an actual tree view
336 This methods simply folds the view_type into view_mode by adding a
337 new view mode ``list`` which is the result of the ``tree`` view_mode
338 in conjunction with the ``form`` view_type.
340 TODO: this should go into the doc, some kind of "peculiarities" section
342 :param dict action: an action descriptor
343 :returns: nothing, the action is modified in place
345 if 'views' not in action:
346 generate_views(action)
348 if action.pop('view_type') != 'form':
352 [id, mode if mode != 'tree' else 'list']
353 for id, mode in action['views']
358 class Menu(openerpweb.Controller):
359 _cp_path = "/base/menu"
361 @openerpweb.jsonrequest
363 return {'data': self.do_load(req)}
365 def do_load(self, req):
366 """ Loads all menu items (all applications and their sub-menus).
368 :param req: A request object, with an OpenERP session attribute
369 :type req: < session -> OpenERPSession >
370 :return: the menu root
371 :rtype: dict('children': menu_nodes)
373 Menus = req.session.model('ir.ui.menu')
374 # menus are loaded fully unlike a regular tree view, cause there are
375 # less than 512 items
376 context = req.session.eval_context(req.context)
377 menu_ids = Menus.search([], 0, False, False, context)
378 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
379 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
380 menu_items.append(menu_root)
382 # make a tree using parent_id
383 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
384 for menu_item in menu_items:
385 if menu_item['parent_id']:
386 parent = menu_item['parent_id'][0]
389 if parent in menu_items_map:
390 menu_items_map[parent].setdefault(
391 'children', []).append(menu_item)
393 # sort by sequence a tree using parent_id
394 for menu_item in menu_items:
395 menu_item.setdefault('children', []).sort(
396 key=lambda x:x["sequence"])
400 @openerpweb.jsonrequest
401 def action(self, req, menu_id):
402 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
403 [('ir.ui.menu', menu_id)], False,
404 req.session.eval_context(req.context))
405 return {"action": actions}
407 class DataSet(openerpweb.Controller):
408 _cp_path = "/base/dataset"
410 @openerpweb.jsonrequest
411 def fields(self, req, model):
412 return {'fields': req.session.model(model).fields_get(False,
413 req.session.eval_context(req.context))}
415 @openerpweb.jsonrequest
416 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
417 return self.do_search_read(request, model, fields, offset, limit, domain, context, sort)
418 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
419 """ Performs a search() followed by a read() (if needed) using the
420 provided search criteria
422 :param request: a JSON-RPC request object
423 :type request: openerpweb.JsonRequest
424 :param str model: the name of the model to search on
425 :param fields: a list of the fields to return in the result records
427 :param int offset: from which index should the results start being returned
428 :param int limit: the maximum number of records to return
429 :param list domain: the search domain for the query
430 :param list sort: sorting directives
431 :returns: a list of result records
434 Model = request.session.model(model)
435 context, domain = eval_context_and_domain(request.session, request.context, domain)
437 ids = Model.search(domain, offset or 0, limit or False,
438 sort or False, context)
440 if fields and fields == ['id']:
441 # shortcut read if we only want the ids
442 return map(lambda id: {'id': id}, ids)
444 reads = Model.read(ids, fields or False, context)
445 reads.sort(key=lambda obj: ids.index(obj['id']))
448 @openerpweb.jsonrequest
449 def get(self, request, model, ids, fields=False):
450 return self.do_get(request, model, ids, fields)
451 def do_get(self, request, model, ids, fields=False):
452 """ Fetches and returns the records of the model ``model`` whose ids
455 The results are in the same order as the inputs, but elements may be
456 missing (if there is no record left for the id)
458 :param request: the JSON-RPC2 request object
459 :type request: openerpweb.JsonRequest
460 :param model: the model to read from
462 :param ids: a list of identifiers
464 :param fields: a list of fields to fetch, ``False`` or empty to fetch
465 all fields in the model
466 :type fields: list | False
467 :returns: a list of records, in the same order as the list of ids
470 Model = request.session.model(model)
471 records = Model.read(ids, fields, request.session.eval_context(request.context))
473 record_map = dict((record['id'], record) for record in records)
475 return [record_map[id] for id in ids if record_map.get(id)]
477 @openerpweb.jsonrequest
478 def load(self, req, model, id, fields):
479 m = req.session.model(model)
481 r = m.read([id], False, req.session.eval_context(req.context))
484 return {'value': value}
486 @openerpweb.jsonrequest
487 def create(self, req, model, data):
488 m = req.session.model(model)
489 r = m.create(data, req.session.eval_context(req.context))
492 @openerpweb.jsonrequest
493 def save(self, req, model, id, data):
494 m = req.session.model(model)
495 r = m.write([id], data, req.session.eval_context(req.context))
498 @openerpweb.jsonrequest
499 def unlink(self, request, model, ids=()):
500 Model = request.session.model(model)
501 return Model.unlink(ids, request.session.eval_context(request.context))
503 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
504 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
505 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
506 c, d = eval_context_and_domain(req.session, context, domain)
507 if domain_id and len(args) - 1 >= domain_id:
509 if context_id and len(args) - 1 >= context_id:
512 return getattr(req.session.model(model), method)(*args)
514 @openerpweb.jsonrequest
515 def call(self, req, model, method, args, domain_id=None, context_id=None):
516 return {'result': self.call_common(req, model, method, args, domain_id, context_id)}
518 @openerpweb.jsonrequest
519 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
520 action = self.call_common(req, model, method, args, domain_id, context_id)
521 if isinstance(action, dict) and action.get('type') != '':
522 return {'result': clean_action(action, req.session)}
523 return {'result': False}
525 @openerpweb.jsonrequest
526 def exec_workflow(self, req, model, id, signal):
527 r = req.session.exec_workflow(model, id, signal)
530 @openerpweb.jsonrequest
531 def default_get(self, req, model, fields):
532 m = req.session.model(model)
533 r = m.default_get(fields, req.session.eval_context(req.context))
536 class DataGroup(openerpweb.Controller):
537 _cp_path = "/base/group"
538 @openerpweb.jsonrequest
539 def read(self, request, model, group_by_fields, domain=None):
540 Model = request.session.model(model)
541 context, domain = eval_context_and_domain(request.session, request.context, domain)
543 return Model.read_group(
544 domain or [], False, group_by_fields, 0, False,
545 dict(context, group_by=group_by_fields))
547 class View(openerpweb.Controller):
548 _cp_path = "/base/view"
550 def fields_view_get(self, request, model, view_id, view_type,
551 transform=True, toolbar=False, submenu=False):
552 Model = request.session.model(model)
553 context = request.session.eval_context(request.context)
554 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
555 # todo fme?: check that we should pass the evaluated context here
556 self.process_view(request.session, fvg, context, transform)
559 def process_view(self, session, fvg, context, transform):
560 # depending on how it feels, xmlrpclib.ServerProxy can translate
561 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
562 # enjoy unicode strings which can not be trivially converted to
563 # strings, and it blows up during parsing.
565 # So ensure we fix this retardation by converting view xml back to
567 if isinstance(fvg['arch'], unicode):
568 arch = fvg['arch'].encode('utf-8')
573 evaluation_context = session.evaluation_context(context or {})
574 xml = self.transform_view(arch, session, evaluation_context)
576 xml = ElementTree.fromstring(arch)
577 fvg['arch'] = Xml2Json.convert_element(xml)
578 for field in fvg['fields'].values():
579 if field.has_key('views') and field['views']:
580 for view in field["views"].values():
581 self.process_view(session, view, None, transform)
583 @openerpweb.jsonrequest
584 def add_custom(self, request, view_id, arch):
585 CustomView = request.session.model('ir.ui.view.custom')
587 'user_id': request.session._uid,
590 }, request.session.eval_context(request.context))
591 return {'result': True}
593 @openerpweb.jsonrequest
594 def undo_custom(self, request, view_id, reset=False):
595 CustomView = request.session.model('ir.ui.view.custom')
596 context = request.session.eval_context(request.context)
597 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
598 0, False, False, context)
601 CustomView.unlink(vcustom, context)
603 CustomView.unlink([vcustom[0]], context)
604 return {'result': True}
605 return {'result': False}
607 def normalize_attrs(self, elem, context):
608 """ Normalize @attrs, @invisible, @required, @readonly and @states, so
609 the client only has to deal with @attrs.
611 See `the discoveries pad <http://pad.openerp.com/discoveries>`_ for
614 :param elem: the current view node (Python object)
615 :type elem: xml.etree.ElementTree.Element
616 :param dict context: evaluation context
618 # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
619 attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
620 if 'states' in elem.attrib:
621 attrs.setdefault('invisible', [])\
622 .append(('state', 'not in', elem.attrib.pop('states').split(',')))
624 elem.set('attrs', simplejson.dumps(attrs))
625 for a in ['invisible', 'readonly', 'required']:
627 # In the XML we trust
628 avalue = bool(eval(elem.get(a, 'False'),
629 {'context': context or {}}))
634 if a == 'invisible' and 'attrs' in elem.attrib:
635 del elem.attrib['attrs']
637 def transform_view(self, view_string, session, context=None):
638 # transform nodes on the fly via iterparse, instead of
639 # doing it statically on the parsing result
640 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
642 for event, elem in parser:
646 self.normalize_attrs(elem, context)
647 self.parse_domains_and_contexts(elem, session)
650 def parse_domain(self, elem, attr_name, session):
651 """ Parses an attribute of the provided name as a domain, transforms it
652 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
654 :param elem: the node being parsed
655 :type param: xml.etree.ElementTree.Element
656 :param str attr_name: the name of the attribute which should be parsed
657 :param session: Current OpenERP session
658 :type session: openerpweb.openerpweb.OpenERPSession
660 domain = elem.get(attr_name, '').strip()
665 openerpweb.ast.literal_eval(
670 openerpweb.nonliterals.Domain(session, domain))
672 def parse_domains_and_contexts(self, elem, session):
673 """ Converts domains and contexts from the view into Python objects,
674 either literals if they can be parsed by literal_eval or a special
675 placeholder object if the domain or context refers to free variables.
677 :param elem: the current node being parsed
678 :type param: xml.etree.ElementTree.Element
679 :param session: OpenERP session object, used to store and retrieve
681 :type session: openerpweb.openerpweb.OpenERPSession
683 self.parse_domain(elem, 'domain', session)
684 self.parse_domain(elem, 'filter_domain', session)
685 for el in ['context', 'default_get']:
686 context_string = elem.get(el, '').strip()
690 openerpweb.ast.literal_eval(context_string))
693 openerpweb.nonliterals.Context(
694 session, context_string))
696 class FormView(View):
697 _cp_path = "/base/formview"
699 @openerpweb.jsonrequest
700 def load(self, req, model, view_id, toolbar=False):
701 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
702 return {'fields_view': fields_view}
704 class ListView(View):
705 _cp_path = "/base/listview"
707 @openerpweb.jsonrequest
708 def load(self, req, model, view_id, toolbar=False):
709 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
710 return {'fields_view': fields_view}
712 def process_colors(self, view, row, context):
713 colors = view['arch']['attrs'].get('colors')
720 for pair in colors.split(';')
721 if eval(pair.split(':')[1], dict(context, **row))
726 elif len(color) == 1:
730 class SearchView(View):
731 _cp_path = "/base/searchview"
733 @openerpweb.jsonrequest
734 def load(self, req, model, view_id):
735 fields_view = self.fields_view_get(req, model, view_id, 'search')
736 return {'fields_view': fields_view}
738 @openerpweb.jsonrequest
739 def fields_get(self, req, model):
740 Model = req.session.model(model)
741 fields = Model.fields_get(False, req.session.eval_context(req.context))
742 return {'fields': fields}
744 class Binary(openerpweb.Controller):
745 _cp_path = "/base/binary"
747 @openerpweb.httprequest
748 def image(self, request, session_id, model, id, field, **kw):
749 cherrypy.response.headers['Content-Type'] = 'image/png'
750 Model = request.session.model(model)
751 context = request.session.eval_context(request.context)
754 res = Model.default_get([field], context).get(field, '')
756 res = Model.read([int(id)], [field], context)[0].get(field, '')
757 return base64.decodestring(res)
758 except: # TODO: what's the exception here?
759 return self.placeholder()
760 def placeholder(self):
761 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
763 @openerpweb.httprequest
764 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
765 Model = request.session.model(model)
766 context = request.session.eval_context(request.context)
767 res = Model.read([int(id)], [field, fieldname], context)[0]
768 filecontent = res.get(field, '')
770 raise cherrypy.NotFound
772 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
773 filename = '%s_%s' % (model.replace('.', '_'), id)
775 filename = res.get(fieldname, '') or filename
776 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
777 return base64.decodestring(filecontent)
779 @openerpweb.httprequest
780 def upload(self, request, session_id, callback, ufile=None):
781 cherrypy.response.timeout = 500
783 for key, val in cherrypy.request.headers.iteritems():
784 headers[key.lower()] = val
785 size = int(headers.get('content-length', 0))
786 # TODO: might be useful to have a configuration flag for max-length file uploads
788 out = """<script language="javascript" type="text/javascript">
789 var win = window.top.window,
791 if (typeof(callback) === 'function') {
792 callback.apply(this, %s);
794 win.jQuery('#oe_notification', win.document).notify('create', {
795 title: "Ajax File Upload",
796 text: "Could not find callback"
800 data = ufile.file.read()
801 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
803 args = [False, e.message]
804 return out % (simplejson.dumps(callback), simplejson.dumps(args))
806 @openerpweb.httprequest
807 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
808 cherrypy.response.timeout = 500
809 context = request.session.eval_context(request.context)
810 Model = request.session.model('ir.attachment')
812 out = """<script language="javascript" type="text/javascript">
813 var win = window.top.window,
815 if (typeof(callback) === 'function') {
816 callback.call(this, %s);
819 attachment_id = Model.create({
820 'name': ufile.filename,
821 'datas': base64.encodestring(ufile.file.read()),
826 'filename': ufile.filename,
830 args = { 'error': e.message }
831 return out % (simplejson.dumps(callback), simplejson.dumps(args))
833 class Action(openerpweb.Controller):
834 _cp_path = "/base/action"
836 @openerpweb.jsonrequest
837 def load(self, req, action_id):
838 Actions = req.session.model('ir.actions.actions')
840 context = req.session.eval_context(req.context)
841 action_type = Actions.read([action_id], ['type'], context)
843 action = req.session.model(action_type[0]['type']).read([action_id], False,
846 value = clean_action(action[0], req.session)
847 return {'result': value}
849 @openerpweb.jsonrequest
850 def run(self, req, action_id):
851 return clean_action(req.session.model('ir.actions.server').run(
852 [action_id], req.session.eval_context(req.context)), req.session)