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 isinstance(e.faultCode, str)\
276 and e.faultCode.split(':')[0] == 'AccessDenied':
277 return {'error': e.faultCode, 'title': 'Database creation error'}
279 'error': "Could not create database '%s': %s" % (
280 params['db_name'], e.faultString),
281 'title': 'Database creation error'
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,
347 "login": req.session._login
350 @openerpweb.jsonrequest
351 def get_session_info(self, req):
352 req.session.assert_valid(force=True)
354 "uid": req.session._uid,
355 "context": req.session.get_context() if req.session._uid else False,
356 "db": req.session._db,
357 "login": req.session._login
360 @openerpweb.jsonrequest
361 def change_password (self,req,fields):
362 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
363 dict(map(operator.itemgetter('name', 'value'), fields)))
364 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
365 return {'error':'All passwords have to be filled.','title': 'Change Password'}
366 if new_password != confirm_password:
367 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
369 if req.session.model('res.users').change_password(
370 old_password, new_password):
371 return {'new_password':new_password}
373 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
374 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
376 @openerpweb.jsonrequest
377 def sc_list(self, req):
378 return req.session.model('ir.ui.view_sc').get_sc(
379 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
381 @openerpweb.jsonrequest
382 def get_lang_list(self, req):
385 'lang_list': (req.session.proxy("db").list_lang() or []),
389 return {"error": e, "title": "Languages"}
391 @openerpweb.jsonrequest
392 def modules(self, req):
393 # Compute available candidates module
394 loadable = openerpweb.addons_manifest.iterkeys()
395 loaded = req.config.server_wide_modules
396 candidates = [mod for mod in loadable if mod not in loaded]
398 # Compute active true modules that might be on the web side only
399 active = set(name for name in candidates
400 if openerpweb.addons_manifest[name].get('active'))
402 # Retrieve database installed modules
403 Modules = req.session.model('ir.module.module')
404 installed = set(module['name'] for module in Modules.search_read(
405 [('state','=','installed'), ('name','in', candidates)], ['name']))
408 return list(active | installed)
410 @openerpweb.jsonrequest
411 def eval_domain_and_context(self, req, contexts, domains,
413 """ Evaluates sequences of domains and contexts, composing them into
414 a single context, domain or group_by sequence.
416 :param list contexts: list of contexts to merge together. Contexts are
417 evaluated in sequence, all previous contexts
418 are part of their own evaluation context
419 (starting at the session context).
420 :param list domains: list of domains to merge together. Domains are
421 evaluated in sequence and appended to one another
422 (implicit AND), their evaluation domain is the
423 result of merging all contexts.
424 :param list group_by_seq: list of domains (which may be in a different
425 order than the ``contexts`` parameter),
426 evaluated in sequence, their ``'group_by'``
427 key is extracted if they have one.
432 the global context created by merging all of
436 the concatenation of all domains
439 a list of fields to group by, potentially empty (in which case
440 no group by should be performed)
442 context, domain = eval_context_and_domain(req.session,
443 web.common.nonliterals.CompoundContext(*(contexts or [])),
444 web.common.nonliterals.CompoundDomain(*(domains or [])))
446 group_by_sequence = []
447 for candidate in (group_by_seq or []):
448 ctx = req.session.eval_context(candidate, context)
449 group_by = ctx.get('group_by')
452 elif isinstance(group_by, basestring):
453 group_by_sequence.append(group_by)
455 group_by_sequence.extend(group_by)
460 'group_by': group_by_sequence
463 @openerpweb.jsonrequest
464 def save_session_action(self, req, the_action):
466 This method store an action object in the session object and returns an integer
467 identifying that action. The method get_session_action() can be used to get
470 :param the_action: The action to save in the session.
471 :type the_action: anything
472 :return: A key identifying the saved action.
475 saved_actions = req.httpsession.get('saved_actions')
476 if not saved_actions:
477 saved_actions = {"next":0, "actions":{}}
478 req.httpsession['saved_actions'] = saved_actions
479 # we don't allow more than 10 stored actions
480 if len(saved_actions["actions"]) >= 10:
481 del saved_actions["actions"][min(saved_actions["actions"].keys())]
482 key = saved_actions["next"]
483 saved_actions["actions"][key] = the_action
484 saved_actions["next"] = key + 1
487 @openerpweb.jsonrequest
488 def get_session_action(self, req, key):
490 Gets back a previously saved action. This method can return None if the action
491 was saved since too much time (this case should be handled in a smart way).
493 :param key: The key given by save_session_action()
495 :return: The saved action or None.
498 saved_actions = req.httpsession.get('saved_actions')
499 if not saved_actions:
501 return saved_actions["actions"].get(key)
503 @openerpweb.jsonrequest
504 def check(self, req):
505 req.session.assert_valid()
508 def eval_context_and_domain(session, context, domain=None):
509 e_context = session.eval_context(context)
510 # should we give the evaluated context as an evaluation context to the domain?
511 e_domain = session.eval_domain(domain or [])
513 return e_context, e_domain
515 def load_actions_from_ir_values(req, key, key2, models, meta):
516 context = req.session.eval_context(req.context)
517 Values = req.session.model('ir.values')
518 actions = Values.get(key, key2, models, meta, context)
520 return [(id, name, clean_action(req, action))
521 for id, name, action in actions]
523 def clean_action(req, action, do_not_eval=False):
524 action.setdefault('flags', {})
526 context = req.session.eval_context(req.context)
527 eval_ctx = req.session.evaluation_context(context)
530 # values come from the server, we can just eval them
531 if isinstance(action.get('context'), basestring):
532 action['context'] = eval( action['context'], eval_ctx ) or {}
534 if isinstance(action.get('domain'), basestring):
535 action['domain'] = eval( action['domain'], eval_ctx ) or []
537 if 'context' in action:
538 action['context'] = parse_context(action['context'], req.session)
539 if 'domain' in action:
540 action['domain'] = parse_domain(action['domain'], req.session)
542 if 'type' not in action:
543 action['type'] = 'ir.actions.act_window_close'
545 if action['type'] == 'ir.actions.act_window':
546 return fix_view_modes(action)
549 # I think generate_views,fix_view_modes should go into js ActionManager
550 def generate_views(action):
552 While the server generates a sequence called "views" computing dependencies
553 between a bunch of stuff for views coming directly from the database
554 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
555 to return custom view dictionaries generated on the fly.
557 In that case, there is no ``views`` key available on the action.
559 Since the web client relies on ``action['views']``, generate it here from
560 ``view_mode`` and ``view_id``.
562 Currently handles two different cases:
564 * no view_id, multiple view_mode
565 * single view_id, single view_mode
567 :param dict action: action descriptor dictionary to generate a views key for
569 view_id = action.get('view_id', False)
570 if isinstance(view_id, (list, tuple)):
573 # providing at least one view mode is a requirement, not an option
574 view_modes = action['view_mode'].split(',')
576 if len(view_modes) > 1:
578 raise ValueError('Non-db action dictionaries should provide '
579 'either multiple view modes or a single view '
580 'mode and an optional view id.\n\n Got view '
581 'modes %r and view id %r for action %r' % (
582 view_modes, view_id, action))
583 action['views'] = [(False, mode) for mode in view_modes]
585 action['views'] = [(view_id, view_modes[0])]
587 def fix_view_modes(action):
588 """ For historical reasons, OpenERP has weird dealings in relation to
589 view_mode and the view_type attribute (on window actions):
591 * one of the view modes is ``tree``, which stands for both list views
593 * the choice is made by checking ``view_type``, which is either
594 ``form`` for a list view or ``tree`` for an actual tree view
596 This methods simply folds the view_type into view_mode by adding a
597 new view mode ``list`` which is the result of the ``tree`` view_mode
598 in conjunction with the ``form`` view_type.
600 TODO: this should go into the doc, some kind of "peculiarities" section
602 :param dict action: an action descriptor
603 :returns: nothing, the action is modified in place
605 if 'views' not in action:
606 generate_views(action)
609 for index, (id, mode) in enumerate(action['views']):
613 if id_form is not None:
614 action['views'].insert(index + 1, (id_form, 'page'))
616 if action.pop('view_type', 'form') != 'form':
620 [id, mode if mode != 'tree' else 'list']
621 for id, mode in action['views']
626 class Menu(openerpweb.Controller):
627 _cp_path = "/web/menu"
629 @openerpweb.jsonrequest
631 return {'data': self.do_load(req)}
633 def do_load(self, req):
634 """ Loads all menu items (all applications and their sub-menus).
636 :param req: A request object, with an OpenERP session attribute
637 :type req: < session -> OpenERPSession >
638 :return: the menu root
639 :rtype: dict('children': menu_nodes)
641 Menus = req.session.model('ir.ui.menu')
642 # menus are loaded fully unlike a regular tree view, cause there are
643 # less than 512 items
644 context = req.session.eval_context(req.context)
645 menu_ids = Menus.search([], 0, False, False, context)
646 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
647 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
648 menu_items.append(menu_root)
650 # make a tree using parent_id
651 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
652 for menu_item in menu_items:
653 if menu_item['parent_id']:
654 parent = menu_item['parent_id'][0]
657 if parent in menu_items_map:
658 menu_items_map[parent].setdefault(
659 'children', []).append(menu_item)
661 # sort by sequence a tree using parent_id
662 for menu_item in menu_items:
663 menu_item.setdefault('children', []).sort(
664 key=lambda x:x["sequence"])
668 @openerpweb.jsonrequest
669 def action(self, req, menu_id):
670 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
671 [('ir.ui.menu', menu_id)], False)
672 return {"action": actions}
674 class DataSet(openerpweb.Controller):
675 _cp_path = "/web/dataset"
677 @openerpweb.jsonrequest
678 def fields(self, req, model):
679 return {'fields': req.session.model(model).fields_get(False,
680 req.session.eval_context(req.context))}
682 @openerpweb.jsonrequest
683 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
684 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
685 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
687 """ Performs a search() followed by a read() (if needed) using the
688 provided search criteria
690 :param req: a JSON-RPC request object
691 :type req: openerpweb.JsonRequest
692 :param str model: the name of the model to search on
693 :param fields: a list of the fields to return in the result records
695 :param int offset: from which index should the results start being returned
696 :param int limit: the maximum number of records to return
697 :param list domain: the search domain for the query
698 :param list sort: sorting directives
699 :returns: A structure (dict) with two keys: ids (all the ids matching
700 the (domain, context) pair) and records (paginated records
701 matching fields selection set)
704 Model = req.session.model(model)
706 context, domain = eval_context_and_domain(
707 req.session, req.context, domain)
709 ids = Model.search(domain, 0, False, sort or False, context)
710 # need to fill the dataset with all ids for the (domain, context) pair,
711 # so search un-paginated and paginate manually before reading
712 paginated_ids = ids[offset:(offset + limit if limit else None)]
713 if fields and fields == ['id']:
714 # shortcut read if we only want the ids
717 'records': map(lambda id: {'id': id}, paginated_ids)
720 records = Model.read(paginated_ids, fields or False, context)
721 records.sort(key=lambda obj: ids.index(obj['id']))
728 @openerpweb.jsonrequest
729 def read(self, req, model, ids, fields=False):
730 return self.do_search_read(req, model, ids, fields)
732 @openerpweb.jsonrequest
733 def get(self, req, model, ids, fields=False):
734 return self.do_get(req, model, ids, fields)
736 def do_get(self, req, model, ids, fields=False):
737 """ Fetches and returns the records of the model ``model`` whose ids
740 The results are in the same order as the inputs, but elements may be
741 missing (if there is no record left for the id)
743 :param req: the JSON-RPC2 request object
744 :type req: openerpweb.JsonRequest
745 :param model: the model to read from
747 :param ids: a list of identifiers
749 :param fields: a list of fields to fetch, ``False`` or empty to fetch
750 all fields in the model
751 :type fields: list | False
752 :returns: a list of records, in the same order as the list of ids
755 Model = req.session.model(model)
756 records = Model.read(ids, fields, req.session.eval_context(req.context))
758 record_map = dict((record['id'], record) for record in records)
760 return [record_map[id] for id in ids if record_map.get(id)]
762 @openerpweb.jsonrequest
763 def load(self, req, model, id, fields):
764 m = req.session.model(model)
766 r = m.read([id], False, req.session.eval_context(req.context))
769 return {'value': value}
771 @openerpweb.jsonrequest
772 def create(self, req, model, data):
773 m = req.session.model(model)
774 r = m.create(data, req.session.eval_context(req.context))
777 @openerpweb.jsonrequest
778 def save(self, req, model, id, data):
779 m = req.session.model(model)
780 r = m.write([id], data, req.session.eval_context(req.context))
783 @openerpweb.jsonrequest
784 def unlink(self, req, model, ids=()):
785 Model = req.session.model(model)
786 return Model.unlink(ids, req.session.eval_context(req.context))
788 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
789 has_domain = domain_id is not None and domain_id < len(args)
790 has_context = context_id is not None and context_id < len(args)
792 domain = args[domain_id] if has_domain else []
793 context = args[context_id] if has_context else {}
794 c, d = eval_context_and_domain(req.session, context, domain)
800 for i in xrange(len(args)):
801 if isinstance(args[i], web.common.nonliterals.BaseContext):
802 args[i] = req.session.eval_context(args[i])
803 if isinstance(args[i], web.common.nonliterals.BaseDomain):
804 args[i] = req.session.eval_domain(args[i])
806 return getattr(req.session.model(model), method)(*args)
808 @openerpweb.jsonrequest
809 def call(self, req, model, method, args, domain_id=None, context_id=None):
810 return self.call_common(req, model, method, args, domain_id, context_id)
812 @openerpweb.jsonrequest
813 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
814 action = self.call_common(req, model, method, args, domain_id, context_id)
815 if isinstance(action, dict) and action.get('type') != '':
816 return {'result': clean_action(req, action)}
817 return {'result': False}
819 @openerpweb.jsonrequest
820 def exec_workflow(self, req, model, id, signal):
821 r = req.session.exec_workflow(model, id, signal)
824 @openerpweb.jsonrequest
825 def default_get(self, req, model, fields):
826 Model = req.session.model(model)
827 return Model.default_get(fields, req.session.eval_context(req.context))
829 @openerpweb.jsonrequest
830 def name_search(self, req, model, search_str, domain=[], context={}):
831 m = req.session.model(model)
832 r = m.name_search(search_str+'%', domain, '=ilike', context)
835 class DataGroup(openerpweb.Controller):
836 _cp_path = "/web/group"
837 @openerpweb.jsonrequest
838 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
839 Model = req.session.model(model)
840 context, domain = eval_context_and_domain(req.session, req.context, domain)
842 return Model.read_group(
843 domain or [], fields, group_by_fields, 0, False,
844 dict(context, group_by=group_by_fields), sort or False)
846 class View(openerpweb.Controller):
847 _cp_path = "/web/view"
849 def fields_view_get(self, req, model, view_id, view_type,
850 transform=True, toolbar=False, submenu=False):
851 Model = req.session.model(model)
852 context = req.session.eval_context(req.context)
853 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
854 # todo fme?: check that we should pass the evaluated context here
855 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
856 if toolbar and transform:
857 self.process_toolbar(req, fvg['toolbar'])
860 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
861 # depending on how it feels, xmlrpclib.ServerProxy can translate
862 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
863 # enjoy unicode strings which can not be trivially converted to
864 # strings, and it blows up during parsing.
866 # So ensure we fix this retardation by converting view xml back to
868 if isinstance(fvg['arch'], unicode):
869 arch = fvg['arch'].encode('utf-8')
874 evaluation_context = session.evaluation_context(context or {})
875 xml = self.transform_view(arch, session, evaluation_context)
877 xml = ElementTree.fromstring(arch)
878 fvg['arch'] = web.common.xml2json.Xml2Json.convert_element(xml, preserve_whitespaces)
880 for field in fvg['fields'].itervalues():
881 if field.get('views'):
882 for view in field["views"].itervalues():
883 self.process_view(session, view, None, transform)
884 if field.get('domain'):
885 field["domain"] = parse_domain(field["domain"], session)
886 if field.get('context'):
887 field["context"] = parse_context(field["context"], session)
889 def process_toolbar(self, req, toolbar):
891 The toolbar is a mapping of section_key: [action_descriptor]
893 We need to clean all those actions in order to ensure correct
896 for actions in toolbar.itervalues():
897 for action in actions:
898 if 'context' in action:
899 action['context'] = parse_context(
900 action['context'], req.session)
901 if 'domain' in action:
902 action['domain'] = parse_domain(
903 action['domain'], req.session)
905 @openerpweb.jsonrequest
906 def add_custom(self, req, view_id, arch):
907 CustomView = req.session.model('ir.ui.view.custom')
909 'user_id': req.session._uid,
912 }, req.session.eval_context(req.context))
913 return {'result': True}
915 @openerpweb.jsonrequest
916 def undo_custom(self, req, view_id, reset=False):
917 CustomView = req.session.model('ir.ui.view.custom')
918 context = req.session.eval_context(req.context)
919 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
920 0, False, False, context)
923 CustomView.unlink(vcustom, context)
925 CustomView.unlink([vcustom[0]], context)
926 return {'result': True}
927 return {'result': False}
929 def transform_view(self, view_string, session, context=None):
930 # transform nodes on the fly via iterparse, instead of
931 # doing it statically on the parsing result
932 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
934 for event, elem in parser:
938 self.parse_domains_and_contexts(elem, session)
941 def parse_domains_and_contexts(self, elem, session):
942 """ Converts domains and contexts from the view into Python objects,
943 either literals if they can be parsed by literal_eval or a special
944 placeholder object if the domain or context refers to free variables.
946 :param elem: the current node being parsed
947 :type param: xml.etree.ElementTree.Element
948 :param session: OpenERP session object, used to store and retrieve
950 :type session: openerpweb.openerpweb.OpenERPSession
952 for el in ['domain', 'filter_domain']:
953 domain = elem.get(el, '').strip()
955 elem.set(el, parse_domain(domain, session))
956 elem.set(el + '_string', domain)
957 for el in ['context', 'default_get']:
958 context_string = elem.get(el, '').strip()
960 elem.set(el, parse_context(context_string, session))
961 elem.set(el + '_string', context_string)
963 @openerpweb.jsonrequest
964 def load(self, req, model, view_id, view_type, toolbar=False):
965 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
967 def parse_domain(domain, session):
968 """ Parses an arbitrary string containing a domain, transforms it
969 to either a literal domain or a :class:`web.common.nonliterals.Domain`
971 :param domain: the domain to parse, if the domain is not a string it
972 is assumed to be a literal domain and is returned as-is
973 :param session: Current OpenERP session
974 :type session: openerpweb.openerpweb.OpenERPSession
976 if not isinstance(domain, (str, unicode)):
979 return ast.literal_eval(domain)
982 return web.common.nonliterals.Domain(session, domain)
984 def parse_context(context, session):
985 """ Parses an arbitrary string containing a context, transforms it
986 to either a literal context or a :class:`web.common.nonliterals.Context`
988 :param context: the context to parse, if the context is not a string it
989 is assumed to be a literal domain and is returned as-is
990 :param session: Current OpenERP session
991 :type session: openerpweb.openerpweb.OpenERPSession
993 if not isinstance(context, (str, unicode)):
996 return ast.literal_eval(context)
998 return web.common.nonliterals.Context(session, context)
1000 class ListView(View):
1001 _cp_path = "/web/listview"
1003 def process_colors(self, view, row, context):
1004 colors = view['arch']['attrs'].get('colors')
1011 for pair in colors.split(';')
1012 if eval(pair.split(':')[1], dict(context, **row))
1017 elif len(color) == 1:
1021 class TreeView(View):
1022 _cp_path = "/web/treeview"
1024 @openerpweb.jsonrequest
1025 def action(self, req, model, id):
1026 return load_actions_from_ir_values(
1027 req,'action', 'tree_but_open',[(model, id)],
1030 class SearchView(View):
1031 _cp_path = "/web/searchview"
1033 @openerpweb.jsonrequest
1034 def load(self, req, model, view_id):
1035 fields_view = self.fields_view_get(req, model, view_id, 'search')
1036 return {'fields_view': fields_view}
1038 @openerpweb.jsonrequest
1039 def fields_get(self, req, model):
1040 Model = req.session.model(model)
1041 fields = Model.fields_get(False, req.session.eval_context(req.context))
1042 for field in fields.values():
1043 # shouldn't convert the views too?
1044 if field.get('domain'):
1045 field["domain"] = parse_domain(field["domain"], req.session)
1046 if field.get('context'):
1047 field["context"] = parse_context(field["context"], req.session)
1048 return {'fields': fields}
1050 @openerpweb.jsonrequest
1051 def get_filters(self, req, model):
1052 Model = req.session.model("ir.filters")
1053 filters = Model.get_filters(model)
1054 for filter in filters:
1055 filter["context"] = req.session.eval_context(parse_context(filter["context"], req.session))
1056 filter["domain"] = req.session.eval_domain(parse_domain(filter["domain"], req.session))
1059 @openerpweb.jsonrequest
1060 def save_filter(self, req, model, name, context_to_save, domain):
1061 Model = req.session.model("ir.filters")
1062 ctx = web.common.nonliterals.CompoundContext(context_to_save)
1063 ctx.session = req.session
1064 ctx = ctx.evaluate()
1065 domain = web.common.nonliterals.CompoundDomain(domain)
1066 domain.session = req.session
1067 domain = domain.evaluate()
1068 uid = req.session._uid
1069 context = req.session.eval_context(req.context)
1070 to_return = Model.create_or_replace({"context": ctx,
1078 @openerpweb.jsonrequest
1079 def add_to_dashboard(self, req, menu_id, action_id, context_to_save, domain, view_mode, name=''):
1080 ctx = web.common.nonliterals.CompoundContext(context_to_save)
1081 ctx.session = req.session
1082 ctx = ctx.evaluate()
1083 domain = web.common.nonliterals.CompoundDomain(domain)
1084 domain.session = req.session
1085 domain = domain.evaluate()
1087 dashboard_action = load_actions_from_ir_values(req, 'action', 'tree_but_open',
1088 [('ir.ui.menu', menu_id)], False)
1089 if dashboard_action:
1090 action = dashboard_action[0][2]
1091 if action['res_model'] == 'board.board' and action['views'][0][1] == 'form':
1092 # Maybe should check the content instead of model board.board ?
1093 view_id = action['views'][0][0]
1094 board = req.session.model(action['res_model']).fields_view_get(view_id, 'form')
1095 if board and 'arch' in board:
1096 xml = ElementTree.fromstring(board['arch'])
1097 column = xml.find('./board/column')
1099 new_action = ElementTree.Element('action', {
1100 'name' : str(action_id),
1102 'view_mode' : view_mode,
1103 'context' : str(ctx),
1104 'domain' : str(domain)
1106 column.insert(0, new_action)
1107 arch = ElementTree.tostring(xml, 'utf-8')
1108 return req.session.model('ir.ui.view.custom').create({
1109 'user_id': req.session._uid,
1112 }, req.session.eval_context(req.context))
1116 class Binary(openerpweb.Controller):
1117 _cp_path = "/web/binary"
1119 @openerpweb.httprequest
1120 def image(self, req, model, id, field, **kw):
1121 Model = req.session.model(model)
1122 context = req.session.eval_context(req.context)
1126 res = Model.default_get([field], context).get(field)
1128 res = Model.read([int(id)], [field], context)[0].get(field)
1129 image_data = base64.b64decode(res)
1130 except (TypeError, xmlrpclib.Fault):
1131 image_data = self.placeholder(req)
1132 return req.make_response(image_data, [
1133 ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
1134 def placeholder(self, req):
1135 addons_path = openerpweb.addons_manifest['web']['addons_path']
1136 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1138 @openerpweb.httprequest
1139 def saveas(self, req, model, id, field, fieldname, **kw):
1140 Model = req.session.model(model)
1141 context = req.session.eval_context(req.context)
1143 res = Model.read([int(id)], [field, fieldname], context)[0]
1145 res = Model.default_get([field, fieldname], context)
1146 filecontent = base64.b64decode(res.get(field, ''))
1148 return req.not_found()
1150 filename = '%s_%s' % (model.replace('.', '_'), id)
1152 filename = res.get(fieldname, '') or filename
1153 return req.make_response(filecontent,
1154 [('Content-Type', 'application/octet-stream'),
1155 ('Content-Disposition', 'attachment; filename=' + filename)])
1157 @openerpweb.httprequest
1158 def upload(self, req, callback, ufile):
1159 # TODO: might be useful to have a configuration flag for max-length file uploads
1161 out = """<script language="javascript" type="text/javascript">
1162 var win = window.top.window,
1164 if (typeof(callback) === 'function') {
1165 callback.apply(this, %s);
1167 win.jQuery('#oe_notification', win.document).notify('create', {
1168 title: "Ajax File Upload",
1169 text: "Could not find callback"
1174 args = [ufile.content_length, ufile.filename,
1175 ufile.content_type, base64.b64encode(data)]
1176 except Exception, e:
1177 args = [False, e.message]
1178 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1180 @openerpweb.httprequest
1181 def upload_attachment(self, req, callback, model, id, ufile):
1182 context = req.session.eval_context(req.context)
1183 Model = req.session.model('ir.attachment')
1185 out = """<script language="javascript" type="text/javascript">
1186 var win = window.top.window,
1188 if (typeof(callback) === 'function') {
1189 callback.call(this, %s);
1192 attachment_id = Model.create({
1193 'name': ufile.filename,
1194 'datas': base64.encodestring(ufile.read()),
1199 'filename': ufile.filename,
1202 except Exception, e:
1203 args = { 'error': e.message }
1204 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1206 class Action(openerpweb.Controller):
1207 _cp_path = "/web/action"
1209 @openerpweb.jsonrequest
1210 def load(self, req, action_id, do_not_eval=False):
1211 Actions = req.session.model('ir.actions.actions')
1213 context = req.session.eval_context(req.context)
1214 action_type = Actions.read([action_id], ['type'], context)
1217 if action_type[0]['type'] == 'ir.actions.report.xml':
1218 ctx.update({'bin_size': True})
1220 action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
1222 value = clean_action(req, action[0], do_not_eval)
1223 return {'result': value}
1225 @openerpweb.jsonrequest
1226 def run(self, req, action_id):
1227 return clean_action(req, req.session.model('ir.actions.server').run(
1228 [action_id], req.session.eval_context(req.context)))
1231 _cp_path = "/web/export"
1233 @openerpweb.jsonrequest
1234 def formats(self, req):
1235 """ Returns all valid export formats
1237 :returns: for each export format, a pair of identifier and printable name
1238 :rtype: [(str, str)]
1242 for path, controller in openerpweb.controllers_path.iteritems()
1243 if path.startswith(self._cp_path)
1244 if hasattr(controller, 'fmt')
1245 ], key=operator.itemgetter(1))
1247 def fields_get(self, req, model):
1248 Model = req.session.model(model)
1249 fields = Model.fields_get(False, req.session.eval_context(req.context))
1252 @openerpweb.jsonrequest
1253 def get_fields(self, req, model, prefix='', parent_name= '',
1254 import_compat=True, parent_field_type=None,
1257 if import_compat and parent_field_type == "many2one":
1260 fields = self.fields_get(req, model)
1263 fields.pop('id', None)
1265 fields['.id'] = fields.pop('id', {'string': 'ID'})
1267 fields_sequence = sorted(fields.iteritems(),
1268 key=lambda field: field[1].get('string', ''))
1271 for field_name, field in fields_sequence:
1272 if import_compat and (exclude and field_name in exclude):
1274 if import_compat and field.get('readonly'):
1275 # If none of the field's states unsets readonly, skip the field
1276 if all(dict(attrs).get('readonly', True)
1277 for attrs in field.get('states', {}).values()):
1280 id = prefix + (prefix and '/'or '') + field_name
1281 name = parent_name + (parent_name and '/' or '') + field['string']
1282 record = {'id': id, 'string': name,
1283 'value': id, 'children': False,
1284 'field_type': field.get('type'),
1285 'required': field.get('required'),
1286 'relation_field': field.get('relation_field')}
1287 records.append(record)
1289 if len(name.split('/')) < 3 and 'relation' in field:
1290 ref = field.pop('relation')
1291 record['value'] += '/id'
1292 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1294 if not import_compat or field['type'] == 'one2many':
1295 # m2m field in import_compat is childless
1296 record['children'] = True
1300 @openerpweb.jsonrequest
1301 def namelist(self,req, model, export_id):
1302 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1303 export = req.session.model("ir.exports").read([export_id])[0]
1304 export_fields_list = req.session.model("ir.exports.line").read(
1305 export['export_fields'])
1307 fields_data = self.fields_info(
1308 req, model, map(operator.itemgetter('name'), export_fields_list))
1311 {'name': field['name'], 'label': fields_data[field['name']]}
1312 for field in export_fields_list
1315 def fields_info(self, req, model, export_fields):
1317 fields = self.fields_get(req, model)
1319 # To make fields retrieval more efficient, fetch all sub-fields of a
1320 # given field at the same time. Because the order in the export list is
1321 # arbitrary, this requires ordering all sub-fields of a given field
1322 # together so they can be fetched at the same time
1324 # Works the following way:
1325 # * sort the list of fields to export, the default sorting order will
1326 # put the field itself (if present, for xmlid) and all of its
1327 # sub-fields right after it
1328 # * then, group on: the first field of the path (which is the same for
1329 # a field and for its subfields and the length of splitting on the
1330 # first '/', which basically means grouping the field on one side and
1331 # all of the subfields on the other. This way, we have the field (for
1332 # the xmlid) with length 1, and all of the subfields with the same
1333 # base but a length "flag" of 2
1334 # * if we have a normal field (length 1), just add it to the info
1335 # mapping (with its string) as-is
1336 # * otherwise, recursively call fields_info via graft_subfields.
1337 # all graft_subfields does is take the result of fields_info (on the
1338 # field's model) and prepend the current base (current field), which
1339 # rebuilds the whole sub-tree for the field
1341 # result: because we're not fetching the fields_get for half the
1342 # database models, fetching a namelist with a dozen fields (including
1343 # relational data) falls from ~6s to ~300ms (on the leads model).
1344 # export lists with no sub-fields (e.g. import_compatible lists with
1345 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1346 # there's a single fields_get to execute)
1347 for (base, length), subfields in itertools.groupby(
1348 sorted(export_fields),
1349 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1350 subfields = list(subfields)
1352 # subfields is a seq of $base/*rest, and not loaded yet
1353 info.update(self.graft_subfields(
1354 req, fields[base]['relation'], base, fields[base]['string'],
1358 info[base] = fields[base]['string']
1362 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1363 export_fields = [field.split('/', 1)[1] for field in fields]
1365 (prefix + '/' + k, prefix_string + '/' + v)
1366 for k, v in self.fields_info(req, model, export_fields).iteritems())
1368 #noinspection PyPropertyDefinition
1370 def content_type(self):
1371 """ Provides the format's content type """
1372 raise NotImplementedError()
1374 def filename(self, base):
1375 """ Creates a valid filename for the format (with extension) from the
1376 provided base name (exension-less)
1378 raise NotImplementedError()
1380 def from_data(self, fields, rows):
1381 """ Conversion method from OpenERP's export data to whatever the
1382 current export class outputs
1384 :params list fields: a list of fields to export
1385 :params list rows: a list of records to export
1389 raise NotImplementedError()
1391 @openerpweb.httprequest
1392 def index(self, req, data, token):
1393 model, fields, ids, domain, import_compat = \
1394 operator.itemgetter('model', 'fields', 'ids', 'domain',
1396 simplejson.loads(data))
1398 context = req.session.eval_context(req.context)
1399 Model = req.session.model(model)
1400 ids = ids or Model.search(domain, 0, False, False, context)
1402 field_names = map(operator.itemgetter('name'), fields)
1403 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1406 columns_headers = field_names
1408 columns_headers = [val['label'].strip() for val in fields]
1411 return req.make_response(self.from_data(columns_headers, import_data),
1412 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1413 ('Content-Type', self.content_type)],
1414 cookies={'fileToken': int(token)})
1416 class CSVExport(Export):
1417 _cp_path = '/web/export/csv'
1418 fmt = ('csv', 'CSV')
1421 def content_type(self):
1422 return 'text/csv;charset=utf8'
1424 def filename(self, base):
1425 return base + '.csv'
1427 def from_data(self, fields, rows):
1429 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1431 writer.writerow(fields)
1436 if isinstance(d, basestring):
1437 d = d.replace('\n',' ').replace('\t',' ')
1439 d = d.encode('utf-8')
1442 if d is False: d = None
1444 writer.writerow(row)
1451 class ExcelExport(Export):
1452 _cp_path = '/web/export/xls'
1453 fmt = ('xls', 'Excel')
1456 def content_type(self):
1457 return 'application/vnd.ms-excel'
1459 def filename(self, base):
1460 return base + '.xls'
1462 def from_data(self, fields, rows):
1465 workbook = xlwt.Workbook()
1466 worksheet = workbook.add_sheet('Sheet 1')
1468 for i, fieldname in enumerate(fields):
1469 worksheet.write(0, i, str(fieldname))
1470 worksheet.col(i).width = 8000 # around 220 pixels
1472 style = xlwt.easyxf('align: wrap yes')
1474 for row_index, row in enumerate(rows):
1475 for cell_index, cell_value in enumerate(row):
1476 if isinstance(cell_value, basestring):
1477 cell_value = re.sub("\r", " ", cell_value)
1478 if cell_value is False: cell_value = None
1479 worksheet.write(row_index + 1, cell_index, cell_value, style)
1488 class Reports(View):
1489 _cp_path = "/web/report"
1490 POLLING_DELAY = 0.25
1492 'doc': 'application/vnd.ms-word',
1493 'html': 'text/html',
1494 'odt': 'application/vnd.oasis.opendocument.text',
1495 'pdf': 'application/pdf',
1496 'sxw': 'application/vnd.sun.xml.writer',
1497 'xls': 'application/vnd.ms-excel',
1500 @openerpweb.httprequest
1501 def index(self, req, action, token):
1502 action = simplejson.loads(action)
1504 report_srv = req.session.proxy("report")
1505 context = req.session.eval_context(
1506 web.common.nonliterals.CompoundContext(
1507 req.context or {}, action[ "context"]))
1510 report_ids = context["active_ids"]
1511 if 'report_type' in action:
1512 report_data['report_type'] = action['report_type']
1513 if 'datas' in action:
1514 if 'ids' in action['datas']:
1515 report_ids = action['datas'].pop('ids')
1516 report_data.update(action['datas'])
1518 report_id = report_srv.report(
1519 req.session._db, req.session._uid, req.session._password,
1520 action["report_name"], report_ids,
1521 report_data, context)
1523 report_struct = None
1525 report_struct = report_srv.report_get(
1526 req.session._db, req.session._uid, req.session._password, report_id)
1527 if report_struct["state"]:
1530 time.sleep(self.POLLING_DELAY)
1532 report = base64.b64decode(report_struct['result'])
1533 if report_struct.get('code') == 'zlib':
1534 report = zlib.decompress(report)
1535 report_mimetype = self.TYPES_MAPPING.get(
1536 report_struct['format'], 'octet-stream')
1537 return req.make_response(report,
1539 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
1540 ('Content-Type', report_mimetype),
1541 ('Content-Length', len(report))],
1542 cookies={'fileToken': int(token)})
1545 _cp_path = "/web/import"
1547 def fields_get(self, req, model):
1548 Model = req.session.model(model)
1549 fields = Model.fields_get(False, req.session.eval_context(req.context))
1552 @openerpweb.httprequest
1553 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1555 data = list(csv.reader(
1556 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1557 except csv.Error, e:
1559 return '<script>window.top.%s(%s);</script>' % (
1560 jsonp, simplejson.dumps({'error': {
1561 'message': 'Error parsing CSV file: %s' % e,
1562 # decodes each byte to a unicode character, which may or
1563 # may not be printable, but decoding will succeed.
1564 # Otherwise simplejson will try to decode the `str` using
1565 # utf-8, which is very likely to blow up on characters out
1566 # of the ascii range (in range [128, 256))
1567 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1570 return '<script>window.top.%s(%s);</script>' % (
1571 jsonp, simplejson.dumps(
1572 {'records': data[:10]}, encoding=csvcode))
1573 except UnicodeDecodeError:
1574 return '<script>window.top.%s(%s);</script>' % (
1575 jsonp, simplejson.dumps({
1576 'message': u"Failed to decode CSV file using encoding %s, "
1577 u"try switching to a different encoding" % csvcode
1580 @openerpweb.httprequest
1581 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1583 modle_obj = req.session.model(model)
1584 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1585 simplejson.loads(meta))
1588 if not (csvdel and len(csvdel) == 1):
1589 error = u"The CSV delimiter must be a single character"
1591 if not indices and fields:
1592 error = u"You must select at least one field to import"
1595 return '<script>window.top.%s(%s);</script>' % (
1596 jsonp, simplejson.dumps({'error': {'message': error}}))
1598 # skip ignored records
1599 data_record = itertools.islice(
1600 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1603 # if only one index, itemgetter will return an atom rather than a tuple
1604 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1605 else: mapper = operator.itemgetter(*indices)
1610 # decode each data row
1612 [record.decode(csvcode) for record in row]
1613 for row in itertools.imap(mapper, data_record)
1614 # don't insert completely empty rows (can happen due to fields
1615 # filtering in case of e.g. o2m content rows)
1618 except UnicodeDecodeError:
1619 error = u"Failed to decode CSV file using encoding %s" % csvcode
1620 except csv.Error, e:
1621 error = u"Could not process CSV file: %s" % e
1623 # If the file contains nothing,
1625 error = u"File to import is empty"
1627 return '<script>window.top.%s(%s);</script>' % (
1628 jsonp, simplejson.dumps({'error': {'message': error}}))
1631 (code, record, message, _nope) = modle_obj.import_data(
1632 fields, data, 'init', '', False,
1633 req.session.eval_context(req.context))
1634 except xmlrpclib.Fault, e:
1635 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1636 return '<script>window.top.%s(%s);</script>' % (
1637 jsonp, simplejson.dumps({'error':error}))
1640 return '<script>window.top.%s(%s);</script>' % (
1641 jsonp, simplejson.dumps({'success':True}))
1643 msg = u"Error during import: %s\n\nTrying to import record %r" % (
1645 return '<script>window.top.%s(%s);</script>' % (
1646 jsonp, simplejson.dumps({'error': {'message':msg}}))