1 # -*- coding: utf-8 -*-
4 from xml.etree import ElementTree
5 from cStringIO import StringIO
11 import openerpweb.nonliterals
15 # Should move to openerpweb.Xml2Json
18 # Simple and straightforward XML-to-JSON converter in Python
21 # URL: http://code.google.com/p/xml2json-direct/
23 def convert_to_json(s):
24 return simplejson.dumps(
25 Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
28 def convert_to_structure(s):
29 root = ElementTree.fromstring(s)
30 return Xml2Json.convert_element(root)
33 def convert_element(el, skip_whitespaces=True):
36 ns, name = el.tag.rsplit("}", 1)
38 res["namespace"] = ns[1:]
42 for k, v in el.items():
45 if el.text and (not skip_whitespaces or el.text.strip() != ''):
48 kids.append(Xml2Json.convert_element(kid))
49 if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
51 res["children"] = kids
54 #----------------------------------------------------------
55 # OpenERP Web base Controllers
56 #----------------------------------------------------------
58 class Session(openerpweb.Controller):
59 _cp_path = "/base/session"
61 def manifest_glob(self, addons, key):
64 globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
67 resource_path[len(openerpweb.path_addons):]
68 for pattern in globlist
69 for resource_path in glob.glob(os.path.join(
70 openerpweb.path_addons, addon, pattern))
74 def concat_files(self, file_list):
75 """ Concatenate file content
76 return (concat,timestamp)
77 concat: concatenation of file content
78 timestamp: max(os.path.getmtime of file_list)
80 root = openerpweb.path_root
84 fname = os.path.join(root, i)
85 ftime = os.path.getmtime(fname)
86 if ftime > files_timestamp:
87 files_timestamp = ftime
88 files_content = open(fname).read()
89 files_concat = "".join(files_content)
92 @openerpweb.jsonrequest
93 def login(self, req, db, login, password):
94 req.session.login(db, login, password)
97 "session_id": req.session_id,
98 "uid": req.session._uid,
101 @openerpweb.jsonrequest
102 def sc_list(self, req):
103 return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
104 req.session.eval_context(req.context))
106 @openerpweb.jsonrequest
107 def get_databases_list(self, req):
108 proxy = req.session.proxy("db")
111 return {"db_list": dbs}
113 @openerpweb.jsonrequest
114 def modules(self, req):
115 return {"modules": [name
116 for name, manifest in openerpweb.addons_manifest.iteritems()
117 if manifest.get('active', True)]}
119 @openerpweb.jsonrequest
120 def csslist(self, req, mods='base'):
121 return {'files': self.manifest_glob(mods.split(','), 'css')}
123 @openerpweb.jsonrequest
124 def jslist(self, req, mods='base'):
125 return {'files': self.manifest_glob(mods.split(','), 'js')}
127 def css(self, req, mods='base,base_hello'):
128 files = self.manifest_glob(mods.split(','), 'css')
129 concat = self.concat_files(files)[0]
130 # TODO request set the Date of last modif and Etag
134 def js(self, req, mods='base,base_hello'):
135 files = self.manifest_glob(mods.split(','), 'js')
136 concat = self.concat_files(files)[0]
137 # TODO request set the Date of last modif and Etag
141 @openerpweb.jsonrequest
142 def eval_domain_and_context(self, req, contexts, domains,
144 """ Evaluates sequences of domains and contexts, composing them into
145 a single context, domain or group_by sequence.
147 :param list contexts: list of contexts to merge together. Contexts are
148 evaluated in sequence, all previous contexts
149 are part of their own evaluation context
150 (starting at the session context).
151 :param list domains: list of domains to merge together. Domains are
152 evaluated in sequence and appended to one another
153 (implicit AND), their evaluation domain is the
154 result of merging all contexts.
155 :param list group_by_seq: list of domains (which may be in a different
156 order than the ``contexts`` parameter),
157 evaluated in sequence, their ``'group_by'``
158 key is extracted if they have one.
163 the global context created by merging all of
167 the concatenation of all domains
170 a list of fields to group by, potentially empty (in which case
171 no group by should be performed)
173 context, domain = eval_context_and_domain(req.session,
174 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
175 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
177 group_by_sequence = []
178 for candidate in (group_by_seq or []):
179 ctx = req.session.eval_context(candidate, context)
180 group_by = ctx.get('group_by')
183 elif isinstance(group_by, basestring):
184 group_by_sequence.append(group_by)
186 group_by_sequence.extend(group_by)
191 'group_by': group_by_sequence
194 @openerpweb.jsonrequest
195 def save_session_action(self, req, the_action):
197 This method store an action object in the session object and returns an integer
198 identifying that action. The method get_session_action() can be used to get
201 :param the_action: The action to save in the session.
202 :type the_action: anything
203 :return: A key identifying the saved action.
206 saved_actions = cherrypy.session.get('saved_actions')
207 if not saved_actions:
208 saved_actions = {"next":0, "actions":{}}
209 cherrypy.session['saved_actions'] = saved_actions
210 # we don't allow more than 10 stored actions
211 if len(saved_actions["actions"]) >= 10:
212 del saved_actions["actions"][min(saved_actions["actions"].keys())]
213 key = saved_actions["next"]
214 saved_actions["actions"][key] = the_action
215 saved_actions["next"] = key + 1
218 @openerpweb.jsonrequest
219 def get_session_action(self, req, key):
221 Gets back a previously saved action. This method can return None if the action
222 was saved since too much time (this case should be handled in a smart way).
224 :param key: The key given by save_session_action()
226 :return: The saved action or None.
229 saved_actions = cherrypy.session.get('saved_actions')
230 if not saved_actions:
232 return saved_actions["actions"].get(key)
234 @openerpweb.jsonrequest
235 def check(self, req):
236 req.session.assert_valid()
239 def eval_context_and_domain(session, context, domain=None):
240 e_context = session.eval_context(context)
241 # should we give the evaluated context as an evaluation context to the domain?
242 e_domain = session.eval_domain(domain or [])
244 return e_context, e_domain
246 def load_actions_from_ir_values(req, key, key2, models, meta, context):
247 Values = req.session.model('ir.values')
248 actions = Values.get(key, key2, models, meta, context)
250 return [(id, name, clean_action(action, req.session))
251 for id, name, action in actions]
253 def clean_action(action, session):
254 if action['type'] != 'ir.actions.act_window':
256 # values come from the server, we can just eval them
257 if isinstance(action.get('context', None), basestring):
258 action['context'] = eval(
260 session.evaluation_context()) or {}
262 if isinstance(action.get('domain', None), basestring):
263 action['domain'] = eval(
265 session.evaluation_context(
266 action['context'])) or []
267 if 'flags' not in action:
268 # Set empty flags dictionary for web client.
269 action['flags'] = dict()
270 return fix_view_modes(action)
272 def generate_views(action):
274 While the server generates a sequence called "views" computing dependencies
275 between a bunch of stuff for views coming directly from the database
276 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
277 to return custom view dictionaries generated on the fly.
279 In that case, there is no ``views`` key available on the action.
281 Since the web client relies on ``action['views']``, generate it here from
282 ``view_mode`` and ``view_id``.
284 Currently handles two different cases:
286 * no view_id, multiple view_mode
287 * single view_id, single view_mode
289 :param dict action: action descriptor dictionary to generate a views key for
291 view_id = action.get('view_id', False)
292 if isinstance(view_id, (list, tuple)):
295 # providing at least one view mode is a requirement, not an option
296 view_modes = action['view_mode'].split(',')
298 if len(view_modes) > 1:
300 raise ValueError('Non-db action dictionaries should provide '
301 'either multiple view modes or a single view '
302 'mode and an optional view id.\n\n Got view '
303 'modes %r and view id %r for action %r' % (
304 view_modes, view_id, action))
305 action['views'] = [(False, mode) for mode in view_modes]
307 action['views'] = [(view_id, view_modes[0])]
310 def fix_view_modes(action):
311 """ For historical reasons, OpenERP has weird dealings in relation to
312 view_mode and the view_type attribute (on window actions):
314 * one of the view modes is ``tree``, which stands for both list views
316 * the choice is made by checking ``view_type``, which is either
317 ``form`` for a list view or ``tree`` for an actual tree view
319 This methods simply folds the view_type into view_mode by adding a
320 new view mode ``list`` which is the result of the ``tree`` view_mode
321 in conjunction with the ``form`` view_type.
323 TODO: this should go into the doc, some kind of "peculiarities" section
325 :param dict action: an action descriptor
326 :returns: nothing, the action is modified in place
328 if 'views' not in action:
329 generate_views(action)
331 if action.pop('view_type') != 'form':
335 [id, mode if mode != 'tree' else 'list']
336 for id, mode in action['views']
341 class Menu(openerpweb.Controller):
342 _cp_path = "/base/menu"
344 @openerpweb.jsonrequest
346 return {'data': self.do_load(req)}
348 def do_load(self, req):
349 """ Loads all menu items (all applications and their sub-menus).
351 :param req: A request object, with an OpenERP session attribute
352 :type req: < session -> OpenERPSession >
353 :return: the menu root
354 :rtype: dict('children': menu_nodes)
356 Menus = req.session.model('ir.ui.menu')
357 # menus are loaded fully unlike a regular tree view, cause there are
358 # less than 512 items
359 context = req.session.eval_context(req.context)
360 menu_ids = Menus.search([], 0, False, False, context)
361 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
362 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
363 menu_items.append(menu_root)
365 # make a tree using parent_id
366 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
367 for menu_item in menu_items:
368 if menu_item['parent_id']:
369 parent = menu_item['parent_id'][0]
372 if parent in menu_items_map:
373 menu_items_map[parent].setdefault(
374 'children', []).append(menu_item)
376 # sort by sequence a tree using parent_id
377 for menu_item in menu_items:
378 menu_item.setdefault('children', []).sort(
379 key=lambda x:x["sequence"])
383 @openerpweb.jsonrequest
384 def action(self, req, menu_id):
385 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
386 [('ir.ui.menu', menu_id)], False,
387 req.session.eval_context(req.context))
388 return {"action": actions}
390 class DataSet(openerpweb.Controller):
391 _cp_path = "/base/dataset"
393 @openerpweb.jsonrequest
394 def fields(self, req, model):
395 return {'fields': req.session.model(model).fields_get(False,
396 req.session.eval_context(req.context))}
398 @openerpweb.jsonrequest
399 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None):
400 return self.do_search_read(request, model, fields, offset, limit, domain, sort)
401 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None
403 """ Performs a search() followed by a read() (if needed) using the
404 provided search criteria
406 :param request: a JSON-RPC request object
407 :type request: openerpweb.JsonRequest
408 :param str model: the name of the model to search on
409 :param fields: a list of the fields to return in the result records
411 :param int offset: from which index should the results start being returned
412 :param int limit: the maximum number of records to return
413 :param list domain: the search domain for the query
414 :param list sort: sorting directives
415 :returns: A structure (dict) with two keys: ids (all the ids matching
416 the (domain, context) pair) and records (paginated records
417 matching fields selection set)
420 Model = request.session.model(model)
421 context, domain = eval_context_and_domain(
422 request.session, request.context, domain)
424 ids = Model.search(domain, 0, False, sort or False, context)
425 # need to fill the dataset with all ids for the (domain, context) pair,
426 # so search un-paginated and paginate manually before reading
427 paginated_ids = ids[offset:(offset + limit if limit else None)]
428 if fields and fields == ['id']:
429 # shortcut read if we only want the ids
432 'records': map(lambda id: {'id': id}, paginated_ids)
435 records = Model.read(paginated_ids, fields or False, context)
436 records.sort(key=lambda obj: ids.index(obj['id']))
443 @openerpweb.jsonrequest
444 def get(self, request, model, ids, fields=False):
445 return self.do_get(request, model, ids, fields)
446 def do_get(self, request, model, ids, fields=False):
447 """ Fetches and returns the records of the model ``model`` whose ids
450 The results are in the same order as the inputs, but elements may be
451 missing (if there is no record left for the id)
453 :param request: the JSON-RPC2 request object
454 :type request: openerpweb.JsonRequest
455 :param model: the model to read from
457 :param ids: a list of identifiers
459 :param fields: a list of fields to fetch, ``False`` or empty to fetch
460 all fields in the model
461 :type fields: list | False
462 :returns: a list of records, in the same order as the list of ids
465 Model = request.session.model(model)
466 records = Model.read(ids, fields, request.session.eval_context(request.context))
468 record_map = dict((record['id'], record) for record in records)
470 return [record_map[id] for id in ids if record_map.get(id)]
472 @openerpweb.jsonrequest
473 def load(self, req, model, id, fields):
474 m = req.session.model(model)
476 r = m.read([id], False, req.session.eval_context(req.context))
479 return {'value': value}
481 @openerpweb.jsonrequest
482 def create(self, req, model, data):
483 m = req.session.model(model)
484 r = m.create(data, req.session.eval_context(req.context))
487 @openerpweb.jsonrequest
488 def save(self, req, model, id, data):
489 m = req.session.model(model)
490 r = m.write([id], data, req.session.eval_context(req.context))
493 @openerpweb.jsonrequest
494 def unlink(self, request, model, ids=()):
495 Model = request.session.model(model)
496 return Model.unlink(ids, request.session.eval_context(request.context))
498 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
499 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
500 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
501 c, d = eval_context_and_domain(req.session, context, domain)
502 if domain_id and len(args) - 1 >= domain_id:
504 if context_id and len(args) - 1 >= context_id:
507 return getattr(req.session.model(model), method)(*args)
509 @openerpweb.jsonrequest
510 def call(self, req, model, method, args, domain_id=None, context_id=None):
511 return {'result': self.call_common(req, model, method, args, domain_id, context_id)}
513 @openerpweb.jsonrequest
514 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
515 action = self.call_common(req, model, method, args, domain_id, context_id)
516 if isinstance(action, dict) and action.get('type') != '':
517 return {'result': clean_action(action, req.session)}
518 return {'result': False}
520 @openerpweb.jsonrequest
521 def exec_workflow(self, req, model, id, signal):
522 r = req.session.exec_workflow(model, id, signal)
525 @openerpweb.jsonrequest
526 def default_get(self, req, model, fields):
527 m = req.session.model(model)
528 r = m.default_get(fields, req.session.eval_context(req.context))
531 class DataGroup(openerpweb.Controller):
532 _cp_path = "/base/group"
533 @openerpweb.jsonrequest
534 def read(self, request, model, group_by_fields, domain=None):
535 Model = request.session.model(model)
536 context, domain = eval_context_and_domain(request.session, request.context, domain)
538 return Model.read_group(
539 domain or [], False, group_by_fields, 0, False,
540 dict(context, group_by=group_by_fields))
542 class View(openerpweb.Controller):
543 _cp_path = "/base/view"
545 def fields_view_get(self, request, model, view_id, view_type,
546 transform=True, toolbar=False, submenu=False):
547 Model = request.session.model(model)
548 context = request.session.eval_context(request.context)
549 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
550 # todo fme?: check that we should pass the evaluated context here
551 self.process_view(request.session, fvg, context, transform)
554 def process_view(self, session, fvg, context, transform):
555 # depending on how it feels, xmlrpclib.ServerProxy can translate
556 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
557 # enjoy unicode strings which can not be trivially converted to
558 # strings, and it blows up during parsing.
560 # So ensure we fix this retardation by converting view xml back to
562 if isinstance(fvg['arch'], unicode):
563 arch = fvg['arch'].encode('utf-8')
568 evaluation_context = session.evaluation_context(context or {})
569 xml = self.transform_view(arch, session, evaluation_context)
571 xml = ElementTree.fromstring(arch)
572 fvg['arch'] = Xml2Json.convert_element(xml)
573 for field in fvg['fields'].values():
574 if field.has_key('views') and field['views']:
575 for view in field["views"].values():
576 self.process_view(session, view, None, transform)
578 @openerpweb.jsonrequest
579 def add_custom(self, request, view_id, arch):
580 CustomView = request.session.model('ir.ui.view.custom')
582 'user_id': request.session._uid,
585 }, request.session.eval_context(request.context))
586 return {'result': True}
588 @openerpweb.jsonrequest
589 def undo_custom(self, request, view_id, reset=False):
590 CustomView = request.session.model('ir.ui.view.custom')
591 context = request.session.eval_context(request.context)
592 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
593 0, False, False, context)
596 CustomView.unlink(vcustom, context)
598 CustomView.unlink([vcustom[0]], context)
599 return {'result': True}
600 return {'result': False}
602 def normalize_attrs(self, elem, context):
603 """ Normalize @attrs, @invisible, @required, @readonly and @states, so
604 the client only has to deal with @attrs.
606 See `the discoveries pad <http://pad.openerp.com/discoveries>`_ for
609 :param elem: the current view node (Python object)
610 :type elem: xml.etree.ElementTree.Element
611 :param dict context: evaluation context
613 # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
614 attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
615 if 'states' in elem.attrib:
616 attrs.setdefault('invisible', [])\
617 .append(('state', 'not in', elem.attrib.pop('states').split(',')))
619 elem.set('attrs', simplejson.dumps(attrs))
620 for a in ['invisible', 'readonly', 'required']:
622 # In the XML we trust
623 avalue = bool(eval(elem.get(a, 'False'),
624 {'context': context or {}}))
629 if a == 'invisible' and 'attrs' in elem.attrib:
630 del elem.attrib['attrs']
632 def transform_view(self, view_string, session, context=None):
633 # transform nodes on the fly via iterparse, instead of
634 # doing it statically on the parsing result
635 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
637 for event, elem in parser:
641 self.normalize_attrs(elem, context)
642 self.parse_domains_and_contexts(elem, session)
645 def parse_domain(self, elem, attr_name, session):
646 """ Parses an attribute of the provided name as a domain, transforms it
647 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
649 :param elem: the node being parsed
650 :type param: xml.etree.ElementTree.Element
651 :param str attr_name: the name of the attribute which should be parsed
652 :param session: Current OpenERP session
653 :type session: openerpweb.openerpweb.OpenERPSession
655 domain = elem.get(attr_name, '').strip()
660 openerpweb.ast.literal_eval(
665 openerpweb.nonliterals.Domain(session, domain))
667 def parse_domains_and_contexts(self, elem, session):
668 """ Converts domains and contexts from the view into Python objects,
669 either literals if they can be parsed by literal_eval or a special
670 placeholder object if the domain or context refers to free variables.
672 :param elem: the current node being parsed
673 :type param: xml.etree.ElementTree.Element
674 :param session: OpenERP session object, used to store and retrieve
676 :type session: openerpweb.openerpweb.OpenERPSession
678 self.parse_domain(elem, 'domain', session)
679 self.parse_domain(elem, 'filter_domain', session)
680 for el in ['context', 'default_get']:
681 context_string = elem.get(el, '').strip()
685 openerpweb.ast.literal_eval(context_string))
688 openerpweb.nonliterals.Context(
689 session, context_string))
691 class FormView(View):
692 _cp_path = "/base/formview"
694 @openerpweb.jsonrequest
695 def load(self, req, model, view_id, toolbar=False):
696 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
697 return {'fields_view': fields_view}
699 class ListView(View):
700 _cp_path = "/base/listview"
702 @openerpweb.jsonrequest
703 def load(self, req, model, view_id, toolbar=False):
704 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
705 return {'fields_view': fields_view}
707 def process_colors(self, view, row, context):
708 colors = view['arch']['attrs'].get('colors')
715 for pair in colors.split(';')
716 if eval(pair.split(':')[1], dict(context, **row))
721 elif len(color) == 1:
725 class SearchView(View):
726 _cp_path = "/base/searchview"
728 @openerpweb.jsonrequest
729 def load(self, req, model, view_id):
730 fields_view = self.fields_view_get(req, model, view_id, 'search')
731 return {'fields_view': fields_view}
733 @openerpweb.jsonrequest
734 def fields_get(self, req, model):
735 Model = req.session.model(model)
736 fields = Model.fields_get(False, req.session.eval_context(req.context))
737 return {'fields': fields}
739 class Binary(openerpweb.Controller):
740 _cp_path = "/base/binary"
742 @openerpweb.httprequest
743 def image(self, request, session_id, model, id, field, **kw):
744 cherrypy.response.headers['Content-Type'] = 'image/png'
745 Model = request.session.model(model)
746 context = request.session.eval_context(request.context)
749 res = Model.default_get([field], context).get(field, '')
751 res = Model.read([int(id)], [field], context)[0].get(field, '')
752 return base64.decodestring(res)
753 except: # TODO: what's the exception here?
754 return self.placeholder()
755 def placeholder(self):
756 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
758 @openerpweb.httprequest
759 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
760 Model = request.session.model(model)
761 context = request.session.eval_context(request.context)
762 res = Model.read([int(id)], [field, fieldname], context)[0]
763 filecontent = res.get(field, '')
765 raise cherrypy.NotFound
767 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
768 filename = '%s_%s' % (model.replace('.', '_'), id)
770 filename = res.get(fieldname, '') or filename
771 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
772 return base64.decodestring(filecontent)
774 @openerpweb.httprequest
775 def upload(self, request, session_id, callback, ufile=None):
776 cherrypy.response.timeout = 500
778 for key, val in cherrypy.request.headers.iteritems():
779 headers[key.lower()] = val
780 size = int(headers.get('content-length', 0))
781 # TODO: might be useful to have a configuration flag for max-length file uploads
783 out = """<script language="javascript" type="text/javascript">
784 var win = window.top.window,
786 if (typeof(callback) === 'function') {
787 callback.apply(this, %s);
789 win.jQuery('#oe_notification', win.document).notify('create', {
790 title: "Ajax File Upload",
791 text: "Could not find callback"
795 data = ufile.file.read()
796 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
798 args = [False, e.message]
799 return out % (simplejson.dumps(callback), simplejson.dumps(args))
801 @openerpweb.httprequest
802 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
803 cherrypy.response.timeout = 500
804 context = request.session.eval_context(request.context)
805 Model = request.session.model('ir.attachment')
807 out = """<script language="javascript" type="text/javascript">
808 var win = window.top.window,
810 if (typeof(callback) === 'function') {
811 callback.call(this, %s);
814 attachment_id = Model.create({
815 'name': ufile.filename,
816 'datas': base64.encodestring(ufile.file.read()),
821 'filename': ufile.filename,
825 args = { 'error': e.message }
826 return out % (simplejson.dumps(callback), simplejson.dumps(args))
828 class Action(openerpweb.Controller):
829 _cp_path = "/base/action"
831 @openerpweb.jsonrequest
832 def load(self, req, action_id):
833 Actions = req.session.model('ir.actions.actions')
835 context = req.session.eval_context(req.context)
836 action_type = Actions.read([action_id], ['type'], context)
838 action = req.session.model(action_type[0]['type']).read([action_id], False,
841 value = clean_action(action[0], req.session)
842 return {'result': value}
844 @openerpweb.jsonrequest
845 def run(self, req, action_id):
846 return clean_action(req.session.model('ir.actions.server').run(
847 [action_id], req.session.eval_context(req.context)), req.session)