1 # -*- coding: utf-8 -*-
4 from xml.etree import ElementTree
5 from cStringIO import StringIO
11 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):
131 password = kw.get('password')
133 return req.session.proxy("db").drop(password, db)
135 elif flag == 'backup':
137 password = kw.get('password')
139 res = req.session.proxy("db").dump(password, db)
141 cherrypy.response.headers['Content-Type'] = "application/data"
142 cherrypy.response.headers['Content-Disposition'] = 'filename="' + db + '.dump"'
143 return base64.decodestring(res)
145 return {'error': 'Could not create backup !'}
147 elif flag == 'restore':
148 filename = kw.get('filename')
150 password = kw.get('password')
154 data = base64.encodestring(filename.file.read())
155 return req.session.proxy("db").restore(password, db, data)
157 return {'error': 'Could not restore database !'}
159 elif flag == 'change_password':
160 old_password = kw.get('old_password')
161 new_password = kw.get('new_password')
162 confirm_password = kw.get('confirm_password')
164 if old_password and new_password and confirm_password:
165 return req.session.proxy("db").change_admin_password(old_password, new_password)
167 @openerpweb.jsonrequest
168 def modules(self, req):
169 return {"modules": [name
170 for name, manifest in openerpweb.addons_manifest.iteritems()
171 if manifest.get('active', True)]}
173 @openerpweb.jsonrequest
174 def csslist(self, req, mods='base'):
175 return {'files': self.manifest_glob(mods.split(','), 'css')}
177 @openerpweb.jsonrequest
178 def jslist(self, req, mods='base'):
179 return {'files': self.manifest_glob(mods.split(','), 'js')}
181 def css(self, req, mods='base'):
182 files = self.manifest_glob(mods.split(','), 'css')
183 concat = self.concat_files(files)[0]
184 # TODO request set the Date of last modif and Etag
188 def js(self, req, mods='base'):
189 files = self.manifest_glob(mods.split(','), 'js')
190 concat = self.concat_files(files)[0]
191 # TODO request set the Date of last modif and Etag
195 @openerpweb.jsonrequest
196 def eval_domain_and_context(self, req, contexts, domains,
198 """ Evaluates sequences of domains and contexts, composing them into
199 a single context, domain or group_by sequence.
201 :param list contexts: list of contexts to merge together. Contexts are
202 evaluated in sequence, all previous contexts
203 are part of their own evaluation context
204 (starting at the session context).
205 :param list domains: list of domains to merge together. Domains are
206 evaluated in sequence and appended to one another
207 (implicit AND), their evaluation domain is the
208 result of merging all contexts.
209 :param list group_by_seq: list of domains (which may be in a different
210 order than the ``contexts`` parameter),
211 evaluated in sequence, their ``'group_by'``
212 key is extracted if they have one.
217 the global context created by merging all of
221 the concatenation of all domains
224 a list of fields to group by, potentially empty (in which case
225 no group by should be performed)
227 context, domain = eval_context_and_domain(req.session,
228 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
229 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
231 group_by_sequence = []
232 for candidate in (group_by_seq or []):
233 ctx = req.session.eval_context(candidate, context)
234 group_by = ctx.get('group_by')
237 elif isinstance(group_by, basestring):
238 group_by_sequence.append(group_by)
240 group_by_sequence.extend(group_by)
245 'group_by': group_by_sequence
248 @openerpweb.jsonrequest
249 def save_session_action(self, req, the_action):
251 This method store an action object in the session object and returns an integer
252 identifying that action. The method get_session_action() can be used to get
255 :param the_action: The action to save in the session.
256 :type the_action: anything
257 :return: A key identifying the saved action.
260 saved_actions = cherrypy.session.get('saved_actions')
261 if not saved_actions:
262 saved_actions = {"next":0, "actions":{}}
263 cherrypy.session['saved_actions'] = saved_actions
264 # we don't allow more than 10 stored actions
265 if len(saved_actions["actions"]) >= 10:
266 del saved_actions["actions"][min(saved_actions["actions"].keys())]
267 key = saved_actions["next"]
268 saved_actions["actions"][key] = the_action
269 saved_actions["next"] = key + 1
272 @openerpweb.jsonrequest
273 def get_session_action(self, req, key):
275 Gets back a previously saved action. This method can return None if the action
276 was saved since too much time (this case should be handled in a smart way).
278 :param key: The key given by save_session_action()
280 :return: The saved action or None.
283 saved_actions = cherrypy.session.get('saved_actions')
284 if not saved_actions:
286 return saved_actions["actions"].get(key)
288 def eval_context_and_domain(session, context, domain=None):
289 e_context = session.eval_context(context)
290 # should we give the evaluated context as an evaluation context to the domain?
291 e_domain = session.eval_domain(domain or [])
293 return e_context, e_domain
295 def load_actions_from_ir_values(req, key, key2, models, meta, context):
296 Values = req.session.model('ir.values')
297 actions = Values.get(key, key2, models, meta, context)
299 return [(id, name, clean_action(action, req.session))
300 for id, name, action in actions]
302 def clean_action(action, session):
303 if action['type'] != 'ir.actions.act_window':
305 # values come from the server, we can just eval them
306 if isinstance(action.get('context', None), basestring):
307 action['context'] = eval(
309 session.evaluation_context()) or {}
311 if isinstance(action.get('domain', None), basestring):
312 action['domain'] = eval(
314 session.evaluation_context(
315 action['context'])) or []
316 if 'flags' not in action:
317 # Set empty flags dictionary for web client.
318 action['flags'] = dict()
319 return fix_view_modes(action)
321 def generate_views(action):
323 While the server generates a sequence called "views" computing dependencies
324 between a bunch of stuff for views coming directly from the database
325 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
326 to return custom view dictionaries generated on the fly.
328 In that case, there is no ``views`` key available on the action.
330 Since the web client relies on ``action['views']``, generate it here from
331 ``view_mode`` and ``view_id``.
333 Currently handles two different cases:
335 * no view_id, multiple view_mode
336 * single view_id, single view_mode
338 :param dict action: action descriptor dictionary to generate a views key for
340 view_id = action.get('view_id', False)
341 if isinstance(view_id, (list, tuple)):
344 # providing at least one view mode is a requirement, not an option
345 view_modes = action['view_mode'].split(',')
347 if len(view_modes) > 1:
349 raise ValueError('Non-db action dictionaries should provide '
350 'either multiple view modes or a single view '
351 'mode and an optional view id.\n\n Got view '
352 'modes %r and view id %r for action %r' % (
353 view_modes, view_id, action))
354 action['views'] = [(False, mode) for mode in view_modes]
356 action['views'] = [(view_id, view_modes[0])]
359 def fix_view_modes(action):
360 """ For historical reasons, OpenERP has weird dealings in relation to
361 view_mode and the view_type attribute (on window actions):
363 * one of the view modes is ``tree``, which stands for both list views
365 * the choice is made by checking ``view_type``, which is either
366 ``form`` for a list view or ``tree`` for an actual tree view
368 This methods simply folds the view_type into view_mode by adding a
369 new view mode ``list`` which is the result of the ``tree`` view_mode
370 in conjunction with the ``form`` view_type.
372 TODO: this should go into the doc, some kind of "peculiarities" section
374 :param dict action: an action descriptor
375 :returns: nothing, the action is modified in place
377 if 'views' not in action:
378 generate_views(action)
380 if action.pop('view_type') != 'form':
384 [id, mode if mode != 'tree' else 'list']
385 for id, mode in action['views']
390 class Menu(openerpweb.Controller):
391 _cp_path = "/base/menu"
393 @openerpweb.jsonrequest
395 return {'data': self.do_load(req)}
397 def do_load(self, req):
398 """ Loads all menu items (all applications and their sub-menus).
400 :param req: A request object, with an OpenERP session attribute
401 :type req: < session -> OpenERPSession >
402 :return: the menu root
403 :rtype: dict('children': menu_nodes)
405 Menus = req.session.model('ir.ui.menu')
406 # menus are loaded fully unlike a regular tree view, cause there are
407 # less than 512 items
408 context = req.session.eval_context(req.context)
409 menu_ids = Menus.search([], 0, False, False, context)
410 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
411 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
412 menu_items.append(menu_root)
414 # make a tree using parent_id
415 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
416 for menu_item in menu_items:
417 if menu_item['parent_id']:
418 parent = menu_item['parent_id'][0]
421 if parent in menu_items_map:
422 menu_items_map[parent].setdefault(
423 'children', []).append(menu_item)
425 # sort by sequence a tree using parent_id
426 for menu_item in menu_items:
427 menu_item.setdefault('children', []).sort(
428 key=lambda x:x["sequence"])
432 @openerpweb.jsonrequest
433 def action(self, req, menu_id):
434 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
435 [('ir.ui.menu', menu_id)], False,
436 req.session.eval_context(req.context))
437 return {"action": actions}
439 class DataSet(openerpweb.Controller):
440 _cp_path = "/base/dataset"
442 @openerpweb.jsonrequest
443 def fields(self, req, model):
444 return {'fields': req.session.model(model).fields_get(False,
445 req.session.eval_context(req.context))}
447 @openerpweb.jsonrequest
448 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None):
449 return self.do_search_read(request, model, fields, offset, limit, domain, sort)
450 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None
452 """ Performs a search() followed by a read() (if needed) using the
453 provided search criteria
455 :param request: a JSON-RPC request object
456 :type request: openerpweb.JsonRequest
457 :param str model: the name of the model to search on
458 :param fields: a list of the fields to return in the result records
460 :param int offset: from which index should the results start being returned
461 :param int limit: the maximum number of records to return
462 :param list domain: the search domain for the query
463 :param list sort: sorting directives
464 :returns: A structure (dict) with two keys: ids (all the ids matching
465 the (domain, context) pair) and records (paginated records
466 matching fields selection set)
469 Model = request.session.model(model)
470 context, domain = eval_context_and_domain(
471 request.session, request.context, domain)
473 ids = Model.search(domain, 0, False, sort or False, context)
474 # need to fill the dataset with all ids for the (domain, context) pair,
475 # so search un-paginated and paginate manually before reading
476 paginated_ids = ids[offset:(offset + limit if limit else None)]
477 if fields and fields == ['id']:
478 # shortcut read if we only want the ids
481 'records': map(lambda id: {'id': id}, paginated_ids)
484 records = Model.read(paginated_ids, fields or False, context)
485 records.sort(key=lambda obj: ids.index(obj['id']))
492 @openerpweb.jsonrequest
493 def get(self, request, model, ids, fields=False):
494 return self.do_get(request, model, ids, fields)
495 def do_get(self, request, model, ids, fields=False):
496 """ Fetches and returns the records of the model ``model`` whose ids
499 The results are in the same order as the inputs, but elements may be
500 missing (if there is no record left for the id)
502 :param request: the JSON-RPC2 request object
503 :type request: openerpweb.JsonRequest
504 :param model: the model to read from
506 :param ids: a list of identifiers
508 :param fields: a list of fields to fetch, ``False`` or empty to fetch
509 all fields in the model
510 :type fields: list | False
511 :returns: a list of records, in the same order as the list of ids
514 Model = request.session.model(model)
515 records = Model.read(ids, fields, request.session.eval_context(request.context))
517 record_map = dict((record['id'], record) for record in records)
519 return [record_map[id] for id in ids if record_map.get(id)]
521 @openerpweb.jsonrequest
522 def load(self, req, model, id, fields):
523 m = req.session.model(model)
525 r = m.read([id], False, req.session.eval_context(req.context))
528 return {'value': value}
530 @openerpweb.jsonrequest
531 def create(self, req, model, data):
532 m = req.session.model(model)
533 r = m.create(data, req.session.eval_context(req.context))
536 @openerpweb.jsonrequest
537 def save(self, req, model, id, data):
538 m = req.session.model(model)
539 r = m.write([id], data, req.session.eval_context(req.context))
542 @openerpweb.jsonrequest
543 def unlink(self, request, model, ids=()):
544 Model = request.session.model(model)
545 return Model.unlink(ids, request.session.eval_context(request.context))
547 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
548 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
549 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
550 c, d = eval_context_and_domain(req.session, context, domain)
551 if domain_id and len(args) - 1 >= domain_id:
553 if context_id and len(args) - 1 >= context_id:
556 return getattr(req.session.model(model), method)(*args)
558 @openerpweb.jsonrequest
559 def call(self, req, model, method, args, domain_id=None, context_id=None):
560 return {'result': self.call_common(req, model, method, args, domain_id, context_id)}
562 @openerpweb.jsonrequest
563 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
564 action = self.call_common(req, model, method, args, domain_id, context_id)
565 if isinstance(action, dict) and action.get('type') != '':
566 return {'result': clean_action(action, req.session)}
567 return {'result': False}
569 @openerpweb.jsonrequest
570 def exec_workflow(self, req, model, id, signal):
571 r = req.session.exec_workflow(model, id, signal)
574 @openerpweb.jsonrequest
575 def default_get(self, req, model, fields):
576 m = req.session.model(model)
577 r = m.default_get(fields, req.session.eval_context(req.context))
580 class DataGroup(openerpweb.Controller):
581 _cp_path = "/base/group"
582 @openerpweb.jsonrequest
583 def read(self, request, model, group_by_fields, domain=None):
584 Model = request.session.model(model)
585 context, domain = eval_context_and_domain(request.session, request.context, domain)
587 return Model.read_group(
588 domain or [], False, group_by_fields, 0, False,
589 dict(context, group_by=group_by_fields))
591 class View(openerpweb.Controller):
592 _cp_path = "/base/view"
594 def fields_view_get(self, request, model, view_id, view_type,
595 transform=True, toolbar=False, submenu=False):
596 Model = request.session.model(model)
597 context = request.session.eval_context(request.context)
598 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
599 # todo fme?: check that we should pass the evaluated context here
600 self.process_view(request.session, fvg, context, transform)
603 def process_view(self, session, fvg, context, transform):
604 # depending on how it feels, xmlrpclib.ServerProxy can translate
605 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
606 # enjoy unicode strings which can not be trivially converted to
607 # strings, and it blows up during parsing.
609 # So ensure we fix this retardation by converting view xml back to
611 if isinstance(fvg['arch'], unicode):
612 arch = fvg['arch'].encode('utf-8')
617 evaluation_context = session.evaluation_context(context or {})
618 xml = self.transform_view(arch, session, evaluation_context)
620 xml = ElementTree.fromstring(arch)
621 fvg['arch'] = Xml2Json.convert_element(xml)
622 for field in fvg['fields'].values():
623 if field.has_key('views') and field['views']:
624 for view in field["views"].values():
625 self.process_view(session, view, None, transform)
627 @openerpweb.jsonrequest
628 def add_custom(self, request, view_id, arch):
629 CustomView = request.session.model('ir.ui.view.custom')
631 'user_id': request.session._uid,
634 }, request.session.eval_context(request.context))
635 return {'result': True}
637 @openerpweb.jsonrequest
638 def undo_custom(self, request, view_id, reset=False):
639 CustomView = request.session.model('ir.ui.view.custom')
640 context = request.session.eval_context(request.context)
641 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
642 0, False, False, context)
645 CustomView.unlink(vcustom, context)
647 CustomView.unlink([vcustom[0]], context)
648 return {'result': True}
649 return {'result': False}
651 def transform_view(self, view_string, session, context=None):
652 # transform nodes on the fly via iterparse, instead of
653 # doing it statically on the parsing result
654 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
656 for event, elem in parser:
660 self.parse_domains_and_contexts(elem, session)
663 def parse_domain(self, elem, attr_name, session):
664 """ Parses an attribute of the provided name as a domain, transforms it
665 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
667 :param elem: the node being parsed
668 :type param: xml.etree.ElementTree.Element
669 :param str attr_name: the name of the attribute which should be parsed
670 :param session: Current OpenERP session
671 :type session: openerpweb.openerpweb.OpenERPSession
673 domain = elem.get(attr_name, '').strip()
678 openerpweb.ast.literal_eval(
683 openerpweb.nonliterals.Domain(session, domain))
685 def parse_domains_and_contexts(self, elem, session):
686 """ Converts domains and contexts from the view into Python objects,
687 either literals if they can be parsed by literal_eval or a special
688 placeholder object if the domain or context refers to free variables.
690 :param elem: the current node being parsed
691 :type param: xml.etree.ElementTree.Element
692 :param session: OpenERP session object, used to store and retrieve
694 :type session: openerpweb.openerpweb.OpenERPSession
696 self.parse_domain(elem, 'domain', session)
697 self.parse_domain(elem, 'filter_domain', session)
698 for el in ['context', 'default_get']:
699 context_string = elem.get(el, '').strip()
703 openerpweb.ast.literal_eval(context_string))
706 openerpweb.nonliterals.Context(
707 session, context_string))
709 class FormView(View):
710 _cp_path = "/base/formview"
712 @openerpweb.jsonrequest
713 def load(self, req, model, view_id, toolbar=False):
714 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
715 return {'fields_view': fields_view}
717 class ListView(View):
718 _cp_path = "/base/listview"
720 @openerpweb.jsonrequest
721 def load(self, req, model, view_id, toolbar=False):
722 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
723 return {'fields_view': fields_view}
725 def process_colors(self, view, row, context):
726 colors = view['arch']['attrs'].get('colors')
733 for pair in colors.split(';')
734 if eval(pair.split(':')[1], dict(context, **row))
739 elif len(color) == 1:
743 class SearchView(View):
744 _cp_path = "/base/searchview"
746 @openerpweb.jsonrequest
747 def load(self, req, model, view_id):
748 fields_view = self.fields_view_get(req, model, view_id, 'search')
749 return {'fields_view': fields_view}
751 @openerpweb.jsonrequest
752 def fields_get(self, req, model):
753 Model = req.session.model(model)
754 fields = Model.fields_get(False, req.session.eval_context(req.context))
755 return {'fields': fields}
757 class Binary(openerpweb.Controller):
758 _cp_path = "/base/binary"
760 @openerpweb.httprequest
761 def image(self, request, session_id, model, id, field, **kw):
762 cherrypy.response.headers['Content-Type'] = 'image/png'
763 Model = request.session.model(model)
764 context = request.session.eval_context(request.context)
767 res = Model.default_get([field], context).get(field, '')
769 res = Model.read([int(id)], [field], context)[0].get(field, '')
770 return base64.decodestring(res)
771 except: # TODO: what's the exception here?
772 return self.placeholder()
773 def placeholder(self):
774 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
776 @openerpweb.httprequest
777 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
778 Model = request.session.model(model)
779 context = request.session.eval_context(request.context)
780 res = Model.read([int(id)], [field, fieldname], context)[0]
781 filecontent = res.get(field, '')
783 raise cherrypy.NotFound
785 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
786 filename = '%s_%s' % (model.replace('.', '_'), id)
788 filename = res.get(fieldname, '') or filename
789 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
790 return base64.decodestring(filecontent)
792 @openerpweb.httprequest
793 def upload(self, request, session_id, callback, ufile=None):
794 cherrypy.response.timeout = 500
796 for key, val in cherrypy.request.headers.iteritems():
797 headers[key.lower()] = val
798 size = int(headers.get('content-length', 0))
799 # TODO: might be useful to have a configuration flag for max-length file uploads
801 out = """<script language="javascript" type="text/javascript">
802 var win = window.top.window,
804 if (typeof(callback) === 'function') {
805 callback.apply(this, %s);
807 win.jQuery('#oe_notification', win.document).notify('create', {
808 title: "Ajax File Upload",
809 text: "Could not find callback"
813 data = ufile.file.read()
814 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
816 args = [False, e.message]
817 return out % (simplejson.dumps(callback), simplejson.dumps(args))
819 @openerpweb.httprequest
820 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
821 cherrypy.response.timeout = 500
822 context = request.session.eval_context(request.context)
823 Model = request.session.model('ir.attachment')
825 out = """<script language="javascript" type="text/javascript">
826 var win = window.top.window,
828 if (typeof(callback) === 'function') {
829 callback.call(this, %s);
832 attachment_id = Model.create({
833 'name': ufile.filename,
834 'datas': base64.encodestring(ufile.file.read()),
839 'filename': ufile.filename,
843 args = { 'error': e.message }
844 return out % (simplejson.dumps(callback), simplejson.dumps(args))
846 class Action(openerpweb.Controller):
847 _cp_path = "/base/action"
849 @openerpweb.jsonrequest
850 def load(self, req, action_id):
851 Actions = req.session.model('ir.actions.actions')
853 context = req.session.eval_context(req.context)
854 action_type = Actions.read([action_id], ['type'], context)
856 action = req.session.model(action_type[0]['type']).read([action_id], False,
859 value = clean_action(action[0], req.session)
860 return {'result': value}
862 @openerpweb.jsonrequest
863 def run(self, req, action_id):
864 return clean_action(req.session.model('ir.actions.server').run(
865 [action_id], req.session.eval_context(req.context)), req.session)