1 # -*- coding: utf-8 -*-
5 from xml.etree import ElementTree
6 from cStringIO import StringIO
12 import openerpweb.nonliterals
17 # Should move to openerpweb.Xml2Json
20 # Simple and straightforward XML-to-JSON converter in Python
23 # URL: http://code.google.com/p/xml2json-direct/
25 def convert_to_json(s):
26 return simplejson.dumps(
27 Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
30 def convert_to_structure(s):
31 root = ElementTree.fromstring(s)
32 return Xml2Json.convert_element(root)
35 def convert_element(el, skip_whitespaces=True):
38 ns, name = el.tag.rsplit("}", 1)
40 res["namespace"] = ns[1:]
44 for k, v in el.items():
47 if el.text and (not skip_whitespaces or el.text.strip() != ''):
50 kids.append(Xml2Json.convert_element(kid))
51 if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
53 res["children"] = kids
56 #----------------------------------------------------------
57 # OpenERP Web base Controllers
58 #----------------------------------------------------------
60 class Session(openerpweb.Controller):
61 _cp_path = "/base/session"
63 def manifest_glob(self, addons, key):
66 globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
69 resource_path[len(openerpweb.path_addons):]
70 for pattern in globlist
71 for resource_path in glob.glob(os.path.join(
72 openerpweb.path_addons, addon, pattern))
76 def concat_files(self, file_list):
77 """ Concatenate file content
78 return (concat,timestamp)
79 concat: concatenation of file content
80 timestamp: max(os.path.getmtime of file_list)
82 root = openerpweb.path_root
86 fname = os.path.join(root, i)
87 ftime = os.path.getmtime(fname)
88 if ftime > files_timestamp:
89 files_timestamp = ftime
90 files_content = open(fname).read()
91 files_concat = "".join(files_content)
94 @openerpweb.jsonrequest
95 def login(self, req, db, login, password):
96 req.session.login(db, login, password)
99 "session_id": req.session_id,
100 "uid": req.session._uid,
103 @openerpweb.jsonrequest
104 def sc_list(self, req):
105 return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
106 req.session.eval_context(req.context))
108 @openerpweb.jsonrequest
109 def get_databases_list(self, req):
110 proxy = req.session.proxy("db")
113 return {"db_list": dbs}
115 @openerpweb.jsonrequest
116 def get_lang_list(self, req):
117 lang_list = [('en_US', 'English (US)')]
119 lang_list = lang_list + (req.session.proxy("db").list_lang() or [])
122 return {"lang_list": lang_list}
124 @openerpweb.jsonrequest
125 def db_operation(self, req, flag, **kw):
129 password = kw.get('password')
130 return req.session.proxy("db").drop(password, db)
132 @openerpweb.jsonrequest
133 def modules(self, req):
134 return {"modules": [name
135 for name, manifest in openerpweb.addons_manifest.iteritems()
136 if manifest.get('active', True)]}
138 @openerpweb.jsonrequest
139 def csslist(self, req, mods='base'):
140 return {'files': self.manifest_glob(mods.split(','), 'css')}
142 @openerpweb.jsonrequest
143 def jslist(self, req, mods='base'):
144 return {'files': self.manifest_glob(mods.split(','), 'js')}
146 def css(self, req, mods='base,base_hello'):
147 files = self.manifest_glob(mods.split(','), 'css')
148 concat = self.concat_files(files)[0]
149 # TODO request set the Date of last modif and Etag
153 def js(self, req, mods='base,base_hello'):
154 files = self.manifest_glob(mods.split(','), 'js')
155 concat = self.concat_files(files)[0]
156 # TODO request set the Date of last modif and Etag
160 @openerpweb.jsonrequest
161 def eval_domain_and_context(self, req, contexts, domains,
163 """ Evaluates sequences of domains and contexts, composing them into
164 a single context, domain or group_by sequence.
166 :param list contexts: list of contexts to merge together. Contexts are
167 evaluated in sequence, all previous contexts
168 are part of their own evaluation context
169 (starting at the session context).
170 :param list domains: list of domains to merge together. Domains are
171 evaluated in sequence and appended to one another
172 (implicit AND), their evaluation domain is the
173 result of merging all contexts.
174 :param list group_by_seq: list of domains (which may be in a different
175 order than the ``contexts`` parameter),
176 evaluated in sequence, their ``'group_by'``
177 key is extracted if they have one.
182 the global context created by merging all of
186 the concatenation of all domains
189 a list of fields to group by, potentially empty (in which case
190 no group by should be performed)
192 context, domain = eval_context_and_domain(req.session,
193 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
194 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
196 group_by_sequence = []
197 for candidate in (group_by_seq or []):
198 ctx = req.session.eval_context(candidate, context)
199 group_by = ctx.get('group_by')
202 elif isinstance(group_by, basestring):
203 group_by_sequence.append(group_by)
205 group_by_sequence.extend(group_by)
210 'group_by': group_by_sequence
213 @openerpweb.jsonrequest
214 def save_session_action(self, req, the_action):
216 This method store an action object in the session object and returns an integer
217 identifying that action. The method get_session_action() can be used to get
220 :param the_action: The action to save in the session.
221 :type the_action: anything
222 :return: A key identifying the saved action.
225 saved_actions = cherrypy.session.get('saved_actions')
226 if not saved_actions:
227 saved_actions = {"next":0, "actions":{}}
228 cherrypy.session['saved_actions'] = saved_actions
229 # we don't allow more than 10 stored actions
230 if len(saved_actions["actions"]) >= 10:
231 del saved_actions["actions"][min(saved_actions["actions"].keys())]
232 key = saved_actions["next"]
233 saved_actions["actions"][key] = the_action
234 saved_actions["next"] = key + 1
237 @openerpweb.jsonrequest
238 def get_session_action(self, req, key):
240 Gets back a previously saved action. This method can return None if the action
241 was saved since too much time (this case should be handled in a smart way).
243 :param key: The key given by save_session_action()
245 :return: The saved action or None.
248 saved_actions = cherrypy.session.get('saved_actions')
249 if not saved_actions:
251 return saved_actions["actions"].get(key)
253 def eval_context_and_domain(session, context, domain=None):
254 e_context = session.eval_context(context)
255 # should we give the evaluated context as an evaluation context to the domain?
256 e_domain = session.eval_domain(domain or [])
258 return e_context, e_domain
260 def load_actions_from_ir_values(req, key, key2, models, meta, context):
261 Values = req.session.model('ir.values')
262 actions = Values.get(key, key2, models, meta, context)
264 return [(id, name, clean_action(action, req.session))
265 for id, name, action in actions]
267 def clean_action(action, session):
268 if action['type'] != 'ir.actions.act_window':
270 # values come from the server, we can just eval them
271 if isinstance(action.get('context', None), basestring):
272 action['context'] = eval(
274 session.evaluation_context()) or {}
276 if isinstance(action.get('domain', None), basestring):
277 action['domain'] = eval(
279 session.evaluation_context(
280 action['context'])) or []
281 if 'flags' not in action:
282 # Set empty flags dictionary for web client.
283 action['flags'] = dict()
284 return fix_view_modes(action)
286 def generate_views(action):
288 While the server generates a sequence called "views" computing dependencies
289 between a bunch of stuff for views coming directly from the database
290 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
291 to return custom view dictionaries generated on the fly.
293 In that case, there is no ``views`` key available on the action.
295 Since the web client relies on ``action['views']``, generate it here from
296 ``view_mode`` and ``view_id``.
298 Currently handles two different cases:
300 * no view_id, multiple view_mode
301 * single view_id, single view_mode
303 :param dict action: action descriptor dictionary to generate a views key for
305 view_id = action.get('view_id', False)
306 if isinstance(view_id, (list, tuple)):
309 # providing at least one view mode is a requirement, not an option
310 view_modes = action['view_mode'].split(',')
312 if len(view_modes) > 1:
314 raise ValueError('Non-db action dictionaries should provide '
315 'either multiple view modes or a single view '
316 'mode and an optional view id.\n\n Got view '
317 'modes %r and view id %r for action %r' % (
318 view_modes, view_id, action))
319 action['views'] = [(False, mode) for mode in view_modes]
321 action['views'] = [(view_id, view_modes[0])]
324 def fix_view_modes(action):
325 """ For historical reasons, OpenERP has weird dealings in relation to
326 view_mode and the view_type attribute (on window actions):
328 * one of the view modes is ``tree``, which stands for both list views
330 * the choice is made by checking ``view_type``, which is either
331 ``form`` for a list view or ``tree`` for an actual tree view
333 This methods simply folds the view_type into view_mode by adding a
334 new view mode ``list`` which is the result of the ``tree`` view_mode
335 in conjunction with the ``form`` view_type.
337 TODO: this should go into the doc, some kind of "peculiarities" section
339 :param dict action: an action descriptor
340 :returns: nothing, the action is modified in place
342 if 'views' not in action:
343 generate_views(action)
345 if action.pop('view_type') != 'form':
349 [id, mode if mode != 'tree' else 'list']
350 for id, mode in action['views']
355 class Menu(openerpweb.Controller):
356 _cp_path = "/base/menu"
358 @openerpweb.jsonrequest
360 return {'data': self.do_load(req)}
362 def do_load(self, req):
363 """ Loads all menu items (all applications and their sub-menus).
365 :param req: A request object, with an OpenERP session attribute
366 :type req: < session -> OpenERPSession >
367 :return: the menu root
368 :rtype: dict('children': menu_nodes)
370 Menus = req.session.model('ir.ui.menu')
371 # menus are loaded fully unlike a regular tree view, cause there are
372 # less than 512 items
373 context = req.session.eval_context(req.context)
374 menu_ids = Menus.search([], 0, False, False, context)
375 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
376 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
377 menu_items.append(menu_root)
379 # make a tree using parent_id
380 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
381 for menu_item in menu_items:
382 if menu_item['parent_id']:
383 parent = menu_item['parent_id'][0]
386 if parent in menu_items_map:
387 menu_items_map[parent].setdefault(
388 'children', []).append(menu_item)
390 # sort by sequence a tree using parent_id
391 for menu_item in menu_items:
392 menu_item.setdefault('children', []).sort(
393 key=lambda x:x["sequence"])
397 @openerpweb.jsonrequest
398 def action(self, req, menu_id):
399 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
400 [('ir.ui.menu', menu_id)], False,
401 req.session.eval_context(req.context))
402 return {"action": actions}
404 class DataSet(openerpweb.Controller):
405 _cp_path = "/base/dataset"
407 @openerpweb.jsonrequest
408 def fields(self, req, model):
409 return {'fields': req.session.model(model).fields_get(False,
410 req.session.eval_context(req.context))}
412 @openerpweb.jsonrequest
413 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
414 return self.do_search_read(request, model, fields, offset, limit, domain, context, sort)
415 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
416 """ Performs a search() followed by a read() (if needed) using the
417 provided search criteria
419 :param request: a JSON-RPC request object
420 :type request: openerpweb.JsonRequest
421 :param str model: the name of the model to search on
422 :param fields: a list of the fields to return in the result records
424 :param int offset: from which index should the results start being returned
425 :param int limit: the maximum number of records to return
426 :param list domain: the search domain for the query
427 :param list sort: sorting directives
428 :returns: a list of result records
431 Model = request.session.model(model)
432 context, domain = eval_context_and_domain(request.session, request.context, domain)
434 ids = Model.search(domain, offset or 0, limit or False,
435 sort or False, context)
437 if fields and fields == ['id']:
438 # shortcut read if we only want the ids
439 return map(lambda id: {'id': id}, ids)
441 reads = Model.read(ids, fields or False, context)
442 reads.sort(key=lambda obj: ids.index(obj['id']))
445 @openerpweb.jsonrequest
446 def get(self, request, model, ids, fields=False):
447 return self.do_get(request, model, ids, fields)
448 def do_get(self, request, model, ids, fields=False):
449 """ Fetches and returns the records of the model ``model`` whose ids
452 The results are in the same order as the inputs, but elements may be
453 missing (if there is no record left for the id)
455 :param request: the JSON-RPC2 request object
456 :type request: openerpweb.JsonRequest
457 :param model: the model to read from
459 :param ids: a list of identifiers
461 :param fields: a list of fields to fetch, ``False`` or empty to fetch
462 all fields in the model
463 :type fields: list | False
464 :returns: a list of records, in the same order as the list of ids
467 Model = request.session.model(model)
468 records = Model.read(ids, fields, request.session.eval_context(request.context))
470 record_map = dict((record['id'], record) for record in records)
472 return [record_map[id] for id in ids if record_map.get(id)]
474 @openerpweb.jsonrequest
475 def load(self, req, model, id, fields):
476 m = req.session.model(model)
478 r = m.read([id], False, req.session.eval_context(req.context))
481 return {'value': value}
483 @openerpweb.jsonrequest
484 def create(self, req, model, data):
485 m = req.session.model(model)
486 r = m.create(data, req.session.eval_context(req.context))
489 @openerpweb.jsonrequest
490 def save(self, req, model, id, data):
491 m = req.session.model(model)
492 r = m.write([id], data, req.session.eval_context(req.context))
495 @openerpweb.jsonrequest
496 def unlink(self, request, model, ids=()):
497 Model = request.session.model(model)
498 return Model.unlink(ids, request.session.eval_context(request.context))
500 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
501 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
502 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
503 c, d = eval_context_and_domain(req.session, context, domain)
504 if domain_id and len(args) - 1 >= domain_id:
506 if context_id and len(args) - 1 >= context_id:
509 return getattr(req.session.model(model), method)(*args)
511 @openerpweb.jsonrequest
512 def call(self, req, model, method, args, domain_id=None, context_id=None):
513 return {'result': self.call_common(req, model, method, args, domain_id, context_id)}
515 @openerpweb.jsonrequest
516 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
517 action = self.call_common(req, model, method, args, domain_id, context_id)
518 if isinstance(action, dict) and action.get('type') != '':
519 return {'result': clean_action(action, req.session)}
520 return {'result': False}
522 @openerpweb.jsonrequest
523 def exec_workflow(self, req, model, id, signal):
524 r = req.session.exec_workflow(model, id, signal)
527 @openerpweb.jsonrequest
528 def default_get(self, req, model, fields):
529 m = req.session.model(model)
530 r = m.default_get(fields, req.session.eval_context(req.context))
533 class DataGroup(openerpweb.Controller):
534 _cp_path = "/base/group"
535 @openerpweb.jsonrequest
536 def read(self, request, model, group_by_fields, domain=None):
537 Model = request.session.model(model)
538 context, domain = eval_context_and_domain(request.session, request.context, domain)
540 return Model.read_group(
541 domain or [], False, group_by_fields, 0, False,
542 dict(context, group_by=group_by_fields))
544 class View(openerpweb.Controller):
545 _cp_path = "/base/view"
547 def fields_view_get(self, request, model, view_id, view_type,
548 transform=True, toolbar=False, submenu=False):
549 Model = request.session.model(model)
550 context = request.session.eval_context(request.context)
551 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
552 # todo fme?: check that we should pass the evaluated context here
553 self.process_view(request.session, fvg, context, transform)
556 def process_view(self, session, fvg, context, transform):
557 # depending on how it feels, xmlrpclib.ServerProxy can translate
558 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
559 # enjoy unicode strings which can not be trivially converted to
560 # strings, and it blows up during parsing.
562 # So ensure we fix this retardation by converting view xml back to
564 if isinstance(fvg['arch'], unicode):
565 arch = fvg['arch'].encode('utf-8')
570 evaluation_context = session.evaluation_context(context or {})
571 xml = self.transform_view(arch, session, evaluation_context)
573 xml = ElementTree.fromstring(arch)
574 fvg['arch'] = Xml2Json.convert_element(xml)
575 for field in fvg['fields'].values():
576 if field.has_key('views') and field['views']:
577 for view in field["views"].values():
578 self.process_view(session, view, None, transform)
580 @openerpweb.jsonrequest
581 def add_custom(self, request, view_id, arch):
582 CustomView = request.session.model('ir.ui.view.custom')
584 'user_id': request.session._uid,
587 }, request.session.eval_context(request.context))
588 return {'result': True}
590 @openerpweb.jsonrequest
591 def undo_custom(self, request, view_id, reset=False):
592 CustomView = request.session.model('ir.ui.view.custom')
593 context = request.session.eval_context(request.context)
594 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
595 0, False, False, context)
598 CustomView.unlink(vcustom, context)
600 CustomView.unlink([vcustom[0]], context)
601 return {'result': True}
602 return {'result': False}
604 def normalize_attrs(self, elem, context):
605 """ Normalize @attrs, @invisible, @required, @readonly and @states, so
606 the client only has to deal with @attrs.
608 See `the discoveries pad <http://pad.openerp.com/discoveries>`_ for
611 :param elem: the current view node (Python object)
612 :type elem: xml.etree.ElementTree.Element
613 :param dict context: evaluation context
615 # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
616 attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
617 if 'states' in elem.attrib:
618 attrs.setdefault('invisible', [])\
619 .append(('state', 'not in', elem.attrib.pop('states').split(',')))
621 elem.set('attrs', simplejson.dumps(attrs))
622 for a in ['invisible', 'readonly', 'required']:
624 # In the XML we trust
625 avalue = bool(eval(elem.get(a, 'False'),
626 {'context': context or {}}))
631 if a == 'invisible' and 'attrs' in elem.attrib:
632 del elem.attrib['attrs']
634 def transform_view(self, view_string, session, context=None):
635 # transform nodes on the fly via iterparse, instead of
636 # doing it statically on the parsing result
637 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
639 for event, elem in parser:
643 self.normalize_attrs(elem, context)
644 self.parse_domains_and_contexts(elem, session)
647 def parse_domain(self, elem, attr_name, session):
648 """ Parses an attribute of the provided name as a domain, transforms it
649 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
651 :param elem: the node being parsed
652 :type param: xml.etree.ElementTree.Element
653 :param str attr_name: the name of the attribute which should be parsed
654 :param session: Current OpenERP session
655 :type session: openerpweb.openerpweb.OpenERPSession
657 domain = elem.get(attr_name, '').strip()
662 openerpweb.ast.literal_eval(
667 openerpweb.nonliterals.Domain(session, domain))
669 def parse_domains_and_contexts(self, elem, session):
670 """ Converts domains and contexts from the view into Python objects,
671 either literals if they can be parsed by literal_eval or a special
672 placeholder object if the domain or context refers to free variables.
674 :param elem: the current node being parsed
675 :type param: xml.etree.ElementTree.Element
676 :param session: OpenERP session object, used to store and retrieve
678 :type session: openerpweb.openerpweb.OpenERPSession
680 self.parse_domain(elem, 'domain', session)
681 self.parse_domain(elem, 'filter_domain', session)
682 for el in ['context', 'default_get']:
683 context_string = elem.get(el, '').strip()
687 openerpweb.ast.literal_eval(context_string))
690 openerpweb.nonliterals.Context(
691 session, context_string))
693 class FormView(View):
694 _cp_path = "/base/formview"
696 @openerpweb.jsonrequest
697 def load(self, req, model, view_id, toolbar=False):
698 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
699 return {'fields_view': fields_view}
701 class ListView(View):
702 _cp_path = "/base/listview"
704 @openerpweb.jsonrequest
705 def load(self, req, model, view_id, toolbar=False):
706 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
707 return {'fields_view': fields_view}
709 def process_colors(self, view, row, context):
710 colors = view['arch']['attrs'].get('colors')
717 for pair in colors.split(';')
718 if eval(pair.split(':')[1], dict(context, **row))
723 elif len(color) == 1:
727 class SearchView(View):
728 _cp_path = "/base/searchview"
730 @openerpweb.jsonrequest
731 def load(self, req, model, view_id):
732 fields_view = self.fields_view_get(req, model, view_id, 'search')
733 return {'fields_view': fields_view}
735 @openerpweb.jsonrequest
736 def fields_get(self, req, model):
737 Model = req.session.model(model)
738 fields = Model.fields_get(False, req.session.eval_context(req.context))
739 return {'fields': fields}
741 class Binary(openerpweb.Controller):
742 _cp_path = "/base/binary"
744 @openerpweb.httprequest
745 def image(self, request, session_id, model, id, field, **kw):
746 cherrypy.response.headers['Content-Type'] = 'image/png'
747 Model = request.session.model(model)
748 context = request.session.eval_context(request.context)
751 res = Model.default_get([field], context).get(field, '')
753 res = Model.read([int(id)], [field], context)[0].get(field, '')
754 return base64.decodestring(res)
755 except: # TODO: what's the exception here?
756 return self.placeholder()
757 def placeholder(self):
758 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
760 @openerpweb.httprequest
761 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
762 Model = request.session.model(model)
763 context = request.session.eval_context(request.context)
764 res = Model.read([int(id)], [field, fieldname], context)[0]
765 filecontent = res.get(field, '')
767 raise cherrypy.NotFound
769 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
770 filename = '%s_%s' % (model.replace('.', '_'), id)
772 filename = res.get(fieldname, '') or filename
773 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
774 return base64.decodestring(filecontent)
776 @openerpweb.httprequest
777 def upload(self, request, session_id, callback, ufile=None):
778 cherrypy.response.timeout = 500
780 for key, val in cherrypy.request.headers.iteritems():
781 headers[key.lower()] = val
782 size = int(headers.get('content-length', 0))
783 # TODO: might be useful to have a configuration flag for max-length file uploads
785 out = """<script language="javascript" type="text/javascript">
786 var win = window.top.window,
788 if (typeof(callback) === 'function') {
789 callback.apply(this, %s);
791 win.jQuery('#oe_notification', win.document).notify('create', {
792 title: "Ajax File Upload",
793 text: "Could not find callback"
797 data = ufile.file.read()
798 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
800 args = [False, e.message]
801 return out % (simplejson.dumps(callback), simplejson.dumps(args))
803 @openerpweb.httprequest
804 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
805 cherrypy.response.timeout = 500
806 context = request.session.eval_context(request.context)
807 Model = request.session.model('ir.attachment')
809 out = """<script language="javascript" type="text/javascript">
810 var win = window.top.window,
812 if (typeof(callback) === 'function') {
813 callback.call(this, %s);
816 attachment_id = Model.create({
817 'name': ufile.filename,
818 'datas': base64.encodestring(ufile.file.read()),
823 'filename': ufile.filename,
827 args = { 'error': e.message }
828 return out % (simplejson.dumps(callback), simplejson.dumps(args))
830 class Action(openerpweb.Controller):
831 _cp_path = "/base/action"
833 @openerpweb.jsonrequest
834 def load(self, req, action_id):
835 Actions = req.session.model('ir.actions.actions')
837 context = req.session.eval_context(req.context)
838 action_type = Actions.read([action_id], ['type'], context)
840 action = req.session.model(action_type[0]['type']).read([action_id], False,
843 value = clean_action(action[0], req.session)
844 return {'result': value}
846 @openerpweb.jsonrequest
847 def run(self, req, action_id):
848 return clean_action(req.session.model('ir.actions.server').run(
849 [action_id], req.session.eval_context(req.context)), req.session)