1 # -*- coding: utf-8 -*-
2 import base64, glob, os, re
3 from xml.etree import ElementTree
4 from cStringIO import StringIO
10 import openerpweb.nonliterals
14 # Should move to openerpweb.Xml2Json
17 # Simple and straightforward XML-to-JSON converter in Python
20 # URL: http://code.google.com/p/xml2json-direct/
22 def convert_to_json(s):
23 return simplejson.dumps(
24 Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
27 def convert_to_structure(s):
28 root = ElementTree.fromstring(s)
29 return Xml2Json.convert_element(root)
32 def convert_element(el, skip_whitespaces=True):
35 ns, name = el.tag.rsplit("}", 1)
37 res["namespace"] = ns[1:]
41 for k, v in el.items():
44 if el.text and (not skip_whitespaces or el.text.strip() != ''):
47 kids.append(Xml2Json.convert_element(kid))
48 if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
50 res["children"] = kids
53 #----------------------------------------------------------
54 # OpenERP Web base Controllers
55 #----------------------------------------------------------
57 class Database(openerpweb.Controller):
58 _cp_path = "/base/database"
60 @openerpweb.jsonrequest
61 def get_databases_list(self, req):
62 proxy = req.session.proxy("db")
64 h = req.httprequest.headers['Host'].split(':')[0]
66 r = cherrypy.config['openerp.dbfilter'].replace('%h',h).replace('%d',d)
68 dbs = [i for i in dbs if re.match(r,i)]
69 return {"db_list": dbs}
71 class Session(openerpweb.Controller):
72 _cp_path = "/base/session"
74 def manifest_glob(self, addons, key):
77 globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
80 resource_path[len(openerpweb.path_addons):]
81 for pattern in globlist
82 for resource_path in glob.glob(os.path.join(
83 openerpweb.path_addons, addon, pattern))
87 def concat_files(self, file_list):
88 """ Concatenate file content
89 return (concat,timestamp)
90 concat: concatenation of file content
91 timestamp: max(os.path.getmtime of file_list)
93 root = openerpweb.path_root
97 fname = os.path.join(root, i)
98 ftime = os.path.getmtime(fname)
99 if ftime > files_timestamp:
100 files_timestamp = ftime
101 files_content = open(fname).read()
102 files_concat = "".join(files_content)
105 @openerpweb.jsonrequest
106 def login(self, req, db, login, password):
107 req.session.login(db, login, password)
110 "session_id": req.session_id,
111 "uid": req.session._uid,
114 @openerpweb.jsonrequest
115 def sc_list(self, req):
116 return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
117 req.session.eval_context(req.context))
119 @openerpweb.jsonrequest
120 def modules(self, req):
121 return {"modules": [name
122 for name, manifest in openerpweb.addons_manifest.iteritems()
123 if manifest.get('active', True)]}
125 @openerpweb.jsonrequest
126 def csslist(self, req, mods='base'):
127 return {'files': self.manifest_glob(mods.split(','), 'css')}
129 @openerpweb.jsonrequest
130 def jslist(self, req, mods='base'):
131 return {'files': self.manifest_glob(mods.split(','), 'js')}
133 def css(self, req, mods='base'):
134 files = self.manifest_glob(mods.split(','), 'css')
135 concat = self.concat_files(files)[0]
136 # TODO request set the Date of last modif and Etag
140 def js(self, req, mods='base'):
141 files = self.manifest_glob(mods.split(','), 'js')
142 concat = self.concat_files(files)[0]
143 # TODO request set the Date of last modif and Etag
147 @openerpweb.jsonrequest
148 def eval_domain_and_context(self, req, contexts, domains,
150 """ Evaluates sequences of domains and contexts, composing them into
151 a single context, domain or group_by sequence.
153 :param list contexts: list of contexts to merge together. Contexts are
154 evaluated in sequence, all previous contexts
155 are part of their own evaluation context
156 (starting at the session context).
157 :param list domains: list of domains to merge together. Domains are
158 evaluated in sequence and appended to one another
159 (implicit AND), their evaluation domain is the
160 result of merging all contexts.
161 :param list group_by_seq: list of domains (which may be in a different
162 order than the ``contexts`` parameter),
163 evaluated in sequence, their ``'group_by'``
164 key is extracted if they have one.
169 the global context created by merging all of
173 the concatenation of all domains
176 a list of fields to group by, potentially empty (in which case
177 no group by should be performed)
179 context, domain = eval_context_and_domain(req.session,
180 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
181 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
183 group_by_sequence = []
184 for candidate in (group_by_seq or []):
185 ctx = req.session.eval_context(candidate, context)
186 group_by = ctx.get('group_by')
189 elif isinstance(group_by, basestring):
190 group_by_sequence.append(group_by)
192 group_by_sequence.extend(group_by)
197 'group_by': group_by_sequence
200 @openerpweb.jsonrequest
201 def save_session_action(self, req, the_action):
203 This method store an action object in the session object and returns an integer
204 identifying that action. The method get_session_action() can be used to get
207 :param the_action: The action to save in the session.
208 :type the_action: anything
209 :return: A key identifying the saved action.
212 saved_actions = cherrypy.session.get('saved_actions')
213 if not saved_actions:
214 saved_actions = {"next":0, "actions":{}}
215 cherrypy.session['saved_actions'] = saved_actions
216 # we don't allow more than 10 stored actions
217 if len(saved_actions["actions"]) >= 10:
218 del saved_actions["actions"][min(saved_actions["actions"].keys())]
219 key = saved_actions["next"]
220 saved_actions["actions"][key] = the_action
221 saved_actions["next"] = key + 1
224 @openerpweb.jsonrequest
225 def get_session_action(self, req, key):
227 Gets back a previously saved action. This method can return None if the action
228 was saved since too much time (this case should be handled in a smart way).
230 :param key: The key given by save_session_action()
232 :return: The saved action or None.
235 saved_actions = cherrypy.session.get('saved_actions')
236 if not saved_actions:
238 return saved_actions["actions"].get(key)
240 def eval_context_and_domain(session, context, domain=None):
241 e_context = session.eval_context(context)
242 # should we give the evaluated context as an evaluation context to the domain?
243 e_domain = session.eval_domain(domain or [])
245 return e_context, e_domain
247 def load_actions_from_ir_values(req, key, key2, models, meta, context):
248 Values = req.session.model('ir.values')
249 actions = Values.get(key, key2, models, meta, context)
251 return [(id, name, clean_action(action, req.session))
252 for id, name, action in actions]
254 def clean_action(action, session):
255 if action['type'] != 'ir.actions.act_window':
257 # values come from the server, we can just eval them
258 if isinstance(action.get('context', None), basestring):
259 action['context'] = eval(
261 session.evaluation_context()) or {}
263 if isinstance(action.get('domain', None), basestring):
264 action['domain'] = eval(
266 session.evaluation_context(
267 action['context'])) or []
268 if 'flags' not in action:
269 # Set empty flags dictionary for web client.
270 action['flags'] = dict()
271 return fix_view_modes(action)
273 def generate_views(action):
275 While the server generates a sequence called "views" computing dependencies
276 between a bunch of stuff for views coming directly from the database
277 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
278 to return custom view dictionaries generated on the fly.
280 In that case, there is no ``views`` key available on the action.
282 Since the web client relies on ``action['views']``, generate it here from
283 ``view_mode`` and ``view_id``.
285 Currently handles two different cases:
287 * no view_id, multiple view_mode
288 * single view_id, single view_mode
290 :param dict action: action descriptor dictionary to generate a views key for
292 view_id = action.get('view_id', False)
293 if isinstance(view_id, (list, tuple)):
296 # providing at least one view mode is a requirement, not an option
297 view_modes = action['view_mode'].split(',')
299 if len(view_modes) > 1:
301 raise ValueError('Non-db action dictionaries should provide '
302 'either multiple view modes or a single view '
303 'mode and an optional view id.\n\n Got view '
304 'modes %r and view id %r for action %r' % (
305 view_modes, view_id, action))
306 action['views'] = [(False, mode) for mode in view_modes]
308 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 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 Model = req.session.model(model)
528 return Model.default_get(fields, req.session.eval_context(req.context))
530 class DataGroup(openerpweb.Controller):
531 _cp_path = "/base/group"
532 @openerpweb.jsonrequest
533 def read(self, request, model, fields, group_by_fields, domain=None, sort=None):
534 Model = request.session.model(model)
535 context, domain = eval_context_and_domain(request.session, request.context, domain)
537 return Model.read_group(
538 domain or [], fields, group_by_fields, 0, False,
539 dict(context, group_by=group_by_fields), sort or False)
541 class View(openerpweb.Controller):
542 _cp_path = "/base/view"
544 def fields_view_get(self, request, model, view_id, view_type,
545 transform=True, toolbar=False, submenu=False):
546 Model = request.session.model(model)
547 context = request.session.eval_context(request.context)
548 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
549 # todo fme?: check that we should pass the evaluated context here
550 self.process_view(request.session, fvg, context, transform)
553 def process_view(self, session, fvg, context, transform):
554 # depending on how it feels, xmlrpclib.ServerProxy can translate
555 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
556 # enjoy unicode strings which can not be trivially converted to
557 # strings, and it blows up during parsing.
559 # So ensure we fix this retardation by converting view xml back to
561 if isinstance(fvg['arch'], unicode):
562 arch = fvg['arch'].encode('utf-8')
567 evaluation_context = session.evaluation_context(context or {})
568 xml = self.transform_view(arch, session, evaluation_context)
570 xml = ElementTree.fromstring(arch)
571 fvg['arch'] = Xml2Json.convert_element(xml)
572 for field in fvg['fields'].values():
573 if field.has_key('views') and field['views']:
574 for view in field["views"].values():
575 self.process_view(session, view, None, transform)
576 if field.get('domain'):
577 field["domain"] = self.parse_domain(field["domain"], session)
578 if field.get('context'):
579 field["context"] = self.parse_domain(field["context"], session)
581 @openerpweb.jsonrequest
582 def add_custom(self, request, view_id, arch):
583 CustomView = request.session.model('ir.ui.view.custom')
585 'user_id': request.session._uid,
588 }, request.session.eval_context(request.context))
589 return {'result': True}
591 @openerpweb.jsonrequest
592 def undo_custom(self, request, view_id, reset=False):
593 CustomView = request.session.model('ir.ui.view.custom')
594 context = request.session.eval_context(request.context)
595 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
596 0, False, False, context)
599 CustomView.unlink(vcustom, context)
601 CustomView.unlink([vcustom[0]], context)
602 return {'result': True}
603 return {'result': False}
605 def transform_view(self, view_string, session, context=None):
606 # transform nodes on the fly via iterparse, instead of
607 # doing it statically on the parsing result
608 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
610 for event, elem in parser:
614 self.parse_domains_and_contexts(elem, session)
617 def parse_domain(self, domain, session):
618 """ Parses an arbitrary string containing a domain, transforms it
619 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
621 :param domain: the domain to parse, if the domain is not a string it is assumed to
622 be a literal domain and is returned as-is
623 :param session: Current OpenERP session
624 :type session: openerpweb.openerpweb.OpenERPSession
626 if not isinstance(domain, (str, unicode)):
629 return openerpweb.ast.literal_eval(domain)
632 return openerpweb.nonliterals.Domain(session, domain)
634 def parse_context(self, context, session):
635 """ Parses an arbitrary string containing a context, transforms it
636 to either a literal context or a :class:`openerpweb.nonliterals.Context`
638 :param context: the context to parse, if the context is not a string it is assumed to
639 be a literal domain and is returned as-is
640 :param session: Current OpenERP session
641 :type session: openerpweb.openerpweb.OpenERPSession
643 if not isinstance(context, (str, unicode)):
646 return openerpweb.ast.literal_eval(context)
648 return openerpweb.nonliterals.Context(session, context)
650 def parse_domains_and_contexts(self, elem, session):
651 """ Converts domains and contexts from the view into Python objects,
652 either literals if they can be parsed by literal_eval or a special
653 placeholder object if the domain or context refers to free variables.
655 :param elem: the current node being parsed
656 :type param: xml.etree.ElementTree.Element
657 :param session: OpenERP session object, used to store and retrieve
659 :type session: openerpweb.openerpweb.OpenERPSession
661 for el in ['domain', 'filter_domain']:
662 domain = elem.get(el, '').strip()
664 elem.set(el, self.parse_domain(domain, session))
665 for el in ['context', 'default_get']:
666 context_string = elem.get(el, '').strip()
668 elem.set(el, self.parse_context(context_string, session))
670 class FormView(View):
671 _cp_path = "/base/formview"
673 @openerpweb.jsonrequest
674 def load(self, req, model, view_id, toolbar=False):
675 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
676 return {'fields_view': fields_view}
678 class ListView(View):
679 _cp_path = "/base/listview"
681 @openerpweb.jsonrequest
682 def load(self, req, model, view_id, toolbar=False):
683 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
684 return {'fields_view': fields_view}
686 def process_colors(self, view, row, context):
687 colors = view['arch']['attrs'].get('colors')
694 for pair in colors.split(';')
695 if eval(pair.split(':')[1], dict(context, **row))
700 elif len(color) == 1:
704 class SearchView(View):
705 _cp_path = "/base/searchview"
707 @openerpweb.jsonrequest
708 def load(self, req, model, view_id):
709 fields_view = self.fields_view_get(req, model, view_id, 'search')
710 return {'fields_view': fields_view}
712 @openerpweb.jsonrequest
713 def fields_get(self, req, model):
714 Model = req.session.model(model)
715 fields = Model.fields_get(False, req.session.eval_context(req.context))
716 return {'fields': fields}
718 class Binary(openerpweb.Controller):
719 _cp_path = "/base/binary"
721 @openerpweb.httprequest
722 def image(self, request, session_id, model, id, field, **kw):
723 cherrypy.response.headers['Content-Type'] = 'image/png'
724 Model = request.session.model(model)
725 context = request.session.eval_context(request.context)
728 res = Model.default_get([field], context).get(field, '')
730 res = Model.read([int(id)], [field], context)[0].get(field, '')
731 return base64.decodestring(res)
732 except: # TODO: what's the exception here?
733 return self.placeholder()
734 def placeholder(self):
735 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
737 @openerpweb.httprequest
738 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
739 Model = request.session.model(model)
740 context = request.session.eval_context(request.context)
741 res = Model.read([int(id)], [field, fieldname], context)[0]
742 filecontent = res.get(field, '')
744 raise cherrypy.NotFound
746 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
747 filename = '%s_%s' % (model.replace('.', '_'), id)
749 filename = res.get(fieldname, '') or filename
750 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
751 return base64.decodestring(filecontent)
753 @openerpweb.httprequest
754 def upload(self, request, session_id, callback, ufile=None):
755 cherrypy.response.timeout = 500
757 for key, val in cherrypy.request.headers.iteritems():
758 headers[key.lower()] = val
759 size = int(headers.get('content-length', 0))
760 # TODO: might be useful to have a configuration flag for max-length file uploads
762 out = """<script language="javascript" type="text/javascript">
763 var win = window.top.window,
765 if (typeof(callback) === 'function') {
766 callback.apply(this, %s);
768 win.jQuery('#oe_notification', win.document).notify('create', {
769 title: "Ajax File Upload",
770 text: "Could not find callback"
774 data = ufile.file.read()
775 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
777 args = [False, e.message]
778 return out % (simplejson.dumps(callback), simplejson.dumps(args))
780 @openerpweb.httprequest
781 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
782 cherrypy.response.timeout = 500
783 context = request.session.eval_context(request.context)
784 Model = request.session.model('ir.attachment')
786 out = """<script language="javascript" type="text/javascript">
787 var win = window.top.window,
789 if (typeof(callback) === 'function') {
790 callback.call(this, %s);
793 attachment_id = Model.create({
794 'name': ufile.filename,
795 'datas': base64.encodestring(ufile.file.read()),
800 'filename': ufile.filename,
804 args = { 'error': e.message }
805 return out % (simplejson.dumps(callback), simplejson.dumps(args))
807 class Action(openerpweb.Controller):
808 _cp_path = "/base/action"
810 @openerpweb.jsonrequest
811 def load(self, req, action_id):
812 Actions = req.session.model('ir.actions.actions')
814 context = req.session.eval_context(req.context)
815 action_type = Actions.read([action_id], ['type'], context)
817 action = req.session.model(action_type[0]['type']).read([action_id], False,
820 value = clean_action(action[0], req.session)
821 return {'result': value}
823 @openerpweb.jsonrequest
824 def run(self, req, action_id):
825 return clean_action(req.session.model('ir.actions.server').run(
826 [action_id], req.session.eval_context(req.context)), req.session)