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 db_operation(self, req, flag, **kw):
128 password = kw.get('password')
130 res = req.session.proxy("db").drop(password, db)
136 @openerpweb.jsonrequest
137 def modules(self, req):
138 return {"modules": [name
139 for name, manifest in openerpweb.addons_manifest.iteritems()
140 if manifest.get('active', True)]}
142 @openerpweb.jsonrequest
143 def csslist(self, req, mods='base'):
144 return {'files': self.manifest_glob(mods.split(','), 'css')}
146 @openerpweb.jsonrequest
147 def jslist(self, req, mods='base'):
148 return {'files': self.manifest_glob(mods.split(','), 'js')}
150 def css(self, req, mods='base,base_hello'):
151 files = self.manifest_glob(mods.split(','), 'css')
152 concat = self.concat_files(files)[0]
153 # TODO request set the Date of last modif and Etag
157 def js(self, req, mods='base,base_hello'):
158 files = self.manifest_glob(mods.split(','), 'js')
159 concat = self.concat_files(files)[0]
160 # TODO request set the Date of last modif and Etag
164 @openerpweb.jsonrequest
165 def eval_domain_and_context(self, req, contexts, domains,
167 """ Evaluates sequences of domains and contexts, composing them into
168 a single context, domain or group_by sequence.
170 :param list contexts: list of contexts to merge together. Contexts are
171 evaluated in sequence, all previous contexts
172 are part of their own evaluation context
173 (starting at the session context).
174 :param list domains: list of domains to merge together. Domains are
175 evaluated in sequence and appended to one another
176 (implicit AND), their evaluation domain is the
177 result of merging all contexts.
178 :param list group_by_seq: list of domains (which may be in a different
179 order than the ``contexts`` parameter),
180 evaluated in sequence, their ``'group_by'``
181 key is extracted if they have one.
186 the global context created by merging all of
190 the concatenation of all domains
193 a list of fields to group by, potentially empty (in which case
194 no group by should be performed)
196 context, domain = eval_context_and_domain(req.session,
197 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
198 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
200 group_by_sequence = []
201 for candidate in (group_by_seq or []):
202 ctx = req.session.eval_context(candidate, context)
203 group_by = ctx.get('group_by')
206 elif isinstance(group_by, basestring):
207 group_by_sequence.append(group_by)
209 group_by_sequence.extend(group_by)
214 'group_by': group_by_sequence
217 @openerpweb.jsonrequest
218 def save_session_action(self, req, the_action):
220 This method store an action object in the session object and returns an integer
221 identifying that action. The method get_session_action() can be used to get
224 :param the_action: The action to save in the session.
225 :type the_action: anything
226 :return: A key identifying the saved action.
229 saved_actions = cherrypy.session.get('saved_actions')
230 if not saved_actions:
231 saved_actions = {"next":0, "actions":{}}
232 cherrypy.session['saved_actions'] = saved_actions
233 # we don't allow more than 10 stored actions
234 if len(saved_actions["actions"]) >= 10:
235 del saved_actions["actions"][min(saved_actions["actions"].keys())]
236 key = saved_actions["next"]
237 saved_actions["actions"][key] = the_action
238 saved_actions["next"] = key + 1
241 @openerpweb.jsonrequest
242 def get_session_action(self, req, key):
244 Gets back a previously saved action. This method can return None if the action
245 was saved since too much time (this case should be handled in a smart way).
247 :param key: The key given by save_session_action()
249 :return: The saved action or None.
252 saved_actions = cherrypy.session.get('saved_actions')
253 if not saved_actions:
255 return saved_actions["actions"].get(key)
257 def eval_context_and_domain(session, context, domain=None):
258 e_context = session.eval_context(context)
259 # should we give the evaluated context as an evaluation context to the domain?
260 e_domain = session.eval_domain(domain or [])
262 return e_context, e_domain
264 def load_actions_from_ir_values(req, key, key2, models, meta, context):
265 Values = req.session.model('ir.values')
266 actions = Values.get(key, key2, models, meta, context)
268 return [(id, name, clean_action(action, req.session))
269 for id, name, action in actions]
271 def clean_action(action, session):
272 if action['type'] != 'ir.actions.act_window':
274 # values come from the server, we can just eval them
275 if isinstance(action.get('context', None), basestring):
276 action['context'] = eval(
278 session.evaluation_context()) or {}
280 if isinstance(action.get('domain', None), basestring):
281 action['domain'] = eval(
283 session.evaluation_context(
284 action['context'])) or []
285 if 'flags' not in action:
286 # Set empty flags dictionary for web client.
287 action['flags'] = dict()
288 return fix_view_modes(action)
290 def generate_views(action):
292 While the server generates a sequence called "views" computing dependencies
293 between a bunch of stuff for views coming directly from the database
294 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
295 to return custom view dictionaries generated on the fly.
297 In that case, there is no ``views`` key available on the action.
299 Since the web client relies on ``action['views']``, generate it here from
300 ``view_mode`` and ``view_id``.
302 Currently handles two different cases:
304 * no view_id, multiple view_mode
305 * single view_id, single view_mode
307 :param dict action: action descriptor dictionary to generate a views key for
309 view_id = action.get('view_id', False)
310 if isinstance(view_id, (list, tuple)):
313 # providing at least one view mode is a requirement, not an option
314 view_modes = action['view_mode'].split(',')
316 if len(view_modes) > 1:
318 raise ValueError('Non-db action dictionaries should provide '
319 'either multiple view modes or a single view '
320 'mode and an optional view id.\n\n Got view '
321 'modes %r and view id %r for action %r' % (
322 view_modes, view_id, action))
323 action['views'] = [(False, mode) for mode in view_modes]
325 action['views'] = [(view_id, view_modes[0])]
328 def fix_view_modes(action):
329 """ For historical reasons, OpenERP has weird dealings in relation to
330 view_mode and the view_type attribute (on window actions):
332 * one of the view modes is ``tree``, which stands for both list views
334 * the choice is made by checking ``view_type``, which is either
335 ``form`` for a list view or ``tree`` for an actual tree view
337 This methods simply folds the view_type into view_mode by adding a
338 new view mode ``list`` which is the result of the ``tree`` view_mode
339 in conjunction with the ``form`` view_type.
341 TODO: this should go into the doc, some kind of "peculiarities" section
343 :param dict action: an action descriptor
344 :returns: nothing, the action is modified in place
346 if 'views' not in action:
347 generate_views(action)
349 if action.pop('view_type') != 'form':
353 [id, mode if mode != 'tree' else 'list']
354 for id, mode in action['views']
359 class Menu(openerpweb.Controller):
360 _cp_path = "/base/menu"
362 @openerpweb.jsonrequest
364 return {'data': self.do_load(req)}
366 def do_load(self, req):
367 """ Loads all menu items (all applications and their sub-menus).
369 :param req: A request object, with an OpenERP session attribute
370 :type req: < session -> OpenERPSession >
371 :return: the menu root
372 :rtype: dict('children': menu_nodes)
374 Menus = req.session.model('ir.ui.menu')
375 # menus are loaded fully unlike a regular tree view, cause there are
376 # less than 512 items
377 context = req.session.eval_context(req.context)
378 menu_ids = Menus.search([], 0, False, False, context)
379 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
380 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
381 menu_items.append(menu_root)
383 # make a tree using parent_id
384 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
385 for menu_item in menu_items:
386 if menu_item['parent_id']:
387 parent = menu_item['parent_id'][0]
390 if parent in menu_items_map:
391 menu_items_map[parent].setdefault(
392 'children', []).append(menu_item)
394 # sort by sequence a tree using parent_id
395 for menu_item in menu_items:
396 menu_item.setdefault('children', []).sort(
397 key=lambda x:x["sequence"])
401 @openerpweb.jsonrequest
402 def action(self, req, menu_id):
403 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
404 [('ir.ui.menu', menu_id)], False,
405 req.session.eval_context(req.context))
406 return {"action": actions}
408 class DataSet(openerpweb.Controller):
409 _cp_path = "/base/dataset"
411 @openerpweb.jsonrequest
412 def fields(self, req, model):
413 return {'fields': req.session.model(model).fields_get(False,
414 req.session.eval_context(req.context))}
416 @openerpweb.jsonrequest
417 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
418 return self.do_search_read(request, model, fields, offset, limit, domain, context, sort)
419 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
420 """ Performs a search() followed by a read() (if needed) using the
421 provided search criteria
423 :param request: a JSON-RPC request object
424 :type request: openerpweb.JsonRequest
425 :param str model: the name of the model to search on
426 :param fields: a list of the fields to return in the result records
428 :param int offset: from which index should the results start being returned
429 :param int limit: the maximum number of records to return
430 :param list domain: the search domain for the query
431 :param list sort: sorting directives
432 :returns: a list of result records
435 Model = request.session.model(model)
436 context, domain = eval_context_and_domain(request.session, request.context, domain)
438 ids = Model.search(domain, offset or 0, limit or False,
439 sort or False, context)
441 if fields and fields == ['id']:
442 # shortcut read if we only want the ids
443 return map(lambda id: {'id': id}, ids)
445 reads = Model.read(ids, fields or False, context)
446 reads.sort(key=lambda obj: ids.index(obj['id']))
449 @openerpweb.jsonrequest
450 def get(self, request, model, ids, fields=False):
451 return self.do_get(request, model, ids, fields)
452 def do_get(self, request, model, ids, fields=False):
453 """ Fetches and returns the records of the model ``model`` whose ids
456 The results are in the same order as the inputs, but elements may be
457 missing (if there is no record left for the id)
459 :param request: the JSON-RPC2 request object
460 :type request: openerpweb.JsonRequest
461 :param model: the model to read from
463 :param ids: a list of identifiers
465 :param fields: a list of fields to fetch, ``False`` or empty to fetch
466 all fields in the model
467 :type fields: list | False
468 :returns: a list of records, in the same order as the list of ids
471 Model = request.session.model(model)
472 records = Model.read(ids, fields, request.session.eval_context(request.context))
474 record_map = dict((record['id'], record) for record in records)
476 return [record_map[id] for id in ids if record_map.get(id)]
478 @openerpweb.jsonrequest
479 def load(self, req, model, id, fields):
480 m = req.session.model(model)
482 r = m.read([id], False, req.session.eval_context(req.context))
485 return {'value': value}
487 @openerpweb.jsonrequest
488 def create(self, req, model, data):
489 m = req.session.model(model)
490 r = m.create(data, req.session.eval_context(req.context))
493 @openerpweb.jsonrequest
494 def save(self, req, model, id, data):
495 m = req.session.model(model)
496 r = m.write([id], data, req.session.eval_context(req.context))
499 @openerpweb.jsonrequest
500 def unlink(self, request, model, ids=()):
501 Model = request.session.model(model)
502 return Model.unlink(ids, request.session.eval_context(request.context))
504 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
505 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
506 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
507 c, d = eval_context_and_domain(req.session, context, domain)
508 if domain_id and len(args) - 1 >= domain_id:
510 if context_id and len(args) - 1 >= context_id:
513 return getattr(req.session.model(model), method)(*args)
515 @openerpweb.jsonrequest
516 def call(self, req, model, method, args, domain_id=None, context_id=None):
517 return {'result': self.call_common(req, model, method, args, domain_id, context_id)}
519 @openerpweb.jsonrequest
520 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
521 action = self.call_common(req, model, method, args, domain_id, context_id)
522 if isinstance(action, dict) and action.get('type') != '':
523 return {'result': clean_action(action, req.session)}
524 return {'result': False}
526 @openerpweb.jsonrequest
527 def exec_workflow(self, req, model, id, signal):
528 r = req.session.exec_workflow(model, id, signal)
531 @openerpweb.jsonrequest
532 def default_get(self, req, model, fields):
533 m = req.session.model(model)
534 r = m.default_get(fields, req.session.eval_context(req.context))
537 class DataGroup(openerpweb.Controller):
538 _cp_path = "/base/group"
539 @openerpweb.jsonrequest
540 def read(self, request, model, group_by_fields, domain=None):
541 Model = request.session.model(model)
542 context, domain = eval_context_and_domain(request.session, request.context, domain)
544 return Model.read_group(
545 domain or [], False, group_by_fields, 0, False,
546 dict(context, group_by=group_by_fields))
548 class View(openerpweb.Controller):
549 _cp_path = "/base/view"
551 def fields_view_get(self, request, model, view_id, view_type,
552 transform=True, toolbar=False, submenu=False):
553 Model = request.session.model(model)
554 context = request.session.eval_context(request.context)
555 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
556 # todo fme?: check that we should pass the evaluated context here
557 self.process_view(request.session, fvg, context, transform)
560 def process_view(self, session, fvg, context, transform):
561 # depending on how it feels, xmlrpclib.ServerProxy can translate
562 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
563 # enjoy unicode strings which can not be trivially converted to
564 # strings, and it blows up during parsing.
566 # So ensure we fix this retardation by converting view xml back to
568 if isinstance(fvg['arch'], unicode):
569 arch = fvg['arch'].encode('utf-8')
574 evaluation_context = session.evaluation_context(context or {})
575 xml = self.transform_view(arch, session, evaluation_context)
577 xml = ElementTree.fromstring(arch)
578 fvg['arch'] = Xml2Json.convert_element(xml)
579 for field in fvg['fields'].values():
580 if field.has_key('views') and field['views']:
581 for view in field["views"].values():
582 self.process_view(session, view, None, transform)
584 @openerpweb.jsonrequest
585 def add_custom(self, request, view_id, arch):
586 CustomView = request.session.model('ir.ui.view.custom')
588 'user_id': request.session._uid,
591 }, request.session.eval_context(request.context))
592 return {'result': True}
594 @openerpweb.jsonrequest
595 def undo_custom(self, request, view_id, reset=False):
596 CustomView = request.session.model('ir.ui.view.custom')
597 context = request.session.eval_context(request.context)
598 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
599 0, False, False, context)
602 CustomView.unlink(vcustom, context)
604 CustomView.unlink([vcustom[0]], context)
605 return {'result': True}
606 return {'result': False}
608 def normalize_attrs(self, elem, context):
609 """ Normalize @attrs, @invisible, @required, @readonly and @states, so
610 the client only has to deal with @attrs.
612 See `the discoveries pad <http://pad.openerp.com/discoveries>`_ for
615 :param elem: the current view node (Python object)
616 :type elem: xml.etree.ElementTree.Element
617 :param dict context: evaluation context
619 # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
620 attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
621 if 'states' in elem.attrib:
622 attrs.setdefault('invisible', [])\
623 .append(('state', 'not in', elem.attrib.pop('states').split(',')))
625 elem.set('attrs', simplejson.dumps(attrs))
626 for a in ['invisible', 'readonly', 'required']:
628 # In the XML we trust
629 avalue = bool(eval(elem.get(a, 'False'),
630 {'context': context or {}}))
635 if a == 'invisible' and 'attrs' in elem.attrib:
636 del elem.attrib['attrs']
638 def transform_view(self, view_string, session, context=None):
639 # transform nodes on the fly via iterparse, instead of
640 # doing it statically on the parsing result
641 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
643 for event, elem in parser:
647 self.normalize_attrs(elem, context)
648 self.parse_domains_and_contexts(elem, session)
651 def parse_domain(self, elem, attr_name, session):
652 """ Parses an attribute of the provided name as a domain, transforms it
653 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
655 :param elem: the node being parsed
656 :type param: xml.etree.ElementTree.Element
657 :param str attr_name: the name of the attribute which should be parsed
658 :param session: Current OpenERP session
659 :type session: openerpweb.openerpweb.OpenERPSession
661 domain = elem.get(attr_name, '').strip()
666 openerpweb.ast.literal_eval(
671 openerpweb.nonliterals.Domain(session, domain))
673 def parse_domains_and_contexts(self, elem, session):
674 """ Converts domains and contexts from the view into Python objects,
675 either literals if they can be parsed by literal_eval or a special
676 placeholder object if the domain or context refers to free variables.
678 :param elem: the current node being parsed
679 :type param: xml.etree.ElementTree.Element
680 :param session: OpenERP session object, used to store and retrieve
682 :type session: openerpweb.openerpweb.OpenERPSession
684 self.parse_domain(elem, 'domain', session)
685 self.parse_domain(elem, 'filter_domain', session)
686 for el in ['context', 'default_get']:
687 context_string = elem.get(el, '').strip()
691 openerpweb.ast.literal_eval(context_string))
694 openerpweb.nonliterals.Context(
695 session, context_string))
697 class FormView(View):
698 _cp_path = "/base/formview"
700 @openerpweb.jsonrequest
701 def load(self, req, model, view_id, toolbar=False):
702 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
703 return {'fields_view': fields_view}
705 class ListView(View):
706 _cp_path = "/base/listview"
708 @openerpweb.jsonrequest
709 def load(self, req, model, view_id, toolbar=False):
710 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
711 return {'fields_view': fields_view}
713 def process_colors(self, view, row, context):
714 colors = view['arch']['attrs'].get('colors')
721 for pair in colors.split(';')
722 if eval(pair.split(':')[1], dict(context, **row))
727 elif len(color) == 1:
731 class SearchView(View):
732 _cp_path = "/base/searchview"
734 @openerpweb.jsonrequest
735 def load(self, req, model, view_id):
736 fields_view = self.fields_view_get(req, model, view_id, 'search')
737 return {'fields_view': fields_view}
739 @openerpweb.jsonrequest
740 def fields_get(self, req, model):
741 Model = req.session.model(model)
742 fields = Model.fields_get(False, req.session.eval_context(req.context))
743 return {'fields': fields}
745 class Binary(openerpweb.Controller):
746 _cp_path = "/base/binary"
748 @openerpweb.httprequest
749 def image(self, request, session_id, model, id, field, **kw):
750 cherrypy.response.headers['Content-Type'] = 'image/png'
751 Model = request.session.model(model)
752 context = request.session.eval_context(request.context)
755 res = Model.default_get([field], context).get(field, '')
757 res = Model.read([int(id)], [field], context)[0].get(field, '')
758 return base64.decodestring(res)
759 except: # TODO: what's the exception here?
760 return self.placeholder()
761 def placeholder(self):
762 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
764 @openerpweb.httprequest
765 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
766 Model = request.session.model(model)
767 context = request.session.eval_context(request.context)
768 res = Model.read([int(id)], [field, fieldname], context)[0]
769 filecontent = res.get(field, '')
771 raise cherrypy.NotFound
773 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
774 filename = '%s_%s' % (model.replace('.', '_'), id)
776 filename = res.get(fieldname, '') or filename
777 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
778 return base64.decodestring(filecontent)
780 @openerpweb.httprequest
781 def upload(self, request, session_id, callback, ufile=None):
782 cherrypy.response.timeout = 500
784 for key, val in cherrypy.request.headers.iteritems():
785 headers[key.lower()] = val
786 size = int(headers.get('content-length', 0))
787 # TODO: might be useful to have a configuration flag for max-length file uploads
789 out = """<script language="javascript" type="text/javascript">
790 var win = window.top.window,
792 if (typeof(callback) === 'function') {
793 callback.apply(this, %s);
795 win.jQuery('#oe_notification', win.document).notify('create', {
796 title: "Ajax File Upload",
797 text: "Could not find callback"
801 data = ufile.file.read()
802 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
804 args = [False, e.message]
805 return out % (simplejson.dumps(callback), simplejson.dumps(args))
807 @openerpweb.httprequest
808 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
809 cherrypy.response.timeout = 500
810 context = request.session.eval_context(request.context)
811 Model = request.session.model('ir.attachment')
813 out = """<script language="javascript" type="text/javascript">
814 var win = window.top.window,
816 if (typeof(callback) === 'function') {
817 callback.call(this, %s);
820 attachment_id = Model.create({
821 'name': ufile.filename,
822 'datas': base64.encodestring(ufile.file.read()),
827 'filename': ufile.filename,
831 args = { 'error': e.message }
832 return out % (simplejson.dumps(callback), simplejson.dumps(args))
834 class Action(openerpweb.Controller):
835 _cp_path = "/base/action"
837 @openerpweb.jsonrequest
838 def load(self, req, action_id):
839 Actions = req.session.model('ir.actions.actions')
841 context = req.session.eval_context(req.context)
842 action_type = Actions.read([action_id], ['type'], context)
844 action = req.session.model(action_type[0]['type']).read([action_id], False,
847 value = clean_action(action[0], req.session)
848 return {'result': value}
850 @openerpweb.jsonrequest
851 def run(self, req, action_id):
852 return clean_action(req.session.model('ir.actions.server').run(
853 [action_id], req.session.eval_context(req.context)), req.session)