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']
274 return req.session.proxy("db").create(*create_attrs)
275 except xmlrpclib.Fault, e:
277 error_message = e.faultCode
278 if e.faultCode.split(':')[0] == 'AccessDenied':
279 return {'error': error_message, 'title': 'Create Database'}
280 if not error_message:
281 error_message = 'System has encountered problems while creating the Database "' + params['db_name'] + '"'
282 return {'error': error_message , 'title': 'Could not create database!'}
284 @openerpweb.jsonrequest
285 def drop(self, req, fields):
286 password, db = operator.itemgetter(
287 'drop_pwd', 'drop_db')(
288 dict(map(operator.itemgetter('name', 'value'), fields)))
291 return req.session.proxy("db").drop(password, db)
292 except xmlrpclib.Fault, e:
293 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
294 return {'error': e.faultCode, 'title': 'Drop Database'}
295 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
297 @openerpweb.httprequest
298 def backup(self, req, backup_db, backup_pwd, token):
300 db_dump = base64.b64decode(
301 req.session.proxy("db").dump(backup_pwd, backup_db))
302 return req.make_response(db_dump,
303 [('Content-Type', 'application/octet-stream; charset=binary'),
304 ('Content-Disposition', 'attachment; filename="' + backup_db + '.dump"')],
305 {'fileToken': int(token)}
307 except xmlrpclib.Fault, e:
308 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
309 return 'Backup Database|' + e.faultCode
310 return 'Backup Database|Could not generate database backup'
312 @openerpweb.httprequest
313 def restore(self, req, db_file, restore_pwd, new_db):
315 data = base64.b64encode(db_file.read())
316 req.session.proxy("db").restore(restore_pwd, new_db, data)
318 except xmlrpclib.Fault, e:
319 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
320 raise Exception("AccessDenied")
322 @openerpweb.jsonrequest
323 def change_password(self, req, fields):
324 old_password, new_password = operator.itemgetter(
325 'old_pwd', 'new_pwd')(
326 dict(map(operator.itemgetter('name', 'value'), fields)))
328 return req.session.proxy("db").change_admin_password(old_password, new_password)
329 except xmlrpclib.Fault, e:
330 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
331 return {'error': e.faultCode, 'title': 'Change Password'}
332 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
334 class Session(openerpweb.Controller):
335 _cp_path = "/web/session"
337 @openerpweb.jsonrequest
338 def login(self, req, db, login, password):
339 req.session.login(db, login, password)
340 ctx = req.session.get_context() if req.session._uid else {}
343 "session_id": req.session_id,
344 "uid": req.session._uid,
346 "db": req.session._db
349 @openerpweb.jsonrequest
350 def get_session_info(self, req):
351 req.session.assert_valid(force=True)
353 "uid": req.session._uid,
354 "context": req.session.get_context() if req.session._uid else False,
355 "db": req.session._db
358 @openerpweb.jsonrequest
359 def change_password (self,req,fields):
360 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
361 dict(map(operator.itemgetter('name', 'value'), fields)))
362 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
363 return {'error':'All passwords have to be filled.','title': 'Change Password'}
364 if new_password != confirm_password:
365 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
367 if req.session.model('res.users').change_password(
368 old_password, new_password):
369 return {'new_password':new_password}
371 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
372 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
374 @openerpweb.jsonrequest
375 def sc_list(self, req):
376 return req.session.model('ir.ui.view_sc').get_sc(
377 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
379 @openerpweb.jsonrequest
380 def get_lang_list(self, req):
383 'lang_list': (req.session.proxy("db").list_lang() or []),
387 return {"error": e, "title": "Languages"}
389 @openerpweb.jsonrequest
390 def modules(self, req):
391 # Compute available candidates module
392 loadable = openerpweb.addons_manifest.iterkeys()
393 loaded = req.config.server_wide_modules
394 candidates = [mod for mod in loadable if mod not in loaded]
396 # Compute active true modules that might be on the web side only
397 active = set(name for name in candidates
398 if openerpweb.addons_manifest[name].get('active'))
400 # Retrieve database installed modules
401 Modules = req.session.model('ir.module.module')
402 installed = set(module['name'] for module in Modules.search_read(
403 [('state','=','installed'), ('name','in', candidates)], ['name']))
406 return list(active | installed)
408 @openerpweb.jsonrequest
409 def eval_domain_and_context(self, req, contexts, domains,
411 """ Evaluates sequences of domains and contexts, composing them into
412 a single context, domain or group_by sequence.
414 :param list contexts: list of contexts to merge together. Contexts are
415 evaluated in sequence, all previous contexts
416 are part of their own evaluation context
417 (starting at the session context).
418 :param list domains: list of domains to merge together. Domains are
419 evaluated in sequence and appended to one another
420 (implicit AND), their evaluation domain is the
421 result of merging all contexts.
422 :param list group_by_seq: list of domains (which may be in a different
423 order than the ``contexts`` parameter),
424 evaluated in sequence, their ``'group_by'``
425 key is extracted if they have one.
430 the global context created by merging all of
434 the concatenation of all domains
437 a list of fields to group by, potentially empty (in which case
438 no group by should be performed)
440 context, domain = eval_context_and_domain(req.session,
441 web.common.nonliterals.CompoundContext(*(contexts or [])),
442 web.common.nonliterals.CompoundDomain(*(domains or [])))
444 group_by_sequence = []
445 for candidate in (group_by_seq or []):
446 ctx = req.session.eval_context(candidate, context)
447 group_by = ctx.get('group_by')
450 elif isinstance(group_by, basestring):
451 group_by_sequence.append(group_by)
453 group_by_sequence.extend(group_by)
458 'group_by': group_by_sequence
461 @openerpweb.jsonrequest
462 def save_session_action(self, req, the_action):
464 This method store an action object in the session object and returns an integer
465 identifying that action. The method get_session_action() can be used to get
468 :param the_action: The action to save in the session.
469 :type the_action: anything
470 :return: A key identifying the saved action.
473 saved_actions = req.httpsession.get('saved_actions')
474 if not saved_actions:
475 saved_actions = {"next":0, "actions":{}}
476 req.httpsession['saved_actions'] = saved_actions
477 # we don't allow more than 10 stored actions
478 if len(saved_actions["actions"]) >= 10:
479 del saved_actions["actions"][min(saved_actions["actions"].keys())]
480 key = saved_actions["next"]
481 saved_actions["actions"][key] = the_action
482 saved_actions["next"] = key + 1
485 @openerpweb.jsonrequest
486 def get_session_action(self, req, key):
488 Gets back a previously saved action. This method can return None if the action
489 was saved since too much time (this case should be handled in a smart way).
491 :param key: The key given by save_session_action()
493 :return: The saved action or None.
496 saved_actions = req.httpsession.get('saved_actions')
497 if not saved_actions:
499 return saved_actions["actions"].get(key)
501 @openerpweb.jsonrequest
502 def check(self, req):
503 req.session.assert_valid()
506 def eval_context_and_domain(session, context, domain=None):
507 e_context = session.eval_context(context)
508 # should we give the evaluated context as an evaluation context to the domain?
509 e_domain = session.eval_domain(domain or [])
511 return e_context, e_domain
513 def load_actions_from_ir_values(req, key, key2, models, meta):
514 context = req.session.eval_context(req.context)
515 Values = req.session.model('ir.values')
516 actions = Values.get(key, key2, models, meta, context)
518 return [(id, name, clean_action(req, action))
519 for id, name, action in actions]
521 def clean_action(req, action, do_not_eval=False):
522 action.setdefault('flags', {})
524 context = req.session.eval_context(req.context)
525 eval_ctx = req.session.evaluation_context(context)
528 # values come from the server, we can just eval them
529 if isinstance(action.get('context'), basestring):
530 action['context'] = eval( action['context'], eval_ctx ) or {}
532 if isinstance(action.get('domain'), basestring):
533 action['domain'] = eval( action['domain'], eval_ctx ) or []
535 if 'context' in action:
536 action['context'] = parse_context(action['context'], req.session)
537 if 'domain' in action:
538 action['domain'] = parse_domain(action['domain'], req.session)
540 if 'type' not in action:
541 action['type'] = 'ir.actions.act_window_close'
543 if action['type'] == 'ir.actions.act_window':
544 return fix_view_modes(action)
547 # I think generate_views,fix_view_modes should go into js ActionManager
548 def generate_views(action):
550 While the server generates a sequence called "views" computing dependencies
551 between a bunch of stuff for views coming directly from the database
552 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
553 to return custom view dictionaries generated on the fly.
555 In that case, there is no ``views`` key available on the action.
557 Since the web client relies on ``action['views']``, generate it here from
558 ``view_mode`` and ``view_id``.
560 Currently handles two different cases:
562 * no view_id, multiple view_mode
563 * single view_id, single view_mode
565 :param dict action: action descriptor dictionary to generate a views key for
567 view_id = action.get('view_id', False)
568 if isinstance(view_id, (list, tuple)):
571 # providing at least one view mode is a requirement, not an option
572 view_modes = action['view_mode'].split(',')
574 if len(view_modes) > 1:
576 raise ValueError('Non-db action dictionaries should provide '
577 'either multiple view modes or a single view '
578 'mode and an optional view id.\n\n Got view '
579 'modes %r and view id %r for action %r' % (
580 view_modes, view_id, action))
581 action['views'] = [(False, mode) for mode in view_modes]
583 action['views'] = [(view_id, view_modes[0])]
585 def fix_view_modes(action):
586 """ For historical reasons, OpenERP has weird dealings in relation to
587 view_mode and the view_type attribute (on window actions):
589 * one of the view modes is ``tree``, which stands for both list views
591 * the choice is made by checking ``view_type``, which is either
592 ``form`` for a list view or ``tree`` for an actual tree view
594 This methods simply folds the view_type into view_mode by adding a
595 new view mode ``list`` which is the result of the ``tree`` view_mode
596 in conjunction with the ``form`` view_type.
598 TODO: this should go into the doc, some kind of "peculiarities" section
600 :param dict action: an action descriptor
601 :returns: nothing, the action is modified in place
603 if 'views' not in action:
604 generate_views(action)
606 if action.pop('view_type', 'form') != 'form':
610 [id, mode if mode != 'tree' else 'list']
611 for id, mode in action['views']
616 class Menu(openerpweb.Controller):
617 _cp_path = "/web/menu"
619 @openerpweb.jsonrequest
621 return {'data': self.do_load(req)}
623 def do_load(self, req):
624 """ Loads all menu items (all applications and their sub-menus).
626 :param req: A request object, with an OpenERP session attribute
627 :type req: < session -> OpenERPSession >
628 :return: the menu root
629 :rtype: dict('children': menu_nodes)
631 Menus = req.session.model('ir.ui.menu')
632 # menus are loaded fully unlike a regular tree view, cause there are
633 # less than 512 items
634 context = req.session.eval_context(req.context)
635 menu_ids = Menus.search([], 0, False, False, context)
636 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
637 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
638 menu_items.append(menu_root)
640 # make a tree using parent_id
641 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
642 for menu_item in menu_items:
643 if menu_item['parent_id']:
644 parent = menu_item['parent_id'][0]
647 if parent in menu_items_map:
648 menu_items_map[parent].setdefault(
649 'children', []).append(menu_item)
651 # sort by sequence a tree using parent_id
652 for menu_item in menu_items:
653 menu_item.setdefault('children', []).sort(
654 key=lambda x:x["sequence"])
658 @openerpweb.jsonrequest
659 def action(self, req, menu_id):
660 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
661 [('ir.ui.menu', menu_id)], False)
662 return {"action": actions}
664 class DataSet(openerpweb.Controller):
665 _cp_path = "/web/dataset"
667 @openerpweb.jsonrequest
668 def fields(self, req, model):
669 return {'fields': req.session.model(model).fields_get(False,
670 req.session.eval_context(req.context))}
672 @openerpweb.jsonrequest
673 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
674 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
675 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
677 """ Performs a search() followed by a read() (if needed) using the
678 provided search criteria
680 :param req: a JSON-RPC request object
681 :type req: openerpweb.JsonRequest
682 :param str model: the name of the model to search on
683 :param fields: a list of the fields to return in the result records
685 :param int offset: from which index should the results start being returned
686 :param int limit: the maximum number of records to return
687 :param list domain: the search domain for the query
688 :param list sort: sorting directives
689 :returns: A structure (dict) with two keys: ids (all the ids matching
690 the (domain, context) pair) and records (paginated records
691 matching fields selection set)
694 Model = req.session.model(model)
696 context, domain = eval_context_and_domain(
697 req.session, req.context, domain)
699 ids = Model.search(domain, 0, False, sort or False, context)
700 # need to fill the dataset with all ids for the (domain, context) pair,
701 # so search un-paginated and paginate manually before reading
702 paginated_ids = ids[offset:(offset + limit if limit else None)]
703 if fields and fields == ['id']:
704 # shortcut read if we only want the ids
707 'records': map(lambda id: {'id': id}, paginated_ids)
710 records = Model.read(paginated_ids, fields or False, context)
711 records.sort(key=lambda obj: ids.index(obj['id']))
718 @openerpweb.jsonrequest
719 def read(self, req, model, ids, fields=False):
720 return self.do_search_read(req, model, ids, fields)
722 @openerpweb.jsonrequest
723 def get(self, req, model, ids, fields=False):
724 return self.do_get(req, model, ids, fields)
726 def do_get(self, req, model, ids, fields=False):
727 """ Fetches and returns the records of the model ``model`` whose ids
730 The results are in the same order as the inputs, but elements may be
731 missing (if there is no record left for the id)
733 :param req: the JSON-RPC2 request object
734 :type req: openerpweb.JsonRequest
735 :param model: the model to read from
737 :param ids: a list of identifiers
739 :param fields: a list of fields to fetch, ``False`` or empty to fetch
740 all fields in the model
741 :type fields: list | False
742 :returns: a list of records, in the same order as the list of ids
745 Model = req.session.model(model)
746 records = Model.read(ids, fields, req.session.eval_context(req.context))
748 record_map = dict((record['id'], record) for record in records)
750 return [record_map[id] for id in ids if record_map.get(id)]
752 @openerpweb.jsonrequest
753 def load(self, req, model, id, fields):
754 m = req.session.model(model)
756 r = m.read([id], False, req.session.eval_context(req.context))
759 return {'value': value}
761 @openerpweb.jsonrequest
762 def create(self, req, model, data):
763 m = req.session.model(model)
764 r = m.create(data, req.session.eval_context(req.context))
767 @openerpweb.jsonrequest
768 def save(self, req, model, id, data):
769 m = req.session.model(model)
770 r = m.write([id], data, req.session.eval_context(req.context))
773 @openerpweb.jsonrequest
774 def unlink(self, req, model, ids=()):
775 Model = req.session.model(model)
776 return Model.unlink(ids, req.session.eval_context(req.context))
778 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
779 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
780 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
781 c, d = eval_context_and_domain(req.session, context, domain)
782 if domain_id and len(args) - 1 >= domain_id:
784 if context_id and len(args) - 1 >= context_id:
787 for i in xrange(len(args)):
788 if isinstance(args[i], web.common.nonliterals.BaseContext):
789 args[i] = req.session.eval_context(args[i])
790 if isinstance(args[i], web.common.nonliterals.BaseDomain):
791 args[i] = req.session.eval_domain(args[i])
793 return getattr(req.session.model(model), method)(*args)
795 @openerpweb.jsonrequest
796 def call(self, req, model, method, args, domain_id=None, context_id=None):
797 return self.call_common(req, model, method, args, domain_id, context_id)
799 @openerpweb.jsonrequest
800 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
801 action = self.call_common(req, model, method, args, domain_id, context_id)
802 if isinstance(action, dict) and action.get('type') != '':
803 return {'result': clean_action(req, action)}
804 return {'result': False}
806 @openerpweb.jsonrequest
807 def exec_workflow(self, req, model, id, signal):
808 r = req.session.exec_workflow(model, id, signal)
811 @openerpweb.jsonrequest
812 def default_get(self, req, model, fields):
813 Model = req.session.model(model)
814 return Model.default_get(fields, req.session.eval_context(req.context))
816 @openerpweb.jsonrequest
817 def name_search(self, req, model, search_str, domain=[], context={}):
818 m = req.session.model(model)
819 r = m.name_search(search_str+'%', domain, '=ilike', context)
822 class DataGroup(openerpweb.Controller):
823 _cp_path = "/web/group"
824 @openerpweb.jsonrequest
825 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
826 Model = req.session.model(model)
827 context, domain = eval_context_and_domain(req.session, req.context, domain)
829 return Model.read_group(
830 domain or [], fields, group_by_fields, 0, False,
831 dict(context, group_by=group_by_fields), sort or False)
833 class View(openerpweb.Controller):
834 _cp_path = "/web/view"
836 def fields_view_get(self, req, model, view_id, view_type,
837 transform=True, toolbar=False, submenu=False):
838 Model = req.session.model(model)
839 context = req.session.eval_context(req.context)
840 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
841 # todo fme?: check that we should pass the evaluated context here
842 self.process_view(req.session, fvg, context, transform)
843 if toolbar and transform:
844 self.process_toolbar(req, fvg['toolbar'])
847 def process_view(self, session, fvg, context, transform):
848 # depending on how it feels, xmlrpclib.ServerProxy can translate
849 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
850 # enjoy unicode strings which can not be trivially converted to
851 # strings, and it blows up during parsing.
853 # So ensure we fix this retardation by converting view xml back to
855 if isinstance(fvg['arch'], unicode):
856 arch = fvg['arch'].encode('utf-8')
861 evaluation_context = session.evaluation_context(context or {})
862 xml = self.transform_view(arch, session, evaluation_context)
864 xml = ElementTree.fromstring(arch)
865 fvg['arch'] = web.common.xml2json.Xml2Json.convert_element(xml)
867 for field in fvg['fields'].itervalues():
868 if field.get('views'):
869 for view in field["views"].itervalues():
870 self.process_view(session, view, None, transform)
871 if field.get('domain'):
872 field["domain"] = parse_domain(field["domain"], session)
873 if field.get('context'):
874 field["context"] = parse_context(field["context"], session)
876 def process_toolbar(self, req, toolbar):
878 The toolbar is a mapping of section_key: [action_descriptor]
880 We need to clean all those actions in order to ensure correct
883 for actions in toolbar.itervalues():
884 for action in actions:
885 if 'context' in action:
886 action['context'] = parse_context(
887 action['context'], req.session)
888 if 'domain' in action:
889 action['domain'] = parse_domain(
890 action['domain'], req.session)
892 @openerpweb.jsonrequest
893 def add_custom(self, req, view_id, arch):
894 CustomView = req.session.model('ir.ui.view.custom')
896 'user_id': req.session._uid,
899 }, req.session.eval_context(req.context))
900 return {'result': True}
902 @openerpweb.jsonrequest
903 def undo_custom(self, req, view_id, reset=False):
904 CustomView = req.session.model('ir.ui.view.custom')
905 context = req.session.eval_context(req.context)
906 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
907 0, False, False, context)
910 CustomView.unlink(vcustom, context)
912 CustomView.unlink([vcustom[0]], context)
913 return {'result': True}
914 return {'result': False}
916 def transform_view(self, view_string, session, context=None):
917 # transform nodes on the fly via iterparse, instead of
918 # doing it statically on the parsing result
919 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
921 for event, elem in parser:
925 self.parse_domains_and_contexts(elem, session)
928 def parse_domains_and_contexts(self, elem, session):
929 """ Converts domains and contexts from the view into Python objects,
930 either literals if they can be parsed by literal_eval or a special
931 placeholder object if the domain or context refers to free variables.
933 :param elem: the current node being parsed
934 :type param: xml.etree.ElementTree.Element
935 :param session: OpenERP session object, used to store and retrieve
937 :type session: openerpweb.openerpweb.OpenERPSession
939 for el in ['domain', 'filter_domain']:
940 domain = elem.get(el, '').strip()
942 elem.set(el, parse_domain(domain, session))
943 elem.set(el + '_string', domain)
944 for el in ['context', 'default_get']:
945 context_string = elem.get(el, '').strip()
947 elem.set(el, parse_context(context_string, session))
948 elem.set(el + '_string', context_string)
950 @openerpweb.jsonrequest
951 def load(self, req, model, view_id, view_type, toolbar=False):
952 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
954 def parse_domain(domain, session):
955 """ Parses an arbitrary string containing a domain, transforms it
956 to either a literal domain or a :class:`web.common.nonliterals.Domain`
958 :param domain: the domain to parse, if the domain is not a string it
959 is assumed to be a literal domain and is returned as-is
960 :param session: Current OpenERP session
961 :type session: openerpweb.openerpweb.OpenERPSession
963 if not isinstance(domain, (str, unicode)):
966 return ast.literal_eval(domain)
969 return web.common.nonliterals.Domain(session, domain)
971 def parse_context(context, session):
972 """ Parses an arbitrary string containing a context, transforms it
973 to either a literal context or a :class:`web.common.nonliterals.Context`
975 :param context: the context to parse, if the context is not a string it
976 is assumed to be a literal domain and is returned as-is
977 :param session: Current OpenERP session
978 :type session: openerpweb.openerpweb.OpenERPSession
980 if not isinstance(context, (str, unicode)):
983 return ast.literal_eval(context)
985 return web.common.nonliterals.Context(session, context)
987 class ListView(View):
988 _cp_path = "/web/listview"
990 def process_colors(self, view, row, context):
991 colors = view['arch']['attrs'].get('colors')
998 for pair in colors.split(';')
999 if eval(pair.split(':')[1], dict(context, **row))
1004 elif len(color) == 1:
1008 class TreeView(View):
1009 _cp_path = "/web/treeview"
1011 @openerpweb.jsonrequest
1012 def action(self, req, model, id):
1013 return load_actions_from_ir_values(
1014 req,'action', 'tree_but_open',[(model, id)],
1017 class SearchView(View):
1018 _cp_path = "/web/searchview"
1020 @openerpweb.jsonrequest
1021 def load(self, req, model, view_id):
1022 fields_view = self.fields_view_get(req, model, view_id, 'search')
1023 return {'fields_view': fields_view}
1025 @openerpweb.jsonrequest
1026 def fields_get(self, req, model):
1027 Model = req.session.model(model)
1028 fields = Model.fields_get(False, req.session.eval_context(req.context))
1029 for field in fields.values():
1030 # shouldn't convert the views too?
1031 if field.get('domain'):
1032 field["domain"] = parse_domain(field["domain"], req.session)
1033 if field.get('context'):
1034 field["context"] = parse_context(field["context"], req.session)
1035 return {'fields': fields}
1037 @openerpweb.jsonrequest
1038 def get_filters(self, req, model):
1039 Model = req.session.model("ir.filters")
1040 filters = Model.get_filters(model)
1041 for filter in filters:
1042 filter["context"] = req.session.eval_context(parse_context(filter["context"], req.session))
1043 filter["domain"] = req.session.eval_domain(parse_domain(filter["domain"], req.session))
1046 @openerpweb.jsonrequest
1047 def save_filter(self, req, model, name, context_to_save, domain):
1048 Model = req.session.model("ir.filters")
1049 ctx = web.common.nonliterals.CompoundContext(context_to_save)
1050 ctx.session = req.session
1051 ctx = ctx.evaluate()
1052 domain = web.common.nonliterals.CompoundDomain(domain)
1053 domain.session = req.session
1054 domain = domain.evaluate()
1055 uid = req.session._uid
1056 context = req.session.eval_context(req.context)
1057 to_return = Model.create_or_replace({"context": ctx,
1065 class Binary(openerpweb.Controller):
1066 _cp_path = "/web/binary"
1068 @openerpweb.httprequest
1069 def image(self, req, model, id, field, **kw):
1070 Model = req.session.model(model)
1071 context = req.session.eval_context(req.context)
1075 res = Model.default_get([field], context).get(field)
1077 res = Model.read([int(id)], [field], context)[0].get(field)
1078 image_data = base64.b64decode(res)
1079 except (TypeError, xmlrpclib.Fault):
1080 image_data = self.placeholder(req)
1081 return req.make_response(image_data, [
1082 ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
1083 def placeholder(self, req):
1084 addons_path = openerpweb.addons_manifest['web']['addons_path']
1085 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1087 @openerpweb.httprequest
1088 def saveas(self, req, model, id, field, fieldname, **kw):
1089 Model = req.session.model(model)
1090 context = req.session.eval_context(req.context)
1092 res = Model.read([int(id)], [field, fieldname], context)[0]
1094 res = Model.default_get([field, fieldname], context)
1095 filecontent = base64.b64decode(res.get(field, ''))
1097 return req.not_found()
1099 filename = '%s_%s' % (model.replace('.', '_'), id)
1101 filename = res.get(fieldname, '') or filename
1102 return req.make_response(filecontent,
1103 [('Content-Type', 'application/octet-stream'),
1104 ('Content-Disposition', 'attachment; filename=' + filename)])
1106 @openerpweb.httprequest
1107 def upload(self, req, callback, ufile):
1108 # TODO: might be useful to have a configuration flag for max-length file uploads
1110 out = """<script language="javascript" type="text/javascript">
1111 var win = window.top.window,
1113 if (typeof(callback) === 'function') {
1114 callback.apply(this, %s);
1116 win.jQuery('#oe_notification', win.document).notify('create', {
1117 title: "Ajax File Upload",
1118 text: "Could not find callback"
1123 args = [ufile.content_length, ufile.filename,
1124 ufile.content_type, base64.b64encode(data)]
1125 except Exception, e:
1126 args = [False, e.message]
1127 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1129 @openerpweb.httprequest
1130 def upload_attachment(self, req, callback, model, id, ufile):
1131 context = req.session.eval_context(req.context)
1132 Model = req.session.model('ir.attachment')
1134 out = """<script language="javascript" type="text/javascript">
1135 var win = window.top.window,
1137 if (typeof(callback) === 'function') {
1138 callback.call(this, %s);
1141 attachment_id = Model.create({
1142 'name': ufile.filename,
1143 'datas': base64.encodestring(ufile.read()),
1148 'filename': ufile.filename,
1151 except Exception, e:
1152 args = { 'error': e.message }
1153 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1155 class Action(openerpweb.Controller):
1156 _cp_path = "/web/action"
1158 @openerpweb.jsonrequest
1159 def load(self, req, action_id, do_not_eval=False):
1160 Actions = req.session.model('ir.actions.actions')
1162 context = req.session.eval_context(req.context)
1163 action_type = Actions.read([action_id], ['type'], context)
1166 if action_type[0]['type'] == 'ir.actions.report.xml':
1167 ctx.update({'bin_size': True})
1169 action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
1171 value = clean_action(req, action[0], do_not_eval)
1172 return {'result': value}
1174 @openerpweb.jsonrequest
1175 def run(self, req, action_id):
1176 return clean_action(req, req.session.model('ir.actions.server').run(
1177 [action_id], req.session.eval_context(req.context)))
1180 _cp_path = "/web/export"
1182 @openerpweb.jsonrequest
1183 def formats(self, req):
1184 """ Returns all valid export formats
1186 :returns: for each export format, a pair of identifier and printable name
1187 :rtype: [(str, str)]
1191 for path, controller in openerpweb.controllers_path.iteritems()
1192 if path.startswith(self._cp_path)
1193 if hasattr(controller, 'fmt')
1194 ], key=operator.itemgetter(1))
1196 def fields_get(self, req, model):
1197 Model = req.session.model(model)
1198 fields = Model.fields_get(False, req.session.eval_context(req.context))
1201 @openerpweb.jsonrequest
1202 def get_fields(self, req, model, prefix='', parent_name= '',
1203 import_compat=True, parent_field_type=None,
1206 if import_compat and parent_field_type == "many2one":
1209 fields = self.fields_get(req, model)
1212 fields.pop('id', None)
1214 fields['.id'] = fields.pop('id', {'string': 'ID'})
1216 fields_sequence = sorted(fields.iteritems(),
1217 key=lambda field: field[1].get('string', ''))
1220 for field_name, field in fields_sequence:
1221 if import_compat and (exclude and field_name in exclude):
1223 if import_compat and field.get('readonly'):
1224 # If none of the field's states unsets readonly, skip the field
1225 if all(dict(attrs).get('readonly', True)
1226 for attrs in field.get('states', {}).values()):
1229 id = prefix + (prefix and '/'or '') + field_name
1230 name = parent_name + (parent_name and '/' or '') + field['string']
1231 record = {'id': id, 'string': name,
1232 'value': id, 'children': False,
1233 'field_type': field.get('type'),
1234 'required': field.get('required'),
1235 'relation_field': field.get('relation_field')}
1236 records.append(record)
1238 if len(name.split('/')) < 3 and 'relation' in field:
1239 ref = field.pop('relation')
1240 record['value'] += '/id'
1241 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1243 if not import_compat or field['type'] == 'one2many':
1244 # m2m field in import_compat is childless
1245 record['children'] = True
1249 @openerpweb.jsonrequest
1250 def namelist(self,req, model, export_id):
1251 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1252 export = req.session.model("ir.exports").read([export_id])[0]
1253 export_fields_list = req.session.model("ir.exports.line").read(
1254 export['export_fields'])
1256 fields_data = self.fields_info(
1257 req, model, map(operator.itemgetter('name'), export_fields_list))
1260 {'name': field['name'], 'label': fields_data[field['name']]}
1261 for field in export_fields_list
1264 def fields_info(self, req, model, export_fields):
1266 fields = self.fields_get(req, model)
1268 # To make fields retrieval more efficient, fetch all sub-fields of a
1269 # given field at the same time. Because the order in the export list is
1270 # arbitrary, this requires ordering all sub-fields of a given field
1271 # together so they can be fetched at the same time
1273 # Works the following way:
1274 # * sort the list of fields to export, the default sorting order will
1275 # put the field itself (if present, for xmlid) and all of its
1276 # sub-fields right after it
1277 # * then, group on: the first field of the path (which is the same for
1278 # a field and for its subfields and the length of splitting on the
1279 # first '/', which basically means grouping the field on one side and
1280 # all of the subfields on the other. This way, we have the field (for
1281 # the xmlid) with length 1, and all of the subfields with the same
1282 # base but a length "flag" of 2
1283 # * if we have a normal field (length 1), just add it to the info
1284 # mapping (with its string) as-is
1285 # * otherwise, recursively call fields_info via graft_subfields.
1286 # all graft_subfields does is take the result of fields_info (on the
1287 # field's model) and prepend the current base (current field), which
1288 # rebuilds the whole sub-tree for the field
1290 # result: because we're not fetching the fields_get for half the
1291 # database models, fetching a namelist with a dozen fields (including
1292 # relational data) falls from ~6s to ~300ms (on the leads model).
1293 # export lists with no sub-fields (e.g. import_compatible lists with
1294 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1295 # there's a single fields_get to execute)
1296 for (base, length), subfields in itertools.groupby(
1297 sorted(export_fields),
1298 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1299 subfields = list(subfields)
1301 # subfields is a seq of $base/*rest, and not loaded yet
1302 info.update(self.graft_subfields(
1303 req, fields[base]['relation'], base, fields[base]['string'],
1307 info[base] = fields[base]['string']
1311 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1312 export_fields = [field.split('/', 1)[1] for field in fields]
1314 (prefix + '/' + k, prefix_string + '/' + v)
1315 for k, v in self.fields_info(req, model, export_fields).iteritems())
1317 #noinspection PyPropertyDefinition
1319 def content_type(self):
1320 """ Provides the format's content type """
1321 raise NotImplementedError()
1323 def filename(self, base):
1324 """ Creates a valid filename for the format (with extension) from the
1325 provided base name (exension-less)
1327 raise NotImplementedError()
1329 def from_data(self, fields, rows):
1330 """ Conversion method from OpenERP's export data to whatever the
1331 current export class outputs
1333 :params list fields: a list of fields to export
1334 :params list rows: a list of records to export
1338 raise NotImplementedError()
1340 @openerpweb.httprequest
1341 def index(self, req, data, token):
1342 model, fields, ids, domain, import_compat = \
1343 operator.itemgetter('model', 'fields', 'ids', 'domain',
1345 simplejson.loads(data))
1347 context = req.session.eval_context(req.context)
1348 Model = req.session.model(model)
1349 ids = ids or Model.search(domain, context=context)
1351 field_names = map(operator.itemgetter('name'), fields)
1352 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1355 columns_headers = field_names
1357 columns_headers = [val['label'].strip() for val in fields]
1360 return req.make_response(self.from_data(columns_headers, import_data),
1361 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1362 ('Content-Type', self.content_type)],
1363 cookies={'fileToken': int(token)})
1365 class CSVExport(Export):
1366 _cp_path = '/web/export/csv'
1367 fmt = ('csv', 'CSV')
1370 def content_type(self):
1371 return 'text/csv;charset=utf8'
1373 def filename(self, base):
1374 return base + '.csv'
1376 def from_data(self, fields, rows):
1378 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1380 writer.writerow(fields)
1385 if isinstance(d, basestring):
1386 d = d.replace('\n',' ').replace('\t',' ')
1388 d = d.encode('utf-8')
1391 if d is False: d = None
1393 writer.writerow(row)
1400 class ExcelExport(Export):
1401 _cp_path = '/web/export/xls'
1402 fmt = ('xls', 'Excel')
1405 def content_type(self):
1406 return 'application/vnd.ms-excel'
1408 def filename(self, base):
1409 return base + '.xls'
1411 def from_data(self, fields, rows):
1414 workbook = xlwt.Workbook()
1415 worksheet = workbook.add_sheet('Sheet 1')
1417 for i, fieldname in enumerate(fields):
1418 worksheet.write(0, i, str(fieldname))
1419 worksheet.col(i).width = 8000 # around 220 pixels
1421 style = xlwt.easyxf('align: wrap yes')
1423 for row_index, row in enumerate(rows):
1424 for cell_index, cell_value in enumerate(row):
1425 if isinstance(cell_value, basestring):
1426 cell_value = re.sub("\r", " ", cell_value)
1427 if cell_value is False: cell_value = None
1428 worksheet.write(row_index + 1, cell_index, cell_value, style)
1437 class Reports(View):
1438 _cp_path = "/web/report"
1439 POLLING_DELAY = 0.25
1441 'doc': 'application/vnd.ms-word',
1442 'html': 'text/html',
1443 'odt': 'application/vnd.oasis.opendocument.text',
1444 'pdf': 'application/pdf',
1445 'sxw': 'application/vnd.sun.xml.writer',
1446 'xls': 'application/vnd.ms-excel',
1449 @openerpweb.httprequest
1450 def index(self, req, action, token):
1451 action = simplejson.loads(action)
1453 report_srv = req.session.proxy("report")
1454 context = req.session.eval_context(
1455 web.common.nonliterals.CompoundContext(
1456 req.context or {}, action[ "context"]))
1459 report_ids = context["active_ids"]
1460 if 'report_type' in action:
1461 report_data['report_type'] = action['report_type']
1462 if 'datas' in action:
1463 if 'ids' in action['datas']:
1464 report_ids = action['datas'].pop('ids')
1465 report_data.update(action['datas'])
1467 report_id = report_srv.report(
1468 req.session._db, req.session._uid, req.session._password,
1469 action["report_name"], report_ids,
1470 report_data, context)
1472 report_struct = None
1474 report_struct = report_srv.report_get(
1475 req.session._db, req.session._uid, req.session._password, report_id)
1476 if report_struct["state"]:
1479 time.sleep(self.POLLING_DELAY)
1481 report = base64.b64decode(report_struct['result'])
1482 if report_struct.get('code') == 'zlib':
1483 report = zlib.decompress(report)
1484 report_mimetype = self.TYPES_MAPPING.get(
1485 report_struct['format'], 'octet-stream')
1486 return req.make_response(report,
1488 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
1489 ('Content-Type', report_mimetype),
1490 ('Content-Length', len(report))],
1491 cookies={'fileToken': int(token)})
1494 _cp_path = "/web/import"
1496 def fields_get(self, req, model):
1497 Model = req.session.model(model)
1498 fields = Model.fields_get(False, req.session.eval_context(req.context))
1501 @openerpweb.httprequest
1502 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1504 data = list(csv.reader(
1505 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1506 except csv.Error, e:
1508 return '<script>window.top.%s(%s);</script>' % (
1509 jsonp, simplejson.dumps({'error': {
1510 'message': 'Error parsing CSV file: %s' % e,
1511 # decodes each byte to a unicode character, which may or
1512 # may not be printable, but decoding will succeed.
1513 # Otherwise simplejson will try to decode the `str` using
1514 # utf-8, which is very likely to blow up on characters out
1515 # of the ascii range (in range [128, 256))
1516 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1519 return '<script>window.top.%s(%s);</script>' % (
1520 jsonp, simplejson.dumps(
1521 {'records': data[:10]}, encoding=csvcode))
1522 except UnicodeDecodeError:
1523 return '<script>window.top.%s(%s);</script>' % (
1524 jsonp, simplejson.dumps({
1525 'message': u"Failed to decode CSV file using encoding %s, "
1526 u"try switching to a different encoding" % csvcode
1529 @openerpweb.httprequest
1530 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1532 modle_obj = req.session.model(model)
1533 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1534 simplejson.loads(meta))
1537 if not (csvdel and len(csvdel) == 1):
1538 error = u"The CSV delimiter must be a single character"
1540 if not indices and fields:
1541 error = u"You must select at least one field to import"
1544 return '<script>window.top.%s(%s);</script>' % (
1545 jsonp, simplejson.dumps({'error': {'message': error}}))
1547 # skip ignored records
1548 data_record = itertools.islice(
1549 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1552 # if only one index, itemgetter will return an atom rather than a tuple
1553 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1554 else: mapper = operator.itemgetter(*indices)
1559 # decode each data row
1561 [record.decode(csvcode) for record in row]
1562 for row in itertools.imap(mapper, data_record)
1563 # don't insert completely empty rows (can happen due to fields
1564 # filtering in case of e.g. o2m content rows)
1567 except UnicodeDecodeError:
1568 error = u"Failed to decode CSV file using encoding %s" % csvcode
1569 except csv.Error, e:
1570 error = u"Could not process CSV file: %s" % e
1572 # If the file contains nothing,
1574 error = u"File to import is empty"
1576 return '<script>window.top.%s(%s);</script>' % (
1577 jsonp, simplejson.dumps({'error': {'message': error}}))
1580 (code, record, message, _nope) = modle_obj.import_data(
1581 fields, data, 'init', '', False,
1582 req.session.eval_context(req.context))
1583 except xmlrpclib.Fault, e:
1584 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1585 return '<script>window.top.%s(%s);</script>' % (
1586 jsonp, simplejson.dumps({'error':error}))
1589 return '<script>window.top.%s(%s);</script>' % (
1590 jsonp, simplejson.dumps({'success':True}))
1592 msg = u"Error during import: %s\n\nTrying to import record %r" % (
1594 return '<script>window.top.%s(%s);</script>' % (
1595 jsonp, simplejson.dumps({'error': {'message':msg}}))