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,
342 "login": req.session._login
345 @openerpweb.jsonrequest
346 def get_session_info(self, req):
347 req.session.assert_valid(force=True)
349 "uid": req.session._uid,
350 "context": req.session.get_context() if req.session._uid else False,
351 "db": req.session._db,
352 "login": req.session._login
355 @openerpweb.jsonrequest
356 def change_password (self,req,fields):
357 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
358 dict(map(operator.itemgetter('name', 'value'), fields)))
359 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
360 return {'error':'All passwords have to be filled.','title': 'Change Password'}
361 if new_password != confirm_password:
362 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
364 if req.session.model('res.users').change_password(
365 old_password, new_password):
366 return {'new_password':new_password}
368 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
369 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
371 @openerpweb.jsonrequest
372 def sc_list(self, req):
373 return req.session.model('ir.ui.view_sc').get_sc(
374 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
376 @openerpweb.jsonrequest
377 def get_lang_list(self, req):
380 'lang_list': (req.session.proxy("db").list_lang() or []),
384 return {"error": e, "title": "Languages"}
386 @openerpweb.jsonrequest
387 def modules(self, req):
388 # Compute available candidates module
389 loadable = openerpweb.addons_manifest.iterkeys()
390 loaded = req.config.server_wide_modules
391 candidates = [mod for mod in loadable if mod not in loaded]
393 # Compute active true modules that might be on the web side only
394 active = set(name for name in candidates
395 if openerpweb.addons_manifest[name].get('active'))
397 # Retrieve database installed modules
398 Modules = req.session.model('ir.module.module')
399 installed = set(module['name'] for module in Modules.search_read(
400 [('state','=','installed'), ('name','in', candidates)], ['name']))
403 return list(active | installed)
405 @openerpweb.jsonrequest
406 def eval_domain_and_context(self, req, contexts, domains,
408 """ Evaluates sequences of domains and contexts, composing them into
409 a single context, domain or group_by sequence.
411 :param list contexts: list of contexts to merge together. Contexts are
412 evaluated in sequence, all previous contexts
413 are part of their own evaluation context
414 (starting at the session context).
415 :param list domains: list of domains to merge together. Domains are
416 evaluated in sequence and appended to one another
417 (implicit AND), their evaluation domain is the
418 result of merging all contexts.
419 :param list group_by_seq: list of domains (which may be in a different
420 order than the ``contexts`` parameter),
421 evaluated in sequence, their ``'group_by'``
422 key is extracted if they have one.
427 the global context created by merging all of
431 the concatenation of all domains
434 a list of fields to group by, potentially empty (in which case
435 no group by should be performed)
437 context, domain = eval_context_and_domain(req.session,
438 web.common.nonliterals.CompoundContext(*(contexts or [])),
439 web.common.nonliterals.CompoundDomain(*(domains or [])))
441 group_by_sequence = []
442 for candidate in (group_by_seq or []):
443 ctx = req.session.eval_context(candidate, context)
444 group_by = ctx.get('group_by')
447 elif isinstance(group_by, basestring):
448 group_by_sequence.append(group_by)
450 group_by_sequence.extend(group_by)
455 'group_by': group_by_sequence
458 @openerpweb.jsonrequest
459 def save_session_action(self, req, the_action):
461 This method store an action object in the session object and returns an integer
462 identifying that action. The method get_session_action() can be used to get
465 :param the_action: The action to save in the session.
466 :type the_action: anything
467 :return: A key identifying the saved action.
470 saved_actions = req.httpsession.get('saved_actions')
471 if not saved_actions:
472 saved_actions = {"next":0, "actions":{}}
473 req.httpsession['saved_actions'] = saved_actions
474 # we don't allow more than 10 stored actions
475 if len(saved_actions["actions"]) >= 10:
476 del saved_actions["actions"][min(saved_actions["actions"].keys())]
477 key = saved_actions["next"]
478 saved_actions["actions"][key] = the_action
479 saved_actions["next"] = key + 1
482 @openerpweb.jsonrequest
483 def get_session_action(self, req, key):
485 Gets back a previously saved action. This method can return None if the action
486 was saved since too much time (this case should be handled in a smart way).
488 :param key: The key given by save_session_action()
490 :return: The saved action or None.
493 saved_actions = req.httpsession.get('saved_actions')
494 if not saved_actions:
496 return saved_actions["actions"].get(key)
498 @openerpweb.jsonrequest
499 def check(self, req):
500 req.session.assert_valid()
503 def eval_context_and_domain(session, context, domain=None):
504 e_context = session.eval_context(context)
505 # should we give the evaluated context as an evaluation context to the domain?
506 e_domain = session.eval_domain(domain or [])
508 return e_context, e_domain
510 def load_actions_from_ir_values(req, key, key2, models, meta):
511 context = req.session.eval_context(req.context)
512 Values = req.session.model('ir.values')
513 actions = Values.get(key, key2, models, meta, context)
515 return [(id, name, clean_action(req, action))
516 for id, name, action in actions]
518 def clean_action(req, action, do_not_eval=False):
519 action.setdefault('flags', {})
521 context = req.session.eval_context(req.context)
522 eval_ctx = req.session.evaluation_context(context)
525 # values come from the server, we can just eval them
526 if isinstance(action.get('context'), basestring):
527 action['context'] = eval( action['context'], eval_ctx ) or {}
529 if isinstance(action.get('domain'), basestring):
530 action['domain'] = eval( action['domain'], eval_ctx ) or []
532 if 'context' in action:
533 action['context'] = parse_context(action['context'], req.session)
534 if 'domain' in action:
535 action['domain'] = parse_domain(action['domain'], req.session)
537 if 'type' not in action:
538 action['type'] = 'ir.actions.act_window_close'
540 if action['type'] == 'ir.actions.act_window':
541 return fix_view_modes(action)
544 # I think generate_views,fix_view_modes should go into js ActionManager
545 def generate_views(action):
547 While the server generates a sequence called "views" computing dependencies
548 between a bunch of stuff for views coming directly from the database
549 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
550 to return custom view dictionaries generated on the fly.
552 In that case, there is no ``views`` key available on the action.
554 Since the web client relies on ``action['views']``, generate it here from
555 ``view_mode`` and ``view_id``.
557 Currently handles two different cases:
559 * no view_id, multiple view_mode
560 * single view_id, single view_mode
562 :param dict action: action descriptor dictionary to generate a views key for
564 view_id = action.get('view_id', False)
565 if isinstance(view_id, (list, tuple)):
568 # providing at least one view mode is a requirement, not an option
569 view_modes = action['view_mode'].split(',')
571 if len(view_modes) > 1:
573 raise ValueError('Non-db action dictionaries should provide '
574 'either multiple view modes or a single view '
575 'mode and an optional view id.\n\n Got view '
576 'modes %r and view id %r for action %r' % (
577 view_modes, view_id, action))
578 action['views'] = [(False, mode) for mode in view_modes]
580 action['views'] = [(view_id, view_modes[0])]
582 def fix_view_modes(action):
583 """ For historical reasons, OpenERP has weird dealings in relation to
584 view_mode and the view_type attribute (on window actions):
586 * one of the view modes is ``tree``, which stands for both list views
588 * the choice is made by checking ``view_type``, which is either
589 ``form`` for a list view or ``tree`` for an actual tree view
591 This methods simply folds the view_type into view_mode by adding a
592 new view mode ``list`` which is the result of the ``tree`` view_mode
593 in conjunction with the ``form`` view_type.
595 TODO: this should go into the doc, some kind of "peculiarities" section
597 :param dict action: an action descriptor
598 :returns: nothing, the action is modified in place
600 if 'views' not in action:
601 generate_views(action)
603 if action.pop('view_type', 'form') != 'form':
607 [id, mode if mode != 'tree' else 'list']
608 for id, mode in action['views']
613 class Menu(openerpweb.Controller):
614 _cp_path = "/web/menu"
616 @openerpweb.jsonrequest
618 return {'data': self.do_load(req)}
620 def do_load(self, req):
621 """ Loads all menu items (all applications and their sub-menus).
623 :param req: A request object, with an OpenERP session attribute
624 :type req: < session -> OpenERPSession >
625 :return: the menu root
626 :rtype: dict('children': menu_nodes)
628 Menus = req.session.model('ir.ui.menu')
629 # menus are loaded fully unlike a regular tree view, cause there are
630 # less than 512 items
631 context = req.session.eval_context(req.context)
632 menu_ids = Menus.search([], 0, False, False, context)
633 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
634 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
635 menu_items.append(menu_root)
637 # make a tree using parent_id
638 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
639 for menu_item in menu_items:
640 if menu_item['parent_id']:
641 parent = menu_item['parent_id'][0]
644 if parent in menu_items_map:
645 menu_items_map[parent].setdefault(
646 'children', []).append(menu_item)
648 # sort by sequence a tree using parent_id
649 for menu_item in menu_items:
650 menu_item.setdefault('children', []).sort(
651 key=lambda x:x["sequence"])
655 @openerpweb.jsonrequest
656 def action(self, req, menu_id):
657 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
658 [('ir.ui.menu', menu_id)], False)
659 return {"action": actions}
661 class DataSet(openerpweb.Controller):
662 _cp_path = "/web/dataset"
664 @openerpweb.jsonrequest
665 def fields(self, req, model):
666 return {'fields': req.session.model(model).fields_get(False,
667 req.session.eval_context(req.context))}
669 @openerpweb.jsonrequest
670 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
671 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
672 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
674 """ Performs a search() followed by a read() (if needed) using the
675 provided search criteria
677 :param req: a JSON-RPC request object
678 :type req: openerpweb.JsonRequest
679 :param str model: the name of the model to search on
680 :param fields: a list of the fields to return in the result records
682 :param int offset: from which index should the results start being returned
683 :param int limit: the maximum number of records to return
684 :param list domain: the search domain for the query
685 :param list sort: sorting directives
686 :returns: A structure (dict) with two keys: ids (all the ids matching
687 the (domain, context) pair) and records (paginated records
688 matching fields selection set)
691 Model = req.session.model(model)
693 context, domain = eval_context_and_domain(
694 req.session, req.context, domain)
696 ids = Model.search(domain, 0, False, sort or False, context)
697 # need to fill the dataset with all ids for the (domain, context) pair,
698 # so search un-paginated and paginate manually before reading
699 paginated_ids = ids[offset:(offset + limit if limit else None)]
700 if fields and fields == ['id']:
701 # shortcut read if we only want the ids
704 'records': map(lambda id: {'id': id}, paginated_ids)
707 records = Model.read(paginated_ids, fields or False, context)
708 records.sort(key=lambda obj: ids.index(obj['id']))
715 @openerpweb.jsonrequest
716 def read(self, req, model, ids, fields=False):
717 return self.do_search_read(req, model, ids, fields)
719 @openerpweb.jsonrequest
720 def get(self, req, model, ids, fields=False):
721 return self.do_get(req, model, ids, fields)
723 def do_get(self, req, model, ids, fields=False):
724 """ Fetches and returns the records of the model ``model`` whose ids
727 The results are in the same order as the inputs, but elements may be
728 missing (if there is no record left for the id)
730 :param req: the JSON-RPC2 request object
731 :type req: openerpweb.JsonRequest
732 :param model: the model to read from
734 :param ids: a list of identifiers
736 :param fields: a list of fields to fetch, ``False`` or empty to fetch
737 all fields in the model
738 :type fields: list | False
739 :returns: a list of records, in the same order as the list of ids
742 Model = req.session.model(model)
743 records = Model.read(ids, fields, req.session.eval_context(req.context))
745 record_map = dict((record['id'], record) for record in records)
747 return [record_map[id] for id in ids if record_map.get(id)]
749 @openerpweb.jsonrequest
750 def load(self, req, model, id, fields):
751 m = req.session.model(model)
753 r = m.read([id], False, req.session.eval_context(req.context))
756 return {'value': value}
758 @openerpweb.jsonrequest
759 def create(self, req, model, data):
760 m = req.session.model(model)
761 r = m.create(data, req.session.eval_context(req.context))
764 @openerpweb.jsonrequest
765 def save(self, req, model, id, data):
766 m = req.session.model(model)
767 r = m.write([id], data, req.session.eval_context(req.context))
770 @openerpweb.jsonrequest
771 def unlink(self, req, model, ids=()):
772 Model = req.session.model(model)
773 return Model.unlink(ids, req.session.eval_context(req.context))
775 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
776 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
777 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
778 c, d = eval_context_and_domain(req.session, context, domain)
779 if domain_id and len(args) - 1 >= domain_id:
781 if context_id and len(args) - 1 >= context_id:
784 for i in xrange(len(args)):
785 if isinstance(args[i], web.common.nonliterals.BaseContext):
786 args[i] = req.session.eval_context(args[i])
787 if isinstance(args[i], web.common.nonliterals.BaseDomain):
788 args[i] = req.session.eval_domain(args[i])
790 return getattr(req.session.model(model), method)(*args)
792 @openerpweb.jsonrequest
793 def call(self, req, model, method, args, domain_id=None, context_id=None):
794 return self.call_common(req, model, method, args, domain_id, context_id)
796 @openerpweb.jsonrequest
797 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
798 action = self.call_common(req, model, method, args, domain_id, context_id)
799 if isinstance(action, dict) and action.get('type') != '':
800 return {'result': clean_action(req, action)}
801 return {'result': False}
803 @openerpweb.jsonrequest
804 def exec_workflow(self, req, model, id, signal):
805 r = req.session.exec_workflow(model, id, signal)
808 @openerpweb.jsonrequest
809 def default_get(self, req, model, fields):
810 Model = req.session.model(model)
811 return Model.default_get(fields, req.session.eval_context(req.context))
813 @openerpweb.jsonrequest
814 def name_search(self, req, model, search_str, domain=[], context={}):
815 m = req.session.model(model)
816 r = m.name_search(search_str+'%', domain, '=ilike', context)
819 class DataGroup(openerpweb.Controller):
820 _cp_path = "/web/group"
821 @openerpweb.jsonrequest
822 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
823 Model = req.session.model(model)
824 context, domain = eval_context_and_domain(req.session, req.context, domain)
826 return Model.read_group(
827 domain or [], fields, group_by_fields, 0, False,
828 dict(context, group_by=group_by_fields), sort or False)
830 class View(openerpweb.Controller):
831 _cp_path = "/web/view"
833 def fields_view_get(self, req, model, view_id, view_type,
834 transform=True, toolbar=False, submenu=False):
835 Model = req.session.model(model)
836 context = req.session.eval_context(req.context)
837 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
838 # todo fme?: check that we should pass the evaluated context here
839 self.process_view(req.session, fvg, context, transform)
840 if toolbar and transform:
841 self.process_toolbar(req, fvg['toolbar'])
844 def process_view(self, session, fvg, context, transform):
845 # depending on how it feels, xmlrpclib.ServerProxy can translate
846 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
847 # enjoy unicode strings which can not be trivially converted to
848 # strings, and it blows up during parsing.
850 # So ensure we fix this retardation by converting view xml back to
852 if isinstance(fvg['arch'], unicode):
853 arch = fvg['arch'].encode('utf-8')
858 evaluation_context = session.evaluation_context(context or {})
859 xml = self.transform_view(arch, session, evaluation_context)
861 xml = ElementTree.fromstring(arch)
862 fvg['arch'] = web.common.xml2json.Xml2Json.convert_element(xml)
864 for field in fvg['fields'].itervalues():
865 if field.get('views'):
866 for view in field["views"].itervalues():
867 self.process_view(session, view, None, transform)
868 if field.get('domain'):
869 field["domain"] = parse_domain(field["domain"], session)
870 if field.get('context'):
871 field["context"] = parse_context(field["context"], session)
873 def process_toolbar(self, req, toolbar):
875 The toolbar is a mapping of section_key: [action_descriptor]
877 We need to clean all those actions in order to ensure correct
880 for actions in toolbar.itervalues():
881 for action in actions:
882 if 'context' in action:
883 action['context'] = parse_context(
884 action['context'], req.session)
885 if 'domain' in action:
886 action['domain'] = parse_domain(
887 action['domain'], req.session)
889 @openerpweb.jsonrequest
890 def add_custom(self, req, view_id, arch):
891 CustomView = req.session.model('ir.ui.view.custom')
893 'user_id': req.session._uid,
896 }, req.session.eval_context(req.context))
897 return {'result': True}
899 @openerpweb.jsonrequest
900 def undo_custom(self, req, view_id, reset=False):
901 CustomView = req.session.model('ir.ui.view.custom')
902 context = req.session.eval_context(req.context)
903 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
904 0, False, False, context)
907 CustomView.unlink(vcustom, context)
909 CustomView.unlink([vcustom[0]], context)
910 return {'result': True}
911 return {'result': False}
913 def transform_view(self, view_string, session, context=None):
914 # transform nodes on the fly via iterparse, instead of
915 # doing it statically on the parsing result
916 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
918 for event, elem in parser:
922 self.parse_domains_and_contexts(elem, session)
925 def parse_domains_and_contexts(self, elem, session):
926 """ Converts domains and contexts from the view into Python objects,
927 either literals if they can be parsed by literal_eval or a special
928 placeholder object if the domain or context refers to free variables.
930 :param elem: the current node being parsed
931 :type param: xml.etree.ElementTree.Element
932 :param session: OpenERP session object, used to store and retrieve
934 :type session: openerpweb.openerpweb.OpenERPSession
936 for el in ['domain', 'filter_domain']:
937 domain = elem.get(el, '').strip()
939 elem.set(el, parse_domain(domain, session))
940 elem.set(el + '_string', domain)
941 for el in ['context', 'default_get']:
942 context_string = elem.get(el, '').strip()
944 elem.set(el, parse_context(context_string, session))
945 elem.set(el + '_string', context_string)
947 @openerpweb.jsonrequest
948 def load(self, req, model, view_id, view_type, toolbar=False):
949 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
951 def parse_domain(domain, session):
952 """ Parses an arbitrary string containing a domain, transforms it
953 to either a literal domain or a :class:`web.common.nonliterals.Domain`
955 :param domain: the domain to parse, if the domain is not a string it
956 is assumed to be a literal domain and is returned as-is
957 :param session: Current OpenERP session
958 :type session: openerpweb.openerpweb.OpenERPSession
960 if not isinstance(domain, (str, unicode)):
963 return ast.literal_eval(domain)
966 return web.common.nonliterals.Domain(session, domain)
968 def parse_context(context, session):
969 """ Parses an arbitrary string containing a context, transforms it
970 to either a literal context or a :class:`web.common.nonliterals.Context`
972 :param context: the context to parse, if the context is not a string it
973 is assumed to be a literal domain and is returned as-is
974 :param session: Current OpenERP session
975 :type session: openerpweb.openerpweb.OpenERPSession
977 if not isinstance(context, (str, unicode)):
980 return ast.literal_eval(context)
982 return web.common.nonliterals.Context(session, context)
984 class ListView(View):
985 _cp_path = "/web/listview"
987 def process_colors(self, view, row, context):
988 colors = view['arch']['attrs'].get('colors')
995 for pair in colors.split(';')
996 if eval(pair.split(':')[1], dict(context, **row))
1001 elif len(color) == 1:
1005 class TreeView(View):
1006 _cp_path = "/web/treeview"
1008 @openerpweb.jsonrequest
1009 def action(self, req, model, id):
1010 return load_actions_from_ir_values(
1011 req,'action', 'tree_but_open',[(model, id)],
1014 class SearchView(View):
1015 _cp_path = "/web/searchview"
1017 @openerpweb.jsonrequest
1018 def load(self, req, model, view_id):
1019 fields_view = self.fields_view_get(req, model, view_id, 'search')
1020 return {'fields_view': fields_view}
1022 @openerpweb.jsonrequest
1023 def fields_get(self, req, model):
1024 Model = req.session.model(model)
1025 fields = Model.fields_get(False, req.session.eval_context(req.context))
1026 for field in fields.values():
1027 # shouldn't convert the views too?
1028 if field.get('domain'):
1029 field["domain"] = parse_domain(field["domain"], req.session)
1030 if field.get('context'):
1031 field["context"] = parse_context(field["context"], req.session)
1032 return {'fields': fields}
1034 @openerpweb.jsonrequest
1035 def get_filters(self, req, model):
1036 Model = req.session.model("ir.filters")
1037 filters = Model.get_filters(model)
1038 for filter in filters:
1039 filter["context"] = req.session.eval_context(parse_context(filter["context"], req.session))
1040 filter["domain"] = req.session.eval_domain(parse_domain(filter["domain"], req.session))
1043 @openerpweb.jsonrequest
1044 def save_filter(self, req, model, name, context_to_save, domain):
1045 Model = req.session.model("ir.filters")
1046 ctx = web.common.nonliterals.CompoundContext(context_to_save)
1047 ctx.session = req.session
1048 ctx = ctx.evaluate()
1049 domain = web.common.nonliterals.CompoundDomain(domain)
1050 domain.session = req.session
1051 domain = domain.evaluate()
1052 uid = req.session._uid
1053 context = req.session.eval_context(req.context)
1054 to_return = Model.create_or_replace({"context": ctx,
1062 class Binary(openerpweb.Controller):
1063 _cp_path = "/web/binary"
1065 @openerpweb.httprequest
1066 def image(self, req, model, id, field, **kw):
1067 Model = req.session.model(model)
1068 context = req.session.eval_context(req.context)
1072 res = Model.default_get([field], context).get(field)
1074 res = Model.read([int(id)], [field], context)[0].get(field)
1075 image_data = base64.b64decode(res)
1076 except (TypeError, xmlrpclib.Fault):
1077 image_data = self.placeholder(req)
1078 return req.make_response(image_data, [
1079 ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
1080 def placeholder(self, req):
1081 addons_path = openerpweb.addons_manifest['web']['addons_path']
1082 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1084 @openerpweb.httprequest
1085 def saveas(self, req, model, id, field, fieldname, **kw):
1086 Model = req.session.model(model)
1087 context = req.session.eval_context(req.context)
1089 res = Model.read([int(id)], [field, fieldname], context)[0]
1091 res = Model.default_get([field, fieldname], context)
1092 filecontent = base64.b64decode(res.get(field, ''))
1094 return req.not_found()
1096 filename = '%s_%s' % (model.replace('.', '_'), id)
1098 filename = res.get(fieldname, '') or filename
1099 return req.make_response(filecontent,
1100 [('Content-Type', 'application/octet-stream'),
1101 ('Content-Disposition', 'attachment; filename=' + filename)])
1103 @openerpweb.httprequest
1104 def upload(self, req, callback, ufile):
1105 # TODO: might be useful to have a configuration flag for max-length file uploads
1107 out = """<script language="javascript" type="text/javascript">
1108 var win = window.top.window,
1110 if (typeof(callback) === 'function') {
1111 callback.apply(this, %s);
1113 win.jQuery('#oe_notification', win.document).notify('create', {
1114 title: "Ajax File Upload",
1115 text: "Could not find callback"
1120 args = [ufile.content_length, ufile.filename,
1121 ufile.content_type, base64.b64encode(data)]
1122 except Exception, e:
1123 args = [False, e.message]
1124 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1126 @openerpweb.httprequest
1127 def upload_attachment(self, req, callback, model, id, ufile):
1128 context = req.session.eval_context(req.context)
1129 Model = req.session.model('ir.attachment')
1131 out = """<script language="javascript" type="text/javascript">
1132 var win = window.top.window,
1134 if (typeof(callback) === 'function') {
1135 callback.call(this, %s);
1138 attachment_id = Model.create({
1139 'name': ufile.filename,
1140 'datas': base64.encodestring(ufile.read()),
1145 'filename': ufile.filename,
1148 except Exception, e:
1149 args = { 'error': e.message }
1150 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1152 class Action(openerpweb.Controller):
1153 _cp_path = "/web/action"
1155 @openerpweb.jsonrequest
1156 def load(self, req, action_id, do_not_eval=False):
1157 Actions = req.session.model('ir.actions.actions')
1159 context = req.session.eval_context(req.context)
1160 action_type = Actions.read([action_id], ['type'], context)
1163 if action_type[0]['type'] == 'ir.actions.report.xml':
1164 ctx.update({'bin_size': True})
1166 action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
1168 value = clean_action(req, action[0], do_not_eval)
1169 return {'result': value}
1171 @openerpweb.jsonrequest
1172 def run(self, req, action_id):
1173 return clean_action(req, req.session.model('ir.actions.server').run(
1174 [action_id], req.session.eval_context(req.context)))
1177 _cp_path = "/web/export"
1179 @openerpweb.jsonrequest
1180 def formats(self, req):
1181 """ Returns all valid export formats
1183 :returns: for each export format, a pair of identifier and printable name
1184 :rtype: [(str, str)]
1188 for path, controller in openerpweb.controllers_path.iteritems()
1189 if path.startswith(self._cp_path)
1190 if hasattr(controller, 'fmt')
1191 ], key=operator.itemgetter(1))
1193 def fields_get(self, req, model):
1194 Model = req.session.model(model)
1195 fields = Model.fields_get(False, req.session.eval_context(req.context))
1198 @openerpweb.jsonrequest
1199 def get_fields(self, req, model, prefix='', parent_name= '',
1200 import_compat=True, parent_field_type=None,
1203 if import_compat and parent_field_type == "many2one":
1206 fields = self.fields_get(req, model)
1209 fields.pop('id', None)
1211 fields['.id'] = fields.pop('id', {'string': 'ID'})
1213 fields_sequence = sorted(fields.iteritems(),
1214 key=lambda field: field[1].get('string', ''))
1217 for field_name, field in fields_sequence:
1218 if import_compat and (exclude and field_name in exclude):
1220 if import_compat and field.get('readonly'):
1221 # If none of the field's states unsets readonly, skip the field
1222 if all(dict(attrs).get('readonly', True)
1223 for attrs in field.get('states', {}).values()):
1226 id = prefix + (prefix and '/'or '') + field_name
1227 name = parent_name + (parent_name and '/' or '') + field['string']
1228 record = {'id': id, 'string': name,
1229 'value': id, 'children': False,
1230 'field_type': field.get('type'),
1231 'required': field.get('required'),
1232 'relation_field': field.get('relation_field')}
1233 records.append(record)
1235 if len(name.split('/')) < 3 and 'relation' in field:
1236 ref = field.pop('relation')
1237 record['value'] += '/id'
1238 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1240 if not import_compat or field['type'] == 'one2many':
1241 # m2m field in import_compat is childless
1242 record['children'] = True
1246 @openerpweb.jsonrequest
1247 def namelist(self,req, model, export_id):
1248 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1249 export = req.session.model("ir.exports").read([export_id])[0]
1250 export_fields_list = req.session.model("ir.exports.line").read(
1251 export['export_fields'])
1253 fields_data = self.fields_info(
1254 req, model, map(operator.itemgetter('name'), export_fields_list))
1257 {'name': field['name'], 'label': fields_data[field['name']]}
1258 for field in export_fields_list
1261 def fields_info(self, req, model, export_fields):
1263 fields = self.fields_get(req, model)
1265 # To make fields retrieval more efficient, fetch all sub-fields of a
1266 # given field at the same time. Because the order in the export list is
1267 # arbitrary, this requires ordering all sub-fields of a given field
1268 # together so they can be fetched at the same time
1270 # Works the following way:
1271 # * sort the list of fields to export, the default sorting order will
1272 # put the field itself (if present, for xmlid) and all of its
1273 # sub-fields right after it
1274 # * then, group on: the first field of the path (which is the same for
1275 # a field and for its subfields and the length of splitting on the
1276 # first '/', which basically means grouping the field on one side and
1277 # all of the subfields on the other. This way, we have the field (for
1278 # the xmlid) with length 1, and all of the subfields with the same
1279 # base but a length "flag" of 2
1280 # * if we have a normal field (length 1), just add it to the info
1281 # mapping (with its string) as-is
1282 # * otherwise, recursively call fields_info via graft_subfields.
1283 # all graft_subfields does is take the result of fields_info (on the
1284 # field's model) and prepend the current base (current field), which
1285 # rebuilds the whole sub-tree for the field
1287 # result: because we're not fetching the fields_get for half the
1288 # database models, fetching a namelist with a dozen fields (including
1289 # relational data) falls from ~6s to ~300ms (on the leads model).
1290 # export lists with no sub-fields (e.g. import_compatible lists with
1291 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1292 # there's a single fields_get to execute)
1293 for (base, length), subfields in itertools.groupby(
1294 sorted(export_fields),
1295 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1296 subfields = list(subfields)
1298 # subfields is a seq of $base/*rest, and not loaded yet
1299 info.update(self.graft_subfields(
1300 req, fields[base]['relation'], base, fields[base]['string'],
1304 info[base] = fields[base]['string']
1308 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1309 export_fields = [field.split('/', 1)[1] for field in fields]
1311 (prefix + '/' + k, prefix_string + '/' + v)
1312 for k, v in self.fields_info(req, model, export_fields).iteritems())
1314 #noinspection PyPropertyDefinition
1316 def content_type(self):
1317 """ Provides the format's content type """
1318 raise NotImplementedError()
1320 def filename(self, base):
1321 """ Creates a valid filename for the format (with extension) from the
1322 provided base name (exension-less)
1324 raise NotImplementedError()
1326 def from_data(self, fields, rows):
1327 """ Conversion method from OpenERP's export data to whatever the
1328 current export class outputs
1330 :params list fields: a list of fields to export
1331 :params list rows: a list of records to export
1335 raise NotImplementedError()
1337 @openerpweb.httprequest
1338 def index(self, req, data, token):
1339 model, fields, ids, domain, import_compat = \
1340 operator.itemgetter('model', 'fields', 'ids', 'domain',
1342 simplejson.loads(data))
1344 context = req.session.eval_context(req.context)
1345 Model = req.session.model(model)
1346 ids = ids or Model.search(domain, context=context)
1348 field_names = map(operator.itemgetter('name'), fields)
1349 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1352 columns_headers = field_names
1354 columns_headers = [val['label'].strip() for val in fields]
1357 return req.make_response(self.from_data(columns_headers, import_data),
1358 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1359 ('Content-Type', self.content_type)],
1360 cookies={'fileToken': int(token)})
1362 class CSVExport(Export):
1363 _cp_path = '/web/export/csv'
1364 fmt = ('csv', 'CSV')
1367 def content_type(self):
1368 return 'text/csv;charset=utf8'
1370 def filename(self, base):
1371 return base + '.csv'
1373 def from_data(self, fields, rows):
1375 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1377 writer.writerow(fields)
1382 if isinstance(d, basestring):
1383 d = d.replace('\n',' ').replace('\t',' ')
1385 d = d.encode('utf-8')
1388 if d is False: d = None
1390 writer.writerow(row)
1397 class ExcelExport(Export):
1398 _cp_path = '/web/export/xls'
1399 fmt = ('xls', 'Excel')
1402 def content_type(self):
1403 return 'application/vnd.ms-excel'
1405 def filename(self, base):
1406 return base + '.xls'
1408 def from_data(self, fields, rows):
1411 workbook = xlwt.Workbook()
1412 worksheet = workbook.add_sheet('Sheet 1')
1414 for i, fieldname in enumerate(fields):
1415 worksheet.write(0, i, str(fieldname))
1416 worksheet.col(i).width = 8000 # around 220 pixels
1418 style = xlwt.easyxf('align: wrap yes')
1420 for row_index, row in enumerate(rows):
1421 for cell_index, cell_value in enumerate(row):
1422 if isinstance(cell_value, basestring):
1423 cell_value = re.sub("\r", " ", cell_value)
1424 if cell_value is False: cell_value = None
1425 worksheet.write(row_index + 1, cell_index, cell_value, style)
1434 class Reports(View):
1435 _cp_path = "/web/report"
1436 POLLING_DELAY = 0.25
1438 'doc': 'application/vnd.ms-word',
1439 'html': 'text/html',
1440 'odt': 'application/vnd.oasis.opendocument.text',
1441 'pdf': 'application/pdf',
1442 'sxw': 'application/vnd.sun.xml.writer',
1443 'xls': 'application/vnd.ms-excel',
1446 @openerpweb.httprequest
1447 def index(self, req, action, token):
1448 action = simplejson.loads(action)
1450 report_srv = req.session.proxy("report")
1451 context = req.session.eval_context(
1452 web.common.nonliterals.CompoundContext(
1453 req.context or {}, action[ "context"]))
1456 report_ids = context["active_ids"]
1457 if 'report_type' in action:
1458 report_data['report_type'] = action['report_type']
1459 if 'datas' in action:
1460 if 'ids' in action['datas']:
1461 report_ids = action['datas'].pop('ids')
1462 report_data.update(action['datas'])
1464 report_id = report_srv.report(
1465 req.session._db, req.session._uid, req.session._password,
1466 action["report_name"], report_ids,
1467 report_data, context)
1469 report_struct = None
1471 report_struct = report_srv.report_get(
1472 req.session._db, req.session._uid, req.session._password, report_id)
1473 if report_struct["state"]:
1476 time.sleep(self.POLLING_DELAY)
1478 report = base64.b64decode(report_struct['result'])
1479 if report_struct.get('code') == 'zlib':
1480 report = zlib.decompress(report)
1481 report_mimetype = self.TYPES_MAPPING.get(
1482 report_struct['format'], 'octet-stream')
1483 return req.make_response(report,
1485 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
1486 ('Content-Type', report_mimetype),
1487 ('Content-Length', len(report))],
1488 cookies={'fileToken': int(token)})
1491 _cp_path = "/web/import"
1493 def fields_get(self, req, model):
1494 Model = req.session.model(model)
1495 fields = Model.fields_get(False, req.session.eval_context(req.context))
1498 @openerpweb.httprequest
1499 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1501 data = list(csv.reader(
1502 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1503 except csv.Error, e:
1505 return '<script>window.top.%s(%s);</script>' % (
1506 jsonp, simplejson.dumps({'error': {
1507 'message': 'Error parsing CSV file: %s' % e,
1508 # decodes each byte to a unicode character, which may or
1509 # may not be printable, but decoding will succeed.
1510 # Otherwise simplejson will try to decode the `str` using
1511 # utf-8, which is very likely to blow up on characters out
1512 # of the ascii range (in range [128, 256))
1513 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1516 return '<script>window.top.%s(%s);</script>' % (
1517 jsonp, simplejson.dumps(
1518 {'records': data[:10]}, encoding=csvcode))
1519 except UnicodeDecodeError:
1520 return '<script>window.top.%s(%s);</script>' % (
1521 jsonp, simplejson.dumps({
1522 'message': u"Failed to decode CSV file using encoding %s, "
1523 u"try switching to a different encoding" % csvcode
1526 @openerpweb.httprequest
1527 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1529 modle_obj = req.session.model(model)
1530 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1531 simplejson.loads(meta))
1534 if not (csvdel and len(csvdel) == 1):
1535 error = u"The CSV delimiter must be a single character"
1537 if not indices and fields:
1538 error = u"You must select at least one field to import"
1541 return '<script>window.top.%s(%s);</script>' % (
1542 jsonp, simplejson.dumps({'error': {'message': error}}))
1544 # skip ignored records
1545 data_record = itertools.islice(
1546 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1549 # if only one index, itemgetter will return an atom rather than a tuple
1550 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1551 else: mapper = operator.itemgetter(*indices)
1556 # decode each data row
1558 [record.decode(csvcode) for record in row]
1559 for row in itertools.imap(mapper, data_record)
1560 # don't insert completely empty rows (can happen due to fields
1561 # filtering in case of e.g. o2m content rows)
1564 except UnicodeDecodeError:
1565 error = u"Failed to decode CSV file using encoding %s" % csvcode
1566 except csv.Error, e:
1567 error = u"Could not process CSV file: %s" % e
1569 # If the file contains nothing,
1571 error = u"File to import is empty"
1573 return '<script>window.top.%s(%s);</script>' % (
1574 jsonp, simplejson.dumps({'error': {'message': error}}))
1577 (code, record, message, _nope) = modle_obj.import_data(
1578 fields, data, 'init', '', False,
1579 req.session.eval_context(req.context))
1580 except xmlrpclib.Fault, e:
1581 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1582 return '<script>window.top.%s(%s);</script>' % (
1583 jsonp, simplejson.dumps({'error':error}))
1586 return '<script>window.top.%s(%s);</script>' % (
1587 jsonp, simplejson.dumps({'success':True}))
1589 msg = u"Error during import: %s\n\nTrying to import record %r" % (
1591 return '<script>window.top.%s(%s);</script>' % (
1592 jsonp, simplejson.dumps({'error': {'message':msg}}))