1 # -*- coding: utf-8 -*-
15 from xml.etree import ElementTree
16 from cStringIO import StringIO
18 import babel.messages.pofile
21 openerpweb = web.common.http
23 #----------------------------------------------------------
24 # OpenERP Web web Controllers
25 #----------------------------------------------------------
28 def concat_xml(file_list):
29 """Concatenate xml files
30 return (concat,timestamp)
31 concat: concatenation of file content
32 timestamp: max(os.path.getmtime of file_list)
36 for fname in file_list:
37 ftime = os.path.getmtime(fname)
38 if ftime > files_timestamp:
39 files_timestamp = ftime
41 xml = ElementTree.parse(fname).getroot()
44 root = ElementTree.Element(xml.tag)
45 #elif root.tag != xml.tag:
46 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
48 for child in xml.getchildren():
50 return ElementTree.tostring(root, 'utf-8'), files_timestamp
53 def concat_files(file_list, reader=None):
54 """ Concatenate file content
55 return (concat,timestamp)
56 concat: concatenation of file content, read by `reader`
57 timestamp: max(os.path.getmtime of file_list)
66 for fname in file_list:
67 ftime = os.path.getmtime(fname)
68 if ftime > files_timestamp:
69 files_timestamp = ftime
71 files_content.append(reader(fname))
72 files_concat = "".join(files_content)
73 return files_concat,files_timestamp
75 html_template = """<!DOCTYPE html>
76 <html style="height: 100%%">
78 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
79 <title>OpenERP</title>
80 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
83 <script type="text/javascript">
85 var s = new openerp.init(%(modules)s);
90 <body id="oe" class="openerp"></body>
94 class WebClient(openerpweb.Controller):
95 _cp_path = "/web/webclient"
97 def server_wide_modules(self, req):
98 addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest]
101 def manifest_glob(self, req, addons, key):
103 addons = self.server_wide_modules(req)
105 addons = addons.split(',')
107 manifest = openerpweb.addons_manifest.get(addon, None)
110 # ensure does not ends with /
111 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
112 globlist = manifest.get(key, [])
113 for pattern in globlist:
114 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
115 yield path, path[len(addons_path):]
117 def manifest_list(self, req, mods, extension):
119 path = '/web/webclient/' + extension
121 path += '?mods=' + mods
123 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in self.manifest_glob(req, mods, extension)]
125 @openerpweb.jsonrequest
126 def csslist(self, req, mods=None):
127 return self.manifest_list(req, mods, 'css')
129 @openerpweb.jsonrequest
130 def jslist(self, req, mods=None):
131 return self.manifest_list(req, mods, 'js')
133 @openerpweb.jsonrequest
134 def qweblist(self, req, mods=None):
135 return self.manifest_list(req, mods, 'qweb')
137 @openerpweb.httprequest
138 def css(self, req, mods=None):
140 files = list(self.manifest_glob(req, mods, 'css'))
141 file_map = dict(files)
143 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
144 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://)""", re.U)
148 """read the a css file and absolutify all relative uris"""
152 web_path = file_map[f]
153 web_dir = os.path.dirname(web_path)
157 r"""@import \1%s/""" % (web_dir,),
163 r"""url(\1%s/""" % (web_dir,),
168 content,timestamp = concat_files((f[0] for f in files), reader)
169 # TODO use timestamp to set Last mofified date and E-tag
170 return req.make_response(content, [('Content-Type', 'text/css')])
172 @openerpweb.httprequest
173 def js(self, req, mods=None):
174 files = [f[0] for f in self.manifest_glob(req, mods, 'js')]
175 content,timestamp = concat_files(files)
176 # TODO use timestamp to set Last mofified date and E-tag
177 return req.make_response(content, [('Content-Type', 'application/javascript')])
179 @openerpweb.httprequest
180 def qweb(self, req, mods=None):
181 files = [f[0] for f in self.manifest_glob(req, mods, 'qweb')]
182 content,timestamp = concat_xml(files)
183 # TODO use timestamp to set Last mofified date and E-tag
184 return req.make_response(content, [('Content-Type', 'text/xml')])
187 @openerpweb.httprequest
188 def home(self, req, s_action=None, **kw):
189 js = "\n ".join('<script type="text/javascript" src="%s"></script>'%i for i in self.manifest_list(req, None, 'js'))
190 css = "\n ".join('<link rel="stylesheet" href="%s">'%i for i in self.manifest_list(req, None, 'css'))
192 r = html_template % {
195 'modules': simplejson.dumps(self.server_wide_modules(req)),
196 'init': 'new s.web.WebClient("oe").start();',
200 @openerpweb.jsonrequest
201 def translations(self, req, mods, lang):
202 lang_model = req.session.model('res.lang')
203 ids = lang_model.search([("code", "=", lang)])
205 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
206 "grouping", "decimal_point", "thousands_sep"])
210 if lang.count("_") > 0:
214 langs = lang.split(separator)
215 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
218 for addon_name in mods:
219 transl = {"messages":[]}
220 transs[addon_name] = transl
222 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
223 f_name = os.path.join(addons_path, addon_name, "po", l + ".po")
224 if not os.path.exists(f_name):
227 with open(f_name) as t_file:
228 po = babel.messages.pofile.read_po(t_file)
232 if x.id and x.string:
233 transl["messages"].append({'id': x.id, 'string': x.string})
234 return {"modules": transs,
235 "lang_parameters": lang_obj}
237 @openerpweb.jsonrequest
238 def version_info(self, req):
240 "version": web.common.release.version
243 class Database(openerpweb.Controller):
244 _cp_path = "/web/database"
246 @openerpweb.jsonrequest
247 def get_list(self, req):
248 proxy = req.session.proxy("db")
250 h = req.httprequest.headers['Host'].split(':')[0]
252 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
253 dbs = [i for i in dbs if re.match(r, i)]
254 return {"db_list": dbs}
256 @openerpweb.jsonrequest
257 def progress(self, req, password, id):
258 return req.session.proxy('db').get_progress(password, id)
260 @openerpweb.jsonrequest
261 def create(self, req, fields):
263 params = dict(map(operator.itemgetter('name', 'value'), fields))
265 params['super_admin_pwd'],
267 bool(params.get('demo_data')),
269 params['create_admin_pwd']
273 return req.session.proxy("db").create(*create_attrs)
274 except xmlrpclib.Fault, e:
275 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
276 return {'error': e.faultCode, 'title': 'Create Database'}
277 return {'error': 'Could not create database !', 'title': 'Create Database'}
279 @openerpweb.jsonrequest
280 def drop(self, req, fields):
281 password, db = operator.itemgetter(
282 'drop_pwd', 'drop_db')(
283 dict(map(operator.itemgetter('name', 'value'), fields)))
286 return req.session.proxy("db").drop(password, db)
287 except xmlrpclib.Fault, e:
288 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
289 return {'error': e.faultCode, 'title': 'Drop Database'}
290 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
292 @openerpweb.httprequest
293 def backup(self, req, backup_db, backup_pwd, token):
295 db_dump = base64.b64decode(
296 req.session.proxy("db").dump(backup_pwd, backup_db))
297 return req.make_response(db_dump,
298 [('Content-Type', 'application/octet-stream; charset=binary'),
299 ('Content-Disposition', 'attachment; filename="' + backup_db + '.dump"')],
300 {'fileToken': int(token)}
302 except xmlrpclib.Fault, e:
303 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
304 return 'Backup Database|' + e.faultCode
305 return 'Backup Database|Could not generate database backup'
307 @openerpweb.httprequest
308 def restore(self, req, db_file, restore_pwd, new_db):
310 data = base64.b64encode(db_file.read())
311 req.session.proxy("db").restore(restore_pwd, new_db, data)
313 except xmlrpclib.Fault, e:
314 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
315 raise Exception("AccessDenied")
317 @openerpweb.jsonrequest
318 def change_password(self, req, fields):
319 old_password, new_password = operator.itemgetter(
320 'old_pwd', 'new_pwd')(
321 dict(map(operator.itemgetter('name', 'value'), fields)))
323 return req.session.proxy("db").change_admin_password(old_password, new_password)
324 except xmlrpclib.Fault, e:
325 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
326 return {'error': e.faultCode, 'title': 'Change Password'}
327 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
329 class Session(openerpweb.Controller):
330 _cp_path = "/web/session"
332 @openerpweb.jsonrequest
333 def login(self, req, db, login, password):
334 req.session.login(db, login, password)
335 ctx = req.session.get_context() if req.session._uid else {}
338 "session_id": req.session_id,
339 "uid": req.session._uid,
341 "db": req.session._db
344 @openerpweb.jsonrequest
345 def get_session_info(self, req):
346 req.session.assert_valid(force=True)
348 "uid": req.session._uid,
349 "context": req.session.get_context() if req.session._uid else False,
350 "db": req.session._db
353 @openerpweb.jsonrequest
354 def change_password (self,req,fields):
355 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
356 dict(map(operator.itemgetter('name', 'value'), fields)))
357 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
358 return {'error':'All passwords have to be filled.','title': 'Change Password'}
359 if new_password != confirm_password:
360 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
362 if req.session.model('res.users').change_password(
363 old_password, new_password):
364 return {'new_password':new_password}
366 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
367 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
369 @openerpweb.jsonrequest
370 def sc_list(self, req):
371 return req.session.model('ir.ui.view_sc').get_sc(
372 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
374 @openerpweb.jsonrequest
375 def get_lang_list(self, req):
378 'lang_list': (req.session.proxy("db").list_lang() or []),
382 return {"error": e, "title": "Languages"}
384 @openerpweb.jsonrequest
385 def modules(self, req):
386 # Compute available candidates module
387 loadable = openerpweb.addons_manifest.iterkeys()
388 loaded = req.config.server_wide_modules
389 candidates = [mod for mod in loadable if mod not in loaded]
391 # Compute active true modules that might be on the web side only
392 active = set(name for name in candidates
393 if openerpweb.addons_manifest[name].get('active'))
395 # Retrieve database installed modules
396 Modules = req.session.model('ir.module.module')
397 installed = set(module['name'] for module in Modules.search_read(
398 [('state','=','installed'), ('name','in', candidates)], ['name']))
401 return list(active | installed)
403 @openerpweb.jsonrequest
404 def eval_domain_and_context(self, req, contexts, domains,
406 """ Evaluates sequences of domains and contexts, composing them into
407 a single context, domain or group_by sequence.
409 :param list contexts: list of contexts to merge together. Contexts are
410 evaluated in sequence, all previous contexts
411 are part of their own evaluation context
412 (starting at the session context).
413 :param list domains: list of domains to merge together. Domains are
414 evaluated in sequence and appended to one another
415 (implicit AND), their evaluation domain is the
416 result of merging all contexts.
417 :param list group_by_seq: list of domains (which may be in a different
418 order than the ``contexts`` parameter),
419 evaluated in sequence, their ``'group_by'``
420 key is extracted if they have one.
425 the global context created by merging all of
429 the concatenation of all domains
432 a list of fields to group by, potentially empty (in which case
433 no group by should be performed)
435 context, domain = eval_context_and_domain(req.session,
436 web.common.nonliterals.CompoundContext(*(contexts or [])),
437 web.common.nonliterals.CompoundDomain(*(domains or [])))
439 group_by_sequence = []
440 for candidate in (group_by_seq or []):
441 ctx = req.session.eval_context(candidate, context)
442 group_by = ctx.get('group_by')
445 elif isinstance(group_by, basestring):
446 group_by_sequence.append(group_by)
448 group_by_sequence.extend(group_by)
453 'group_by': group_by_sequence
456 @openerpweb.jsonrequest
457 def save_session_action(self, req, the_action):
459 This method store an action object in the session object and returns an integer
460 identifying that action. The method get_session_action() can be used to get
463 :param the_action: The action to save in the session.
464 :type the_action: anything
465 :return: A key identifying the saved action.
468 saved_actions = req.httpsession.get('saved_actions')
469 if not saved_actions:
470 saved_actions = {"next":0, "actions":{}}
471 req.httpsession['saved_actions'] = saved_actions
472 # we don't allow more than 10 stored actions
473 if len(saved_actions["actions"]) >= 10:
474 del saved_actions["actions"][min(saved_actions["actions"].keys())]
475 key = saved_actions["next"]
476 saved_actions["actions"][key] = the_action
477 saved_actions["next"] = key + 1
480 @openerpweb.jsonrequest
481 def get_session_action(self, req, key):
483 Gets back a previously saved action. This method can return None if the action
484 was saved since too much time (this case should be handled in a smart way).
486 :param key: The key given by save_session_action()
488 :return: The saved action or None.
491 saved_actions = req.httpsession.get('saved_actions')
492 if not saved_actions:
494 return saved_actions["actions"].get(key)
496 @openerpweb.jsonrequest
497 def check(self, req):
498 req.session.assert_valid()
501 def eval_context_and_domain(session, context, domain=None):
502 e_context = session.eval_context(context)
503 # should we give the evaluated context as an evaluation context to the domain?
504 e_domain = session.eval_domain(domain or [])
506 return e_context, e_domain
508 def load_actions_from_ir_values(req, key, key2, models, meta):
509 context = req.session.eval_context(req.context)
510 Values = req.session.model('ir.values')
511 actions = Values.get(key, key2, models, meta, context)
513 return [(id, name, clean_action(req, action))
514 for id, name, action in actions]
516 def clean_action(req, action, do_not_eval=False):
517 action.setdefault('flags', {})
519 context = req.session.eval_context(req.context)
520 eval_ctx = req.session.evaluation_context(context)
523 # values come from the server, we can just eval them
524 if isinstance(action.get('context'), basestring):
525 action['context'] = eval( action['context'], eval_ctx ) or {}
527 if isinstance(action.get('domain'), basestring):
528 action['domain'] = eval( action['domain'], eval_ctx ) or []
530 if 'context' in action:
531 action['context'] = parse_context(action['context'], req.session)
532 if 'domain' in action:
533 action['domain'] = parse_domain(action['domain'], req.session)
535 if 'type' not in action:
536 action['type'] = 'ir.actions.act_window_close'
538 if action['type'] == 'ir.actions.act_window':
539 return fix_view_modes(action)
542 # I think generate_views,fix_view_modes should go into js ActionManager
543 def generate_views(action):
545 While the server generates a sequence called "views" computing dependencies
546 between a bunch of stuff for views coming directly from the database
547 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
548 to return custom view dictionaries generated on the fly.
550 In that case, there is no ``views`` key available on the action.
552 Since the web client relies on ``action['views']``, generate it here from
553 ``view_mode`` and ``view_id``.
555 Currently handles two different cases:
557 * no view_id, multiple view_mode
558 * single view_id, single view_mode
560 :param dict action: action descriptor dictionary to generate a views key for
562 view_id = action.get('view_id', False)
563 if isinstance(view_id, (list, tuple)):
566 # providing at least one view mode is a requirement, not an option
567 view_modes = action['view_mode'].split(',')
569 if len(view_modes) > 1:
571 raise ValueError('Non-db action dictionaries should provide '
572 'either multiple view modes or a single view '
573 'mode and an optional view id.\n\n Got view '
574 'modes %r and view id %r for action %r' % (
575 view_modes, view_id, action))
576 action['views'] = [(False, mode) for mode in view_modes]
578 action['views'] = [(view_id, view_modes[0])]
580 def fix_view_modes(action):
581 """ For historical reasons, OpenERP has weird dealings in relation to
582 view_mode and the view_type attribute (on window actions):
584 * one of the view modes is ``tree``, which stands for both list views
586 * the choice is made by checking ``view_type``, which is either
587 ``form`` for a list view or ``tree`` for an actual tree view
589 This methods simply folds the view_type into view_mode by adding a
590 new view mode ``list`` which is the result of the ``tree`` view_mode
591 in conjunction with the ``form`` view_type.
593 TODO: this should go into the doc, some kind of "peculiarities" section
595 :param dict action: an action descriptor
596 :returns: nothing, the action is modified in place
598 if 'views' not in action:
599 generate_views(action)
601 if action.pop('view_type', 'form') != 'form':
605 [id, mode if mode != 'tree' else 'list']
606 for id, mode in action['views']
611 class Menu(openerpweb.Controller):
612 _cp_path = "/web/menu"
614 @openerpweb.jsonrequest
616 return {'data': self.do_load(req)}
618 def do_load(self, req):
619 """ Loads all menu items (all applications and their sub-menus).
621 :param req: A request object, with an OpenERP session attribute
622 :type req: < session -> OpenERPSession >
623 :return: the menu root
624 :rtype: dict('children': menu_nodes)
626 Menus = req.session.model('ir.ui.menu')
627 # menus are loaded fully unlike a regular tree view, cause there are
628 # less than 512 items
629 context = req.session.eval_context(req.context)
630 menu_ids = Menus.search([], 0, False, False, context)
631 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
632 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
633 menu_items.append(menu_root)
635 # make a tree using parent_id
636 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
637 for menu_item in menu_items:
638 if menu_item['parent_id']:
639 parent = menu_item['parent_id'][0]
642 if parent in menu_items_map:
643 menu_items_map[parent].setdefault(
644 'children', []).append(menu_item)
646 # sort by sequence a tree using parent_id
647 for menu_item in menu_items:
648 menu_item.setdefault('children', []).sort(
649 key=lambda x:x["sequence"])
653 @openerpweb.jsonrequest
654 def action(self, req, menu_id):
655 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
656 [('ir.ui.menu', menu_id)], False)
657 return {"action": actions}
659 class DataSet(openerpweb.Controller):
660 _cp_path = "/web/dataset"
662 @openerpweb.jsonrequest
663 def fields(self, req, model):
664 return {'fields': req.session.model(model).fields_get(False,
665 req.session.eval_context(req.context))}
667 @openerpweb.jsonrequest
668 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
669 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
670 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
672 """ Performs a search() followed by a read() (if needed) using the
673 provided search criteria
675 :param req: a JSON-RPC request object
676 :type req: openerpweb.JsonRequest
677 :param str model: the name of the model to search on
678 :param fields: a list of the fields to return in the result records
680 :param int offset: from which index should the results start being returned
681 :param int limit: the maximum number of records to return
682 :param list domain: the search domain for the query
683 :param list sort: sorting directives
684 :returns: A structure (dict) with two keys: ids (all the ids matching
685 the (domain, context) pair) and records (paginated records
686 matching fields selection set)
689 Model = req.session.model(model)
691 context, domain = eval_context_and_domain(
692 req.session, req.context, domain)
694 ids = Model.search(domain, 0, False, sort or False, context)
695 # need to fill the dataset with all ids for the (domain, context) pair,
696 # so search un-paginated and paginate manually before reading
697 paginated_ids = ids[offset:(offset + limit if limit else None)]
698 if fields and fields == ['id']:
699 # shortcut read if we only want the ids
702 'records': map(lambda id: {'id': id}, paginated_ids)
705 records = Model.read(paginated_ids, fields or False, context)
706 records.sort(key=lambda obj: ids.index(obj['id']))
713 @openerpweb.jsonrequest
714 def read(self, req, model, ids, fields=False):
715 return self.do_search_read(req, model, ids, fields)
717 @openerpweb.jsonrequest
718 def get(self, req, model, ids, fields=False):
719 return self.do_get(req, model, ids, fields)
721 def do_get(self, req, model, ids, fields=False):
722 """ Fetches and returns the records of the model ``model`` whose ids
725 The results are in the same order as the inputs, but elements may be
726 missing (if there is no record left for the id)
728 :param req: the JSON-RPC2 request object
729 :type req: openerpweb.JsonRequest
730 :param model: the model to read from
732 :param ids: a list of identifiers
734 :param fields: a list of fields to fetch, ``False`` or empty to fetch
735 all fields in the model
736 :type fields: list | False
737 :returns: a list of records, in the same order as the list of ids
740 Model = req.session.model(model)
741 records = Model.read(ids, fields, req.session.eval_context(req.context))
743 record_map = dict((record['id'], record) for record in records)
745 return [record_map[id] for id in ids if record_map.get(id)]
747 @openerpweb.jsonrequest
748 def load(self, req, model, id, fields):
749 m = req.session.model(model)
751 r = m.read([id], False, req.session.eval_context(req.context))
754 return {'value': value}
756 @openerpweb.jsonrequest
757 def create(self, req, model, data):
758 m = req.session.model(model)
759 r = m.create(data, req.session.eval_context(req.context))
762 @openerpweb.jsonrequest
763 def save(self, req, model, id, data):
764 m = req.session.model(model)
765 r = m.write([id], data, req.session.eval_context(req.context))
768 @openerpweb.jsonrequest
769 def unlink(self, req, model, ids=()):
770 Model = req.session.model(model)
771 return Model.unlink(ids, req.session.eval_context(req.context))
773 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
774 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
775 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
776 c, d = eval_context_and_domain(req.session, context, domain)
777 if domain_id and len(args) - 1 >= domain_id:
779 if context_id and len(args) - 1 >= context_id:
782 for i in xrange(len(args)):
783 if isinstance(args[i], web.common.nonliterals.BaseContext):
784 args[i] = req.session.eval_context(args[i])
785 if isinstance(args[i], web.common.nonliterals.BaseDomain):
786 args[i] = req.session.eval_domain(args[i])
788 return getattr(req.session.model(model), method)(*args)
790 @openerpweb.jsonrequest
791 def call(self, req, model, method, args, domain_id=None, context_id=None):
792 return self.call_common(req, model, method, args, domain_id, context_id)
794 @openerpweb.jsonrequest
795 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
796 action = self.call_common(req, model, method, args, domain_id, context_id)
797 if isinstance(action, dict) and action.get('type') != '':
798 return {'result': clean_action(req, action)}
799 return {'result': False}
801 @openerpweb.jsonrequest
802 def exec_workflow(self, req, model, id, signal):
803 r = req.session.exec_workflow(model, id, signal)
806 @openerpweb.jsonrequest
807 def default_get(self, req, model, fields):
808 Model = req.session.model(model)
809 return Model.default_get(fields, req.session.eval_context(req.context))
811 @openerpweb.jsonrequest
812 def name_search(self, req, model, search_str, domain=[], context={}):
813 m = req.session.model(model)
814 r = m.name_search(search_str+'%', domain, '=ilike', context)
817 class DataGroup(openerpweb.Controller):
818 _cp_path = "/web/group"
819 @openerpweb.jsonrequest
820 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
821 Model = req.session.model(model)
822 context, domain = eval_context_and_domain(req.session, req.context, domain)
824 return Model.read_group(
825 domain or [], fields, group_by_fields, 0, False,
826 dict(context, group_by=group_by_fields), sort or False)
828 class View(openerpweb.Controller):
829 _cp_path = "/web/view"
831 def fields_view_get(self, req, model, view_id, view_type,
832 transform=True, toolbar=False, submenu=False):
833 Model = req.session.model(model)
834 context = req.session.eval_context(req.context)
835 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
836 # todo fme?: check that we should pass the evaluated context here
837 self.process_view(req.session, fvg, context, transform)
838 if toolbar and transform:
839 self.process_toolbar(req, fvg['toolbar'])
842 def process_view(self, session, fvg, context, transform):
843 # depending on how it feels, xmlrpclib.ServerProxy can translate
844 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
845 # enjoy unicode strings which can not be trivially converted to
846 # strings, and it blows up during parsing.
848 # So ensure we fix this retardation by converting view xml back to
850 if isinstance(fvg['arch'], unicode):
851 arch = fvg['arch'].encode('utf-8')
856 evaluation_context = session.evaluation_context(context or {})
857 xml = self.transform_view(arch, session, evaluation_context)
859 xml = ElementTree.fromstring(arch)
860 fvg['arch'] = web.common.xml2json.Xml2Json.convert_element(xml)
862 for field in fvg['fields'].itervalues():
863 if field.get('views'):
864 for view in field["views"].itervalues():
865 self.process_view(session, view, None, transform)
866 if field.get('domain'):
867 field["domain"] = parse_domain(field["domain"], session)
868 if field.get('context'):
869 field["context"] = parse_context(field["context"], session)
871 def process_toolbar(self, req, toolbar):
873 The toolbar is a mapping of section_key: [action_descriptor]
875 We need to clean all those actions in order to ensure correct
878 for actions in toolbar.itervalues():
879 for action in actions:
880 if 'context' in action:
881 action['context'] = parse_context(
882 action['context'], req.session)
883 if 'domain' in action:
884 action['domain'] = parse_domain(
885 action['domain'], req.session)
887 @openerpweb.jsonrequest
888 def add_custom(self, req, view_id, arch):
889 CustomView = req.session.model('ir.ui.view.custom')
891 'user_id': req.session._uid,
894 }, req.session.eval_context(req.context))
895 return {'result': True}
897 @openerpweb.jsonrequest
898 def undo_custom(self, req, view_id, reset=False):
899 CustomView = req.session.model('ir.ui.view.custom')
900 context = req.session.eval_context(req.context)
901 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
902 0, False, False, context)
905 CustomView.unlink(vcustom, context)
907 CustomView.unlink([vcustom[0]], context)
908 return {'result': True}
909 return {'result': False}
911 def transform_view(self, view_string, session, context=None):
912 # transform nodes on the fly via iterparse, instead of
913 # doing it statically on the parsing result
914 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
916 for event, elem in parser:
920 self.parse_domains_and_contexts(elem, session)
923 def parse_domains_and_contexts(self, elem, session):
924 """ Converts domains and contexts from the view into Python objects,
925 either literals if they can be parsed by literal_eval or a special
926 placeholder object if the domain or context refers to free variables.
928 :param elem: the current node being parsed
929 :type param: xml.etree.ElementTree.Element
930 :param session: OpenERP session object, used to store and retrieve
932 :type session: openerpweb.openerpweb.OpenERPSession
934 for el in ['domain', 'filter_domain']:
935 domain = elem.get(el, '').strip()
937 elem.set(el, parse_domain(domain, session))
938 elem.set(el + '_string', domain)
939 for el in ['context', 'default_get']:
940 context_string = elem.get(el, '').strip()
942 elem.set(el, parse_context(context_string, session))
943 elem.set(el + '_string', context_string)
945 @openerpweb.jsonrequest
946 def load(self, req, model, view_id, view_type, toolbar=False):
947 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
949 def parse_domain(domain, session):
950 """ Parses an arbitrary string containing a domain, transforms it
951 to either a literal domain or a :class:`web.common.nonliterals.Domain`
953 :param domain: the domain to parse, if the domain is not a string it
954 is assumed to be a literal domain and is returned as-is
955 :param session: Current OpenERP session
956 :type session: openerpweb.openerpweb.OpenERPSession
958 if not isinstance(domain, (str, unicode)):
961 return ast.literal_eval(domain)
964 return web.common.nonliterals.Domain(session, domain)
966 def parse_context(context, session):
967 """ Parses an arbitrary string containing a context, transforms it
968 to either a literal context or a :class:`web.common.nonliterals.Context`
970 :param context: the context to parse, if the context is not a string it
971 is assumed to be a literal domain and is returned as-is
972 :param session: Current OpenERP session
973 :type session: openerpweb.openerpweb.OpenERPSession
975 if not isinstance(context, (str, unicode)):
978 return ast.literal_eval(context)
980 return web.common.nonliterals.Context(session, context)
982 class ListView(View):
983 _cp_path = "/web/listview"
985 def process_colors(self, view, row, context):
986 colors = view['arch']['attrs'].get('colors')
993 for pair in colors.split(';')
994 if eval(pair.split(':')[1], dict(context, **row))
999 elif len(color) == 1:
1003 class TreeView(View):
1004 _cp_path = "/web/treeview"
1006 @openerpweb.jsonrequest
1007 def action(self, req, model, id):
1008 return load_actions_from_ir_values(
1009 req,'action', 'tree_but_open',[(model, id)],
1012 class SearchView(View):
1013 _cp_path = "/web/searchview"
1015 @openerpweb.jsonrequest
1016 def load(self, req, model, view_id):
1017 fields_view = self.fields_view_get(req, model, view_id, 'search')
1018 return {'fields_view': fields_view}
1020 @openerpweb.jsonrequest
1021 def fields_get(self, req, model):
1022 Model = req.session.model(model)
1023 fields = Model.fields_get(False, req.session.eval_context(req.context))
1024 for field in fields.values():
1025 # shouldn't convert the views too?
1026 if field.get('domain'):
1027 field["domain"] = parse_domain(field["domain"], req.session)
1028 if field.get('context'):
1029 field["context"] = parse_context(field["context"], req.session)
1030 return {'fields': fields}
1032 @openerpweb.jsonrequest
1033 def get_filters(self, req, model):
1034 Model = req.session.model("ir.filters")
1035 filters = Model.get_filters(model)
1036 for filter in filters:
1037 filter["context"] = req.session.eval_context(parse_context(filter["context"], req.session))
1038 filter["domain"] = req.session.eval_domain(parse_domain(filter["domain"], req.session))
1041 @openerpweb.jsonrequest
1042 def save_filter(self, req, model, name, context_to_save, domain):
1043 Model = req.session.model("ir.filters")
1044 ctx = web.common.nonliterals.CompoundContext(context_to_save)
1045 ctx.session = req.session
1046 ctx = ctx.evaluate()
1047 domain = web.common.nonliterals.CompoundDomain(domain)
1048 domain.session = req.session
1049 domain = domain.evaluate()
1050 uid = req.session._uid
1051 context = req.session.eval_context(req.context)
1052 to_return = Model.create_or_replace({"context": ctx,
1060 class Binary(openerpweb.Controller):
1061 _cp_path = "/web/binary"
1063 @openerpweb.httprequest
1064 def image(self, req, model, id, field, **kw):
1065 Model = req.session.model(model)
1066 context = req.session.eval_context(req.context)
1070 res = Model.default_get([field], context).get(field)
1072 res = Model.read([int(id)], [field], context)[0].get(field)
1073 image_data = base64.b64decode(res)
1074 except (TypeError, xmlrpclib.Fault):
1075 image_data = self.placeholder(req)
1076 return req.make_response(image_data, [
1077 ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
1078 def placeholder(self, req):
1079 addons_path = openerpweb.addons_manifest['web']['addons_path']
1080 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1082 @openerpweb.httprequest
1083 def saveas(self, req, model, id, field, fieldname, **kw):
1084 Model = req.session.model(model)
1085 context = req.session.eval_context(req.context)
1087 res = Model.read([int(id)], [field, fieldname], context)[0]
1089 res = Model.default_get([field, fieldname], context)
1090 filecontent = base64.b64decode(res.get(field, ''))
1092 return req.not_found()
1094 filename = '%s_%s' % (model.replace('.', '_'), id)
1096 filename = res.get(fieldname, '') or filename
1097 return req.make_response(filecontent,
1098 [('Content-Type', 'application/octet-stream'),
1099 ('Content-Disposition', 'attachment; filename=' + filename)])
1101 @openerpweb.httprequest
1102 def upload(self, req, callback, ufile):
1103 # TODO: might be useful to have a configuration flag for max-length file uploads
1105 out = """<script language="javascript" type="text/javascript">
1106 var win = window.top.window,
1108 if (typeof(callback) === 'function') {
1109 callback.apply(this, %s);
1111 win.jQuery('#oe_notification', win.document).notify('create', {
1112 title: "Ajax File Upload",
1113 text: "Could not find callback"
1118 args = [ufile.content_length, ufile.filename,
1119 ufile.content_type, base64.b64encode(data)]
1120 except Exception, e:
1121 args = [False, e.message]
1122 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1124 @openerpweb.httprequest
1125 def upload_attachment(self, req, callback, model, id, ufile):
1126 context = req.session.eval_context(req.context)
1127 Model = req.session.model('ir.attachment')
1129 out = """<script language="javascript" type="text/javascript">
1130 var win = window.top.window,
1132 if (typeof(callback) === 'function') {
1133 callback.call(this, %s);
1136 attachment_id = Model.create({
1137 'name': ufile.filename,
1138 'datas': base64.encodestring(ufile.read()),
1143 'filename': ufile.filename,
1146 except Exception, e:
1147 args = { 'error': e.message }
1148 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1150 class Action(openerpweb.Controller):
1151 _cp_path = "/web/action"
1153 @openerpweb.jsonrequest
1154 def load(self, req, action_id, do_not_eval=False):
1155 Actions = req.session.model('ir.actions.actions')
1157 context = req.session.eval_context(req.context)
1158 action_type = Actions.read([action_id], ['type'], context)
1161 if action_type[0]['type'] == 'ir.actions.report.xml':
1162 ctx.update({'bin_size': True})
1164 action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
1166 value = clean_action(req, action[0], do_not_eval)
1167 return {'result': value}
1169 @openerpweb.jsonrequest
1170 def run(self, req, action_id):
1171 return clean_action(req, req.session.model('ir.actions.server').run(
1172 [action_id], req.session.eval_context(req.context)))
1175 _cp_path = "/web/export"
1177 @openerpweb.jsonrequest
1178 def formats(self, req):
1179 """ Returns all valid export formats
1181 :returns: for each export format, a pair of identifier and printable name
1182 :rtype: [(str, str)]
1186 for path, controller in openerpweb.controllers_path.iteritems()
1187 if path.startswith(self._cp_path)
1188 if hasattr(controller, 'fmt')
1189 ], key=operator.itemgetter(1))
1191 def fields_get(self, req, model):
1192 Model = req.session.model(model)
1193 fields = Model.fields_get(False, req.session.eval_context(req.context))
1196 @openerpweb.jsonrequest
1197 def get_fields(self, req, model, prefix='', parent_name= '',
1198 import_compat=True, parent_field_type=None,
1201 if import_compat and parent_field_type == "many2one":
1204 fields = self.fields_get(req, model)
1207 fields.pop('id', None)
1209 fields['.id'] = fields.pop('id', {'string': 'ID'})
1211 fields_sequence = sorted(fields.iteritems(),
1212 key=lambda field: field[1].get('string', ''))
1215 for field_name, field in fields_sequence:
1216 if import_compat and (exclude and field_name in exclude):
1218 if import_compat and field.get('readonly'):
1219 # If none of the field's states unsets readonly, skip the field
1220 if all(dict(attrs).get('readonly', True)
1221 for attrs in field.get('states', {}).values()):
1224 id = prefix + (prefix and '/'or '') + field_name
1225 name = parent_name + (parent_name and '/' or '') + field['string']
1226 record = {'id': id, 'string': name,
1227 'value': id, 'children': False,
1228 'field_type': field.get('type'),
1229 'required': field.get('required'),
1230 'relation_field': field.get('relation_field')}
1231 records.append(record)
1233 if len(name.split('/')) < 3 and 'relation' in field:
1234 ref = field.pop('relation')
1235 record['value'] += '/id'
1236 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1238 if not import_compat or field['type'] == 'one2many':
1239 # m2m field in import_compat is childless
1240 record['children'] = True
1244 @openerpweb.jsonrequest
1245 def namelist(self,req, model, export_id):
1246 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1247 export = req.session.model("ir.exports").read([export_id])[0]
1248 export_fields_list = req.session.model("ir.exports.line").read(
1249 export['export_fields'])
1251 fields_data = self.fields_info(
1252 req, model, map(operator.itemgetter('name'), export_fields_list))
1255 {'name': field['name'], 'label': fields_data[field['name']]}
1256 for field in export_fields_list
1259 def fields_info(self, req, model, export_fields):
1261 fields = self.fields_get(req, model)
1263 # To make fields retrieval more efficient, fetch all sub-fields of a
1264 # given field at the same time. Because the order in the export list is
1265 # arbitrary, this requires ordering all sub-fields of a given field
1266 # together so they can be fetched at the same time
1268 # Works the following way:
1269 # * sort the list of fields to export, the default sorting order will
1270 # put the field itself (if present, for xmlid) and all of its
1271 # sub-fields right after it
1272 # * then, group on: the first field of the path (which is the same for
1273 # a field and for its subfields and the length of splitting on the
1274 # first '/', which basically means grouping the field on one side and
1275 # all of the subfields on the other. This way, we have the field (for
1276 # the xmlid) with length 1, and all of the subfields with the same
1277 # base but a length "flag" of 2
1278 # * if we have a normal field (length 1), just add it to the info
1279 # mapping (with its string) as-is
1280 # * otherwise, recursively call fields_info via graft_subfields.
1281 # all graft_subfields does is take the result of fields_info (on the
1282 # field's model) and prepend the current base (current field), which
1283 # rebuilds the whole sub-tree for the field
1285 # result: because we're not fetching the fields_get for half the
1286 # database models, fetching a namelist with a dozen fields (including
1287 # relational data) falls from ~6s to ~300ms (on the leads model).
1288 # export lists with no sub-fields (e.g. import_compatible lists with
1289 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1290 # there's a single fields_get to execute)
1291 for (base, length), subfields in itertools.groupby(
1292 sorted(export_fields),
1293 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1294 subfields = list(subfields)
1296 # subfields is a seq of $base/*rest, and not loaded yet
1297 info.update(self.graft_subfields(
1298 req, fields[base]['relation'], base, fields[base]['string'],
1302 info[base] = fields[base]['string']
1306 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1307 export_fields = [field.split('/', 1)[1] for field in fields]
1309 (prefix + '/' + k, prefix_string + '/' + v)
1310 for k, v in self.fields_info(req, model, export_fields).iteritems())
1312 #noinspection PyPropertyDefinition
1314 def content_type(self):
1315 """ Provides the format's content type """
1316 raise NotImplementedError()
1318 def filename(self, base):
1319 """ Creates a valid filename for the format (with extension) from the
1320 provided base name (exension-less)
1322 raise NotImplementedError()
1324 def from_data(self, fields, rows):
1325 """ Conversion method from OpenERP's export data to whatever the
1326 current export class outputs
1328 :params list fields: a list of fields to export
1329 :params list rows: a list of records to export
1333 raise NotImplementedError()
1335 @openerpweb.httprequest
1336 def index(self, req, data, token):
1337 model, fields, ids, domain, import_compat = \
1338 operator.itemgetter('model', 'fields', 'ids', 'domain',
1340 simplejson.loads(data))
1342 context = req.session.eval_context(req.context)
1343 Model = req.session.model(model)
1344 ids = ids or Model.search(domain, context=context)
1346 field_names = map(operator.itemgetter('name'), fields)
1347 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1350 columns_headers = field_names
1352 columns_headers = [val['label'].strip() for val in fields]
1355 return req.make_response(self.from_data(columns_headers, import_data),
1356 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1357 ('Content-Type', self.content_type)],
1358 cookies={'fileToken': int(token)})
1360 class CSVExport(Export):
1361 _cp_path = '/web/export/csv'
1362 fmt = ('csv', 'CSV')
1365 def content_type(self):
1366 return 'text/csv;charset=utf8'
1368 def filename(self, base):
1369 return base + '.csv'
1371 def from_data(self, fields, rows):
1373 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1375 writer.writerow(fields)
1380 if isinstance(d, basestring):
1381 d = d.replace('\n',' ').replace('\t',' ')
1383 d = d.encode('utf-8')
1386 if d is False: d = None
1388 writer.writerow(row)
1395 class ExcelExport(Export):
1396 _cp_path = '/web/export/xls'
1397 fmt = ('xls', 'Excel')
1400 def content_type(self):
1401 return 'application/vnd.ms-excel'
1403 def filename(self, base):
1404 return base + '.xls'
1406 def from_data(self, fields, rows):
1409 workbook = xlwt.Workbook()
1410 worksheet = workbook.add_sheet('Sheet 1')
1412 for i, fieldname in enumerate(fields):
1413 worksheet.write(0, i, str(fieldname))
1414 worksheet.col(i).width = 8000 # around 220 pixels
1416 style = xlwt.easyxf('align: wrap yes')
1418 for row_index, row in enumerate(rows):
1419 for cell_index, cell_value in enumerate(row):
1420 if isinstance(cell_value, basestring):
1421 cell_value = re.sub("\r", " ", cell_value)
1422 if cell_value is False: cell_value = None
1423 worksheet.write(row_index + 1, cell_index, cell_value, style)
1432 class Reports(View):
1433 _cp_path = "/web/report"
1434 POLLING_DELAY = 0.25
1436 'doc': 'application/vnd.ms-word',
1437 'html': 'text/html',
1438 'odt': 'application/vnd.oasis.opendocument.text',
1439 'pdf': 'application/pdf',
1440 'sxw': 'application/vnd.sun.xml.writer',
1441 'xls': 'application/vnd.ms-excel',
1444 @openerpweb.httprequest
1445 def index(self, req, action, token):
1446 action = simplejson.loads(action)
1448 report_srv = req.session.proxy("report")
1449 context = req.session.eval_context(
1450 web.common.nonliterals.CompoundContext(
1451 req.context or {}, action[ "context"]))
1454 report_ids = context["active_ids"]
1455 if 'report_type' in action:
1456 report_data['report_type'] = action['report_type']
1457 if 'datas' in action:
1458 if 'ids' in action['datas']:
1459 report_ids = action['datas'].pop('ids')
1460 report_data.update(action['datas'])
1462 report_id = report_srv.report(
1463 req.session._db, req.session._uid, req.session._password,
1464 action["report_name"], report_ids,
1465 report_data, context)
1467 report_struct = None
1469 report_struct = report_srv.report_get(
1470 req.session._db, req.session._uid, req.session._password, report_id)
1471 if report_struct["state"]:
1474 time.sleep(self.POLLING_DELAY)
1476 report = base64.b64decode(report_struct['result'])
1477 if report_struct.get('code') == 'zlib':
1478 report = zlib.decompress(report)
1479 report_mimetype = self.TYPES_MAPPING.get(
1480 report_struct['format'], 'octet-stream')
1481 return req.make_response(report,
1483 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
1484 ('Content-Type', report_mimetype),
1485 ('Content-Length', len(report))],
1486 cookies={'fileToken': int(token)})
1489 _cp_path = "/web/import"
1491 def fields_get(self, req, model):
1492 Model = req.session.model(model)
1493 fields = Model.fields_get(False, req.session.eval_context(req.context))
1496 @openerpweb.httprequest
1497 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1499 data = list(csv.reader(
1500 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1501 except csv.Error, e:
1503 return '<script>window.top.%s(%s);</script>' % (
1504 jsonp, simplejson.dumps({'error': {
1505 'message': 'Error parsing CSV file: %s' % e,
1506 # decodes each byte to a unicode character, which may or
1507 # may not be printable, but decoding will succeed.
1508 # Otherwise simplejson will try to decode the `str` using
1509 # utf-8, which is very likely to blow up on characters out
1510 # of the ascii range (in range [128, 256))
1511 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1514 return '<script>window.top.%s(%s);</script>' % (
1515 jsonp, simplejson.dumps(
1516 {'records': data[:10]}, encoding=csvcode))
1517 except UnicodeDecodeError:
1518 return '<script>window.top.%s(%s);</script>' % (
1519 jsonp, simplejson.dumps({
1520 'message': u"Failed to decode CSV file using encoding %s, "
1521 u"try switching to a different encoding" % csvcode
1524 @openerpweb.httprequest
1525 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1527 modle_obj = req.session.model(model)
1528 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1529 simplejson.loads(meta))
1532 if not (csvdel and len(csvdel) == 1):
1533 error = u"The CSV delimiter must be a single character"
1535 if not indices and fields:
1536 error = u"You must select at least one field to import"
1539 return '<script>window.top.%s(%s);</script>' % (
1540 jsonp, simplejson.dumps({'error': {'message': error}}))
1542 # skip ignored records
1543 data_record = itertools.islice(
1544 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1547 # if only one index, itemgetter will return an atom rather than a tuple
1548 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1549 else: mapper = operator.itemgetter(*indices)
1554 # decode each data row
1556 [record.decode(csvcode) for record in row]
1557 for row in itertools.imap(mapper, data_record)
1558 # don't insert completely empty rows (can happen due to fields
1559 # filtering in case of e.g. o2m content rows)
1562 except UnicodeDecodeError:
1563 error = u"Failed to decode CSV file using encoding %s" % csvcode
1564 except csv.Error, e:
1565 error = u"Could not process CSV file: %s" % e
1567 # If the file contains nothing,
1569 error = u"File to import is empty"
1571 return '<script>window.top.%s(%s);</script>' % (
1572 jsonp, simplejson.dumps({'error': {'message': error}}))
1575 (code, record, message, _nope) = modle_obj.import_data(
1576 fields, data, 'init', '', False,
1577 req.session.eval_context(req.context))
1578 except xmlrpclib.Fault, e:
1579 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1580 return '<script>window.top.%s(%s);</script>' % (
1581 jsonp, simplejson.dumps({'error':error}))
1584 return '<script>window.top.%s(%s);</script>' % (
1585 jsonp, simplejson.dumps({'success':True}))
1587 msg = u"Error during import: %s\n\nTrying to import record %r" % (
1589 return '<script>window.top.%s(%s);</script>' % (
1590 jsonp, simplejson.dumps({'error': {'message':msg}}))