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(',')
108 manifest = openerpweb.addons_manifest.get(addon, None)
111 # ensure does not ends with /
112 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
113 globlist = manifest.get(key, [])
114 for pattern in globlist:
115 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
116 r.append( (path, path[len(addons_path):]))
119 def manifest_list(self, req, mods, extension):
121 path = '/web/webclient/' + extension
123 path += '?mods=' + mods
125 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in self.manifest_glob(req, mods, extension)]
127 @openerpweb.jsonrequest
128 def csslist(self, req, mods=None):
129 return self.manifest_list(req, mods, 'css')
131 @openerpweb.jsonrequest
132 def jslist(self, req, mods=None):
133 return self.manifest_list(req, mods, 'js')
135 @openerpweb.jsonrequest
136 def qweblist(self, req, mods=None):
137 return self.manifest_list(req, mods, 'qweb')
139 @openerpweb.httprequest
140 def css(self, req, mods=None):
142 files = list(self.manifest_glob(req, mods, 'css'))
143 file_map = dict(files)
145 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
146 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://)""", re.U)
150 """read the a css file and absolutify all relative uris"""
154 web_path = file_map[f]
155 web_dir = os.path.dirname(web_path)
159 r"""@import \1%s/""" % (web_dir,),
165 r"""url(\1%s/""" % (web_dir,),
170 content,timestamp = concat_files((f[0] for f in files), reader)
171 # TODO use timestamp to set Last mofified date and E-tag
172 return req.make_response(content, [('Content-Type', 'text/css')])
174 @openerpweb.httprequest
175 def js(self, req, mods=None):
176 files = [f[0] for f in self.manifest_glob(req, mods, 'js')]
177 content,timestamp = concat_files(files)
178 # TODO use timestamp to set Last mofified date and E-tag
179 return req.make_response(content, [('Content-Type', 'application/javascript')])
181 @openerpweb.httprequest
182 def qweb(self, req, mods=None):
183 files = [f[0] for f in self.manifest_glob(req, mods, 'qweb')]
184 content,timestamp = concat_xml(files)
185 # TODO use timestamp to set Last mofified date and E-tag
186 return req.make_response(content, [('Content-Type', 'text/xml')])
189 @openerpweb.httprequest
190 def home(self, req, s_action=None, **kw):
191 js = "\n ".join('<script type="text/javascript" src="%s"></script>'%i for i in self.manifest_list(req, None, 'js'))
192 css = "\n ".join('<link rel="stylesheet" href="%s">'%i for i in self.manifest_list(req, None, 'css'))
194 r = html_template % {
197 'modules': simplejson.dumps(self.server_wide_modules(req)),
198 'init': 'new s.web.WebClient("oe").start();',
202 @openerpweb.jsonrequest
203 def translations(self, req, mods, lang):
204 lang_model = req.session.model('res.lang')
205 ids = lang_model.search([("code", "=", lang)])
207 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
208 "grouping", "decimal_point", "thousands_sep"])
212 if lang.count("_") > 0:
216 langs = lang.split(separator)
217 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
220 for addon_name in mods:
221 transl = {"messages":[]}
222 transs[addon_name] = transl
224 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
225 f_name = os.path.join(addons_path, addon_name, "po", l + ".po")
226 if not os.path.exists(f_name):
229 with open(f_name) as t_file:
230 po = babel.messages.pofile.read_po(t_file)
234 if x.id and x.string:
235 transl["messages"].append({'id': x.id, 'string': x.string})
236 return {"modules": transs,
237 "lang_parameters": lang_obj}
239 @openerpweb.jsonrequest
240 def version_info(self, req):
242 "version": web.common.release.version
245 class Database(openerpweb.Controller):
246 _cp_path = "/web/database"
248 @openerpweb.jsonrequest
249 def get_list(self, req):
250 proxy = req.session.proxy("db")
252 h = req.httprequest.headers['Host'].split(':')[0]
254 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
255 dbs = [i for i in dbs if re.match(r, i)]
256 return {"db_list": dbs}
258 @openerpweb.jsonrequest
259 def progress(self, req, password, id):
260 return req.session.proxy('db').get_progress(password, id)
262 @openerpweb.jsonrequest
263 def create(self, req, fields):
265 params = dict(map(operator.itemgetter('name', 'value'), fields))
267 params['super_admin_pwd'],
269 bool(params.get('demo_data')),
271 params['create_admin_pwd']
275 return req.session.proxy("db").create(*create_attrs)
276 except xmlrpclib.Fault, e:
277 if e.faultCode and isinstance(e.faultCode, str)\
278 and e.faultCode.split(':')[0] == 'AccessDenied':
279 return {'error': e.faultCode, 'title': 'Database creation error'}
281 'error': "Could not create database '%s': %s" % (
282 params['db_name'], e.faultString),
283 'title': 'Database creation error'
286 @openerpweb.jsonrequest
287 def drop(self, req, fields):
288 password, db = operator.itemgetter(
289 'drop_pwd', 'drop_db')(
290 dict(map(operator.itemgetter('name', 'value'), fields)))
293 return req.session.proxy("db").drop(password, db)
294 except xmlrpclib.Fault, e:
295 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
296 return {'error': e.faultCode, 'title': 'Drop Database'}
297 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
299 @openerpweb.httprequest
300 def backup(self, req, backup_db, backup_pwd, token):
302 db_dump = base64.b64decode(
303 req.session.proxy("db").dump(backup_pwd, backup_db))
304 return req.make_response(db_dump,
305 [('Content-Type', 'application/octet-stream; charset=binary'),
306 ('Content-Disposition', 'attachment; filename="' + backup_db + '.dump"')],
307 {'fileToken': int(token)}
309 except xmlrpclib.Fault, e:
310 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
311 return 'Backup Database|' + e.faultCode
312 return 'Backup Database|Could not generate database backup'
314 @openerpweb.httprequest
315 def restore(self, req, db_file, restore_pwd, new_db):
317 data = base64.b64encode(db_file.read())
318 req.session.proxy("db").restore(restore_pwd, new_db, data)
320 except xmlrpclib.Fault, e:
321 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
322 raise Exception("AccessDenied")
324 @openerpweb.jsonrequest
325 def change_password(self, req, fields):
326 old_password, new_password = operator.itemgetter(
327 'old_pwd', 'new_pwd')(
328 dict(map(operator.itemgetter('name', 'value'), fields)))
330 return req.session.proxy("db").change_admin_password(old_password, new_password)
331 except xmlrpclib.Fault, e:
332 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
333 return {'error': e.faultCode, 'title': 'Change Password'}
334 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
336 class Session(openerpweb.Controller):
337 _cp_path = "/web/session"
339 @openerpweb.jsonrequest
340 def authenticate(self, req, db, login, password, base_location=None):
341 wsgienv = req.httprequest.environ
342 release = web.common.release
344 base_location=base_location,
345 HTTP_HOST=wsgienv['HTTP_HOST'],
346 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
347 user_agent="%s / %s" % (release.name, release.version),
349 req.session.authenticate(db, login, password, env)
350 ctx = req.session.get_context() if req.session._uid else {}
353 "session_id": req.session_id,
354 "uid": req.session._uid,
356 "db": req.session._db,
357 "login": req.session._login
360 @openerpweb.jsonrequest
361 def get_session_info(self, req):
362 req.session.assert_valid(force=True)
364 "uid": req.session._uid,
365 "context": req.session.get_context() if req.session._uid else False,
366 "db": req.session._db,
367 "login": req.session._login
370 @openerpweb.jsonrequest
371 def change_password (self,req,fields):
372 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
373 dict(map(operator.itemgetter('name', 'value'), fields)))
374 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
375 return {'error':'All passwords have to be filled.','title': 'Change Password'}
376 if new_password != confirm_password:
377 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
379 if req.session.model('res.users').change_password(
380 old_password, new_password):
381 return {'new_password':new_password}
383 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
384 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
386 @openerpweb.jsonrequest
387 def sc_list(self, req):
388 return req.session.model('ir.ui.view_sc').get_sc(
389 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
391 @openerpweb.jsonrequest
392 def get_lang_list(self, req):
395 'lang_list': (req.session.proxy("db").list_lang() or []),
399 return {"error": e, "title": "Languages"}
401 @openerpweb.jsonrequest
402 def modules(self, req):
403 # Compute available candidates module
404 loadable = openerpweb.addons_manifest.iterkeys()
405 loaded = req.config.server_wide_modules
406 candidates = [mod for mod in loadable if mod not in loaded]
408 # Compute active true modules that might be on the web side only
409 active = set(name for name in candidates
410 if openerpweb.addons_manifest[name].get('active'))
412 # Retrieve database installed modules
413 Modules = req.session.model('ir.module.module')
414 installed = set(module['name'] for module in Modules.search_read(
415 [('state','=','installed'), ('name','in', candidates)], ['name']))
418 return list(active | installed)
420 @openerpweb.jsonrequest
421 def eval_domain_and_context(self, req, contexts, domains,
423 """ Evaluates sequences of domains and contexts, composing them into
424 a single context, domain or group_by sequence.
426 :param list contexts: list of contexts to merge together. Contexts are
427 evaluated in sequence, all previous contexts
428 are part of their own evaluation context
429 (starting at the session context).
430 :param list domains: list of domains to merge together. Domains are
431 evaluated in sequence and appended to one another
432 (implicit AND), their evaluation domain is the
433 result of merging all contexts.
434 :param list group_by_seq: list of domains (which may be in a different
435 order than the ``contexts`` parameter),
436 evaluated in sequence, their ``'group_by'``
437 key is extracted if they have one.
442 the global context created by merging all of
446 the concatenation of all domains
449 a list of fields to group by, potentially empty (in which case
450 no group by should be performed)
452 context, domain = eval_context_and_domain(req.session,
453 web.common.nonliterals.CompoundContext(*(contexts or [])),
454 web.common.nonliterals.CompoundDomain(*(domains or [])))
456 group_by_sequence = []
457 for candidate in (group_by_seq or []):
458 ctx = req.session.eval_context(candidate, context)
459 group_by = ctx.get('group_by')
462 elif isinstance(group_by, basestring):
463 group_by_sequence.append(group_by)
465 group_by_sequence.extend(group_by)
470 'group_by': group_by_sequence
473 @openerpweb.jsonrequest
474 def save_session_action(self, req, the_action):
476 This method store an action object in the session object and returns an integer
477 identifying that action. The method get_session_action() can be used to get
480 :param the_action: The action to save in the session.
481 :type the_action: anything
482 :return: A key identifying the saved action.
485 saved_actions = req.httpsession.get('saved_actions')
486 if not saved_actions:
487 saved_actions = {"next":0, "actions":{}}
488 req.httpsession['saved_actions'] = saved_actions
489 # we don't allow more than 10 stored actions
490 if len(saved_actions["actions"]) >= 10:
491 del saved_actions["actions"][min(saved_actions["actions"].keys())]
492 key = saved_actions["next"]
493 saved_actions["actions"][key] = the_action
494 saved_actions["next"] = key + 1
497 @openerpweb.jsonrequest
498 def get_session_action(self, req, key):
500 Gets back a previously saved action. This method can return None if the action
501 was saved since too much time (this case should be handled in a smart way).
503 :param key: The key given by save_session_action()
505 :return: The saved action or None.
508 saved_actions = req.httpsession.get('saved_actions')
509 if not saved_actions:
511 return saved_actions["actions"].get(key)
513 @openerpweb.jsonrequest
514 def check(self, req):
515 req.session.assert_valid()
518 def eval_context_and_domain(session, context, domain=None):
519 e_context = session.eval_context(context)
520 # should we give the evaluated context as an evaluation context to the domain?
521 e_domain = session.eval_domain(domain or [])
523 return e_context, e_domain
525 def load_actions_from_ir_values(req, key, key2, models, meta):
526 context = req.session.eval_context(req.context)
527 Values = req.session.model('ir.values')
528 actions = Values.get(key, key2, models, meta, context)
530 return [(id, name, clean_action(req, action))
531 for id, name, action in actions]
533 def clean_action(req, action, do_not_eval=False):
534 action.setdefault('flags', {})
536 context = req.session.eval_context(req.context)
537 eval_ctx = req.session.evaluation_context(context)
540 # values come from the server, we can just eval them
541 if isinstance(action.get('context'), basestring):
542 action['context'] = eval( action['context'], eval_ctx ) or {}
544 if isinstance(action.get('domain'), basestring):
545 action['domain'] = eval( action['domain'], eval_ctx ) or []
547 if 'context' in action:
548 action['context'] = parse_context(action['context'], req.session)
549 if 'domain' in action:
550 action['domain'] = parse_domain(action['domain'], req.session)
552 if 'type' not in action:
553 action['type'] = 'ir.actions.act_window_close'
555 if action['type'] == 'ir.actions.act_window':
556 return fix_view_modes(action)
559 # I think generate_views,fix_view_modes should go into js ActionManager
560 def generate_views(action):
562 While the server generates a sequence called "views" computing dependencies
563 between a bunch of stuff for views coming directly from the database
564 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
565 to return custom view dictionaries generated on the fly.
567 In that case, there is no ``views`` key available on the action.
569 Since the web client relies on ``action['views']``, generate it here from
570 ``view_mode`` and ``view_id``.
572 Currently handles two different cases:
574 * no view_id, multiple view_mode
575 * single view_id, single view_mode
577 :param dict action: action descriptor dictionary to generate a views key for
579 view_id = action.get('view_id', False)
580 if isinstance(view_id, (list, tuple)):
583 # providing at least one view mode is a requirement, not an option
584 view_modes = action['view_mode'].split(',')
586 if len(view_modes) > 1:
588 raise ValueError('Non-db action dictionaries should provide '
589 'either multiple view modes or a single view '
590 'mode and an optional view id.\n\n Got view '
591 'modes %r and view id %r for action %r' % (
592 view_modes, view_id, action))
593 action['views'] = [(False, mode) for mode in view_modes]
595 action['views'] = [(view_id, view_modes[0])]
597 def fix_view_modes(action):
598 """ For historical reasons, OpenERP has weird dealings in relation to
599 view_mode and the view_type attribute (on window actions):
601 * one of the view modes is ``tree``, which stands for both list views
603 * the choice is made by checking ``view_type``, which is either
604 ``form`` for a list view or ``tree`` for an actual tree view
606 This methods simply folds the view_type into view_mode by adding a
607 new view mode ``list`` which is the result of the ``tree`` view_mode
608 in conjunction with the ``form`` view_type.
610 TODO: this should go into the doc, some kind of "peculiarities" section
612 :param dict action: an action descriptor
613 :returns: nothing, the action is modified in place
615 if not action.get('views'):
616 generate_views(action)
619 for index, (id, mode) in enumerate(action['views']):
623 if id_form is not None:
624 action['views'].insert(index + 1, (id_form, 'page'))
626 if action.pop('view_type', 'form') != 'form':
630 [id, mode if mode != 'tree' else 'list']
631 for id, mode in action['views']
636 class Menu(openerpweb.Controller):
637 _cp_path = "/web/menu"
639 @openerpweb.jsonrequest
641 return {'data': self.do_load(req)}
643 def do_load(self, req):
644 """ Loads all menu items (all applications and their sub-menus).
646 :param req: A request object, with an OpenERP session attribute
647 :type req: < session -> OpenERPSession >
648 :return: the menu root
649 :rtype: dict('children': menu_nodes)
651 Menus = req.session.model('ir.ui.menu')
652 # menus are loaded fully unlike a regular tree view, cause there are
653 # less than 512 items
654 context = req.session.eval_context(req.context)
655 menu_ids = Menus.search([], 0, False, False, context)
656 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
657 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
658 menu_items.append(menu_root)
660 # make a tree using parent_id
661 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
662 for menu_item in menu_items:
663 if menu_item['parent_id']:
664 parent = menu_item['parent_id'][0]
667 if parent in menu_items_map:
668 menu_items_map[parent].setdefault(
669 'children', []).append(menu_item)
671 # sort by sequence a tree using parent_id
672 for menu_item in menu_items:
673 menu_item.setdefault('children', []).sort(
674 key=lambda x:x["sequence"])
678 @openerpweb.jsonrequest
679 def action(self, req, menu_id):
680 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
681 [('ir.ui.menu', menu_id)], False)
682 return {"action": actions}
684 class DataSet(openerpweb.Controller):
685 _cp_path = "/web/dataset"
687 @openerpweb.jsonrequest
688 def fields(self, req, model):
689 return {'fields': req.session.model(model).fields_get(False,
690 req.session.eval_context(req.context))}
692 @openerpweb.jsonrequest
693 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
694 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
695 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
697 """ Performs a search() followed by a read() (if needed) using the
698 provided search criteria
700 :param req: a JSON-RPC request object
701 :type req: openerpweb.JsonRequest
702 :param str model: the name of the model to search on
703 :param fields: a list of the fields to return in the result records
705 :param int offset: from which index should the results start being returned
706 :param int limit: the maximum number of records to return
707 :param list domain: the search domain for the query
708 :param list sort: sorting directives
709 :returns: A structure (dict) with two keys: ids (all the ids matching
710 the (domain, context) pair) and records (paginated records
711 matching fields selection set)
714 Model = req.session.model(model)
716 context, domain = eval_context_and_domain(
717 req.session, req.context, domain)
719 ids = Model.search(domain, 0, False, sort or False, context)
720 # need to fill the dataset with all ids for the (domain, context) pair,
721 # so search un-paginated and paginate manually before reading
722 paginated_ids = ids[offset:(offset + limit if limit else None)]
723 if fields and fields == ['id']:
724 # shortcut read if we only want the ids
727 'records': map(lambda id: {'id': id}, paginated_ids)
730 records = Model.read(paginated_ids, fields or False, context)
731 records.sort(key=lambda obj: ids.index(obj['id']))
738 @openerpweb.jsonrequest
739 def read(self, req, model, ids, fields=False):
740 return self.do_search_read(req, model, ids, fields)
742 @openerpweb.jsonrequest
743 def get(self, req, model, ids, fields=False):
744 return self.do_get(req, model, ids, fields)
746 def do_get(self, req, model, ids, fields=False):
747 """ Fetches and returns the records of the model ``model`` whose ids
750 The results are in the same order as the inputs, but elements may be
751 missing (if there is no record left for the id)
753 :param req: the JSON-RPC2 request object
754 :type req: openerpweb.JsonRequest
755 :param model: the model to read from
757 :param ids: a list of identifiers
759 :param fields: a list of fields to fetch, ``False`` or empty to fetch
760 all fields in the model
761 :type fields: list | False
762 :returns: a list of records, in the same order as the list of ids
765 Model = req.session.model(model)
766 records = Model.read(ids, fields, req.session.eval_context(req.context))
768 record_map = dict((record['id'], record) for record in records)
770 return [record_map[id] for id in ids if record_map.get(id)]
772 @openerpweb.jsonrequest
773 def load(self, req, model, id, fields):
774 m = req.session.model(model)
776 r = m.read([id], False, req.session.eval_context(req.context))
779 return {'value': value}
781 @openerpweb.jsonrequest
782 def create(self, req, model, data):
783 m = req.session.model(model)
784 r = m.create(data, req.session.eval_context(req.context))
787 @openerpweb.jsonrequest
788 def save(self, req, model, id, data):
789 m = req.session.model(model)
790 r = m.write([id], data, req.session.eval_context(req.context))
793 @openerpweb.jsonrequest
794 def unlink(self, req, model, ids=()):
795 Model = req.session.model(model)
796 return Model.unlink(ids, req.session.eval_context(req.context))
798 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
799 has_domain = domain_id is not None and domain_id < len(args)
800 has_context = context_id is not None and context_id < len(args)
802 domain = args[domain_id] if has_domain else []
803 context = args[context_id] if has_context else {}
804 c, d = eval_context_and_domain(req.session, context, domain)
810 for i in xrange(len(args)):
811 if isinstance(args[i], web.common.nonliterals.BaseContext):
812 args[i] = req.session.eval_context(args[i])
813 if isinstance(args[i], web.common.nonliterals.BaseDomain):
814 args[i] = req.session.eval_domain(args[i])
816 return getattr(req.session.model(model), method)(*args)
818 @openerpweb.jsonrequest
819 def call(self, req, model, method, args, domain_id=None, context_id=None):
820 return self.call_common(req, model, method, args, domain_id, context_id)
822 @openerpweb.jsonrequest
823 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
824 action = self.call_common(req, model, method, args, domain_id, context_id)
825 if isinstance(action, dict) and action.get('type') != '':
826 return {'result': clean_action(req, action)}
827 return {'result': False}
829 @openerpweb.jsonrequest
830 def exec_workflow(self, req, model, id, signal):
831 r = req.session.exec_workflow(model, id, signal)
834 @openerpweb.jsonrequest
835 def default_get(self, req, model, fields):
836 Model = req.session.model(model)
837 return Model.default_get(fields, req.session.eval_context(req.context))
839 @openerpweb.jsonrequest
840 def name_search(self, req, model, search_str, domain=[], context={}):
841 m = req.session.model(model)
842 r = m.name_search(search_str+'%', domain, '=ilike', context)
845 class DataGroup(openerpweb.Controller):
846 _cp_path = "/web/group"
847 @openerpweb.jsonrequest
848 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
849 Model = req.session.model(model)
850 context, domain = eval_context_and_domain(req.session, req.context, domain)
852 return Model.read_group(
853 domain or [], fields, group_by_fields, 0, False,
854 dict(context, group_by=group_by_fields), sort or False)
856 class View(openerpweb.Controller):
857 _cp_path = "/web/view"
859 def fields_view_get(self, req, model, view_id, view_type,
860 transform=True, toolbar=False, submenu=False):
861 Model = req.session.model(model)
862 context = req.session.eval_context(req.context)
863 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
864 # todo fme?: check that we should pass the evaluated context here
865 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
866 if toolbar and transform:
867 self.process_toolbar(req, fvg['toolbar'])
870 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
871 # depending on how it feels, xmlrpclib.ServerProxy can translate
872 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
873 # enjoy unicode strings which can not be trivially converted to
874 # strings, and it blows up during parsing.
876 # So ensure we fix this retardation by converting view xml back to
878 if isinstance(fvg['arch'], unicode):
879 arch = fvg['arch'].encode('utf-8')
884 evaluation_context = session.evaluation_context(context or {})
885 xml = self.transform_view(arch, session, evaluation_context)
887 xml = ElementTree.fromstring(arch)
888 fvg['arch'] = web.common.xml2json.Xml2Json.convert_element(xml, preserve_whitespaces)
890 for field in fvg['fields'].itervalues():
891 if field.get('views'):
892 for view in field["views"].itervalues():
893 self.process_view(session, view, None, transform)
894 if field.get('domain'):
895 field["domain"] = parse_domain(field["domain"], session)
896 if field.get('context'):
897 field["context"] = parse_context(field["context"], session)
899 def process_toolbar(self, req, toolbar):
901 The toolbar is a mapping of section_key: [action_descriptor]
903 We need to clean all those actions in order to ensure correct
906 for actions in toolbar.itervalues():
907 for action in actions:
908 if 'context' in action:
909 action['context'] = parse_context(
910 action['context'], req.session)
911 if 'domain' in action:
912 action['domain'] = parse_domain(
913 action['domain'], req.session)
915 @openerpweb.jsonrequest
916 def add_custom(self, req, view_id, arch):
917 CustomView = req.session.model('ir.ui.view.custom')
919 'user_id': req.session._uid,
922 }, req.session.eval_context(req.context))
923 return {'result': True}
925 @openerpweb.jsonrequest
926 def undo_custom(self, req, view_id, reset=False):
927 CustomView = req.session.model('ir.ui.view.custom')
928 context = req.session.eval_context(req.context)
929 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
930 0, False, False, context)
933 CustomView.unlink(vcustom, context)
935 CustomView.unlink([vcustom[0]], context)
936 return {'result': True}
937 return {'result': False}
939 def transform_view(self, view_string, session, context=None):
940 # transform nodes on the fly via iterparse, instead of
941 # doing it statically on the parsing result
942 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
944 for event, elem in parser:
948 self.parse_domains_and_contexts(elem, session)
951 def parse_domains_and_contexts(self, elem, session):
952 """ Converts domains and contexts from the view into Python objects,
953 either literals if they can be parsed by literal_eval or a special
954 placeholder object if the domain or context refers to free variables.
956 :param elem: the current node being parsed
957 :type param: xml.etree.ElementTree.Element
958 :param session: OpenERP session object, used to store and retrieve
960 :type session: openerpweb.openerpweb.OpenERPSession
962 for el in ['domain', 'filter_domain']:
963 domain = elem.get(el, '').strip()
965 elem.set(el, parse_domain(domain, session))
966 elem.set(el + '_string', domain)
967 for el in ['context', 'default_get']:
968 context_string = elem.get(el, '').strip()
970 elem.set(el, parse_context(context_string, session))
971 elem.set(el + '_string', context_string)
973 @openerpweb.jsonrequest
974 def load(self, req, model, view_id, view_type, toolbar=False):
975 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
977 def parse_domain(domain, session):
978 """ Parses an arbitrary string containing a domain, transforms it
979 to either a literal domain or a :class:`web.common.nonliterals.Domain`
981 :param domain: the domain to parse, if the domain is not a string it
982 is assumed to be a literal domain and is returned as-is
983 :param session: Current OpenERP session
984 :type session: openerpweb.openerpweb.OpenERPSession
986 if not isinstance(domain, (str, unicode)):
989 return ast.literal_eval(domain)
992 return web.common.nonliterals.Domain(session, domain)
994 def parse_context(context, session):
995 """ Parses an arbitrary string containing a context, transforms it
996 to either a literal context or a :class:`web.common.nonliterals.Context`
998 :param context: the context to parse, if the context is not a string it
999 is assumed to be a literal domain and is returned as-is
1000 :param session: Current OpenERP session
1001 :type session: openerpweb.openerpweb.OpenERPSession
1003 if not isinstance(context, (str, unicode)):
1006 return ast.literal_eval(context)
1008 return web.common.nonliterals.Context(session, context)
1010 class ListView(View):
1011 _cp_path = "/web/listview"
1013 def process_colors(self, view, row, context):
1014 colors = view['arch']['attrs'].get('colors')
1021 for pair in colors.split(';')
1022 if eval(pair.split(':')[1], dict(context, **row))
1027 elif len(color) == 1:
1031 class TreeView(View):
1032 _cp_path = "/web/treeview"
1034 @openerpweb.jsonrequest
1035 def action(self, req, model, id):
1036 return load_actions_from_ir_values(
1037 req,'action', 'tree_but_open',[(model, id)],
1040 class SearchView(View):
1041 _cp_path = "/web/searchview"
1043 @openerpweb.jsonrequest
1044 def load(self, req, model, view_id):
1045 fields_view = self.fields_view_get(req, model, view_id, 'search')
1046 return {'fields_view': fields_view}
1048 @openerpweb.jsonrequest
1049 def fields_get(self, req, model):
1050 Model = req.session.model(model)
1051 fields = Model.fields_get(False, req.session.eval_context(req.context))
1052 for field in fields.values():
1053 # shouldn't convert the views too?
1054 if field.get('domain'):
1055 field["domain"] = parse_domain(field["domain"], req.session)
1056 if field.get('context'):
1057 field["context"] = parse_context(field["context"], req.session)
1058 return {'fields': fields}
1060 @openerpweb.jsonrequest
1061 def get_filters(self, req, model):
1062 Model = req.session.model("ir.filters")
1063 filters = Model.get_filters(model)
1064 for filter in filters:
1065 filter["context"] = req.session.eval_context(parse_context(filter["context"], req.session))
1066 filter["domain"] = req.session.eval_domain(parse_domain(filter["domain"], req.session))
1069 @openerpweb.jsonrequest
1070 def save_filter(self, req, model, name, context_to_save, domain):
1071 Model = req.session.model("ir.filters")
1072 ctx = web.common.nonliterals.CompoundContext(context_to_save)
1073 ctx.session = req.session
1074 ctx = ctx.evaluate()
1075 domain = web.common.nonliterals.CompoundDomain(domain)
1076 domain.session = req.session
1077 domain = domain.evaluate()
1078 uid = req.session._uid
1079 context = req.session.eval_context(req.context)
1080 to_return = Model.create_or_replace({"context": ctx,
1088 @openerpweb.jsonrequest
1089 def add_to_dashboard(self, req, menu_id, action_id, context_to_save, domain, view_mode, name=''):
1090 ctx = web.common.nonliterals.CompoundContext(context_to_save)
1091 ctx.session = req.session
1092 ctx = ctx.evaluate()
1093 domain = web.common.nonliterals.CompoundDomain(domain)
1094 domain.session = req.session
1095 domain = domain.evaluate()
1097 dashboard_action = load_actions_from_ir_values(req, 'action', 'tree_but_open',
1098 [('ir.ui.menu', menu_id)], False)
1099 if dashboard_action:
1100 action = dashboard_action[0][2]
1101 if action['res_model'] == 'board.board' and action['views'][0][1] == 'form':
1102 # Maybe should check the content instead of model board.board ?
1103 view_id = action['views'][0][0]
1104 board = req.session.model(action['res_model']).fields_view_get(view_id, 'form')
1105 if board and 'arch' in board:
1106 xml = ElementTree.fromstring(board['arch'])
1107 column = xml.find('./board/column')
1109 new_action = ElementTree.Element('action', {
1110 'name' : str(action_id),
1112 'view_mode' : view_mode,
1113 'context' : str(ctx),
1114 'domain' : str(domain)
1116 column.insert(0, new_action)
1117 arch = ElementTree.tostring(xml, 'utf-8')
1118 return req.session.model('ir.ui.view.custom').create({
1119 'user_id': req.session._uid,
1122 }, req.session.eval_context(req.context))
1126 class Binary(openerpweb.Controller):
1127 _cp_path = "/web/binary"
1129 @openerpweb.httprequest
1130 def image(self, req, model, id, field, **kw):
1131 Model = req.session.model(model)
1132 context = req.session.eval_context(req.context)
1136 res = Model.default_get([field], context).get(field)
1138 res = Model.read([int(id)], [field], context)[0].get(field)
1139 image_data = base64.b64decode(res)
1140 except (TypeError, xmlrpclib.Fault):
1141 image_data = self.placeholder(req)
1142 return req.make_response(image_data, [
1143 ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
1144 def placeholder(self, req):
1145 addons_path = openerpweb.addons_manifest['web']['addons_path']
1146 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1148 @openerpweb.httprequest
1149 def saveas(self, req, model, id, field, fieldname, **kw):
1150 Model = req.session.model(model)
1151 context = req.session.eval_context(req.context)
1153 res = Model.read([int(id)], [field, fieldname], context)[0]
1155 res = Model.default_get([field, fieldname], context)
1156 filecontent = base64.b64decode(res.get(field, ''))
1158 return req.not_found()
1160 filename = '%s_%s' % (model.replace('.', '_'), id)
1162 filename = res.get(fieldname, '') or filename
1163 return req.make_response(filecontent,
1164 [('Content-Type', 'application/octet-stream'),
1165 ('Content-Disposition', 'attachment; filename=' + filename)])
1167 @openerpweb.httprequest
1168 def upload(self, req, callback, ufile):
1169 # TODO: might be useful to have a configuration flag for max-length file uploads
1171 out = """<script language="javascript" type="text/javascript">
1172 var win = window.top.window,
1174 if (typeof(callback) === 'function') {
1175 callback.apply(this, %s);
1177 win.jQuery('#oe_notification', win.document).notify('create', {
1178 title: "Ajax File Upload",
1179 text: "Could not find callback"
1184 args = [len(data), ufile.filename,
1185 ufile.content_type, base64.b64encode(data)]
1186 except Exception, e:
1187 args = [False, e.message]
1188 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1190 @openerpweb.httprequest
1191 def upload_attachment(self, req, callback, model, id, ufile):
1192 context = req.session.eval_context(req.context)
1193 Model = req.session.model('ir.attachment')
1195 out = """<script language="javascript" type="text/javascript">
1196 var win = window.top.window,
1198 if (typeof(callback) === 'function') {
1199 callback.call(this, %s);
1202 attachment_id = Model.create({
1203 'name': ufile.filename,
1204 'datas': base64.encodestring(ufile.read()),
1209 'filename': ufile.filename,
1212 except Exception, e:
1213 args = { 'error': e.message }
1214 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1216 class Action(openerpweb.Controller):
1217 _cp_path = "/web/action"
1219 @openerpweb.jsonrequest
1220 def load(self, req, action_id, do_not_eval=False):
1221 Actions = req.session.model('ir.actions.actions')
1223 context = req.session.eval_context(req.context)
1224 action_type = Actions.read([action_id], ['type'], context)
1227 if action_type[0]['type'] == 'ir.actions.report.xml':
1228 ctx.update({'bin_size': True})
1230 action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
1232 value = clean_action(req, action[0], do_not_eval)
1233 return {'result': value}
1235 @openerpweb.jsonrequest
1236 def run(self, req, action_id):
1237 return clean_action(req, req.session.model('ir.actions.server').run(
1238 [action_id], req.session.eval_context(req.context)))
1241 _cp_path = "/web/export"
1243 @openerpweb.jsonrequest
1244 def formats(self, req):
1245 """ Returns all valid export formats
1247 :returns: for each export format, a pair of identifier and printable name
1248 :rtype: [(str, str)]
1252 for path, controller in openerpweb.controllers_path.iteritems()
1253 if path.startswith(self._cp_path)
1254 if hasattr(controller, 'fmt')
1255 ], key=operator.itemgetter(1))
1257 def fields_get(self, req, model):
1258 Model = req.session.model(model)
1259 fields = Model.fields_get(False, req.session.eval_context(req.context))
1262 @openerpweb.jsonrequest
1263 def get_fields(self, req, model, prefix='', parent_name= '',
1264 import_compat=True, parent_field_type=None,
1267 if import_compat and parent_field_type == "many2one":
1270 fields = self.fields_get(req, model)
1273 fields.pop('id', None)
1275 fields['.id'] = fields.pop('id', {'string': 'ID'})
1277 fields_sequence = sorted(fields.iteritems(),
1278 key=lambda field: field[1].get('string', ''))
1281 for field_name, field in fields_sequence:
1282 if import_compat and (exclude and field_name in exclude):
1284 if import_compat and field.get('readonly'):
1285 # If none of the field's states unsets readonly, skip the field
1286 if all(dict(attrs).get('readonly', True)
1287 for attrs in field.get('states', {}).values()):
1290 id = prefix + (prefix and '/'or '') + field_name
1291 name = parent_name + (parent_name and '/' or '') + field['string']
1292 record = {'id': id, 'string': name,
1293 'value': id, 'children': False,
1294 'field_type': field.get('type'),
1295 'required': field.get('required'),
1296 'relation_field': field.get('relation_field')}
1297 records.append(record)
1299 if len(name.split('/')) < 3 and 'relation' in field:
1300 ref = field.pop('relation')
1301 record['value'] += '/id'
1302 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1304 if not import_compat or field['type'] == 'one2many':
1305 # m2m field in import_compat is childless
1306 record['children'] = True
1310 @openerpweb.jsonrequest
1311 def namelist(self,req, model, export_id):
1312 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1313 export = req.session.model("ir.exports").read([export_id])[0]
1314 export_fields_list = req.session.model("ir.exports.line").read(
1315 export['export_fields'])
1317 fields_data = self.fields_info(
1318 req, model, map(operator.itemgetter('name'), export_fields_list))
1321 {'name': field['name'], 'label': fields_data[field['name']]}
1322 for field in export_fields_list
1325 def fields_info(self, req, model, export_fields):
1327 fields = self.fields_get(req, model)
1329 # To make fields retrieval more efficient, fetch all sub-fields of a
1330 # given field at the same time. Because the order in the export list is
1331 # arbitrary, this requires ordering all sub-fields of a given field
1332 # together so they can be fetched at the same time
1334 # Works the following way:
1335 # * sort the list of fields to export, the default sorting order will
1336 # put the field itself (if present, for xmlid) and all of its
1337 # sub-fields right after it
1338 # * then, group on: the first field of the path (which is the same for
1339 # a field and for its subfields and the length of splitting on the
1340 # first '/', which basically means grouping the field on one side and
1341 # all of the subfields on the other. This way, we have the field (for
1342 # the xmlid) with length 1, and all of the subfields with the same
1343 # base but a length "flag" of 2
1344 # * if we have a normal field (length 1), just add it to the info
1345 # mapping (with its string) as-is
1346 # * otherwise, recursively call fields_info via graft_subfields.
1347 # all graft_subfields does is take the result of fields_info (on the
1348 # field's model) and prepend the current base (current field), which
1349 # rebuilds the whole sub-tree for the field
1351 # result: because we're not fetching the fields_get for half the
1352 # database models, fetching a namelist with a dozen fields (including
1353 # relational data) falls from ~6s to ~300ms (on the leads model).
1354 # export lists with no sub-fields (e.g. import_compatible lists with
1355 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1356 # there's a single fields_get to execute)
1357 for (base, length), subfields in itertools.groupby(
1358 sorted(export_fields),
1359 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1360 subfields = list(subfields)
1362 # subfields is a seq of $base/*rest, and not loaded yet
1363 info.update(self.graft_subfields(
1364 req, fields[base]['relation'], base, fields[base]['string'],
1368 info[base] = fields[base]['string']
1372 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1373 export_fields = [field.split('/', 1)[1] for field in fields]
1375 (prefix + '/' + k, prefix_string + '/' + v)
1376 for k, v in self.fields_info(req, model, export_fields).iteritems())
1378 #noinspection PyPropertyDefinition
1380 def content_type(self):
1381 """ Provides the format's content type """
1382 raise NotImplementedError()
1384 def filename(self, base):
1385 """ Creates a valid filename for the format (with extension) from the
1386 provided base name (exension-less)
1388 raise NotImplementedError()
1390 def from_data(self, fields, rows):
1391 """ Conversion method from OpenERP's export data to whatever the
1392 current export class outputs
1394 :params list fields: a list of fields to export
1395 :params list rows: a list of records to export
1399 raise NotImplementedError()
1401 @openerpweb.httprequest
1402 def index(self, req, data, token):
1403 model, fields, ids, domain, import_compat = \
1404 operator.itemgetter('model', 'fields', 'ids', 'domain',
1406 simplejson.loads(data))
1408 context = req.session.eval_context(req.context)
1409 Model = req.session.model(model)
1410 ids = ids or Model.search(domain, 0, False, False, context)
1412 field_names = map(operator.itemgetter('name'), fields)
1413 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1416 columns_headers = field_names
1418 columns_headers = [val['label'].strip() for val in fields]
1421 return req.make_response(self.from_data(columns_headers, import_data),
1422 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1423 ('Content-Type', self.content_type)],
1424 cookies={'fileToken': int(token)})
1426 class CSVExport(Export):
1427 _cp_path = '/web/export/csv'
1428 fmt = ('csv', 'CSV')
1431 def content_type(self):
1432 return 'text/csv;charset=utf8'
1434 def filename(self, base):
1435 return base + '.csv'
1437 def from_data(self, fields, rows):
1439 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1441 writer.writerow(fields)
1446 if isinstance(d, basestring):
1447 d = d.replace('\n',' ').replace('\t',' ')
1449 d = d.encode('utf-8')
1452 if d is False: d = None
1454 writer.writerow(row)
1461 class ExcelExport(Export):
1462 _cp_path = '/web/export/xls'
1463 fmt = ('xls', 'Excel')
1466 def content_type(self):
1467 return 'application/vnd.ms-excel'
1469 def filename(self, base):
1470 return base + '.xls'
1472 def from_data(self, fields, rows):
1475 workbook = xlwt.Workbook()
1476 worksheet = workbook.add_sheet('Sheet 1')
1478 for i, fieldname in enumerate(fields):
1479 worksheet.write(0, i, str(fieldname))
1480 worksheet.col(i).width = 8000 # around 220 pixels
1482 style = xlwt.easyxf('align: wrap yes')
1484 for row_index, row in enumerate(rows):
1485 for cell_index, cell_value in enumerate(row):
1486 if isinstance(cell_value, basestring):
1487 cell_value = re.sub("\r", " ", cell_value)
1488 if cell_value is False: cell_value = None
1489 worksheet.write(row_index + 1, cell_index, cell_value, style)
1498 class Reports(View):
1499 _cp_path = "/web/report"
1500 POLLING_DELAY = 0.25
1502 'doc': 'application/vnd.ms-word',
1503 'html': 'text/html',
1504 'odt': 'application/vnd.oasis.opendocument.text',
1505 'pdf': 'application/pdf',
1506 'sxw': 'application/vnd.sun.xml.writer',
1507 'xls': 'application/vnd.ms-excel',
1510 @openerpweb.httprequest
1511 def index(self, req, action, token):
1512 action = simplejson.loads(action)
1514 report_srv = req.session.proxy("report")
1515 context = req.session.eval_context(
1516 web.common.nonliterals.CompoundContext(
1517 req.context or {}, action[ "context"]))
1520 report_ids = context["active_ids"]
1521 if 'report_type' in action:
1522 report_data['report_type'] = action['report_type']
1523 if 'datas' in action:
1524 if 'ids' in action['datas']:
1525 report_ids = action['datas'].pop('ids')
1526 report_data.update(action['datas'])
1528 report_id = report_srv.report(
1529 req.session._db, req.session._uid, req.session._password,
1530 action["report_name"], report_ids,
1531 report_data, context)
1533 report_struct = None
1535 report_struct = report_srv.report_get(
1536 req.session._db, req.session._uid, req.session._password, report_id)
1537 if report_struct["state"]:
1540 time.sleep(self.POLLING_DELAY)
1542 report = base64.b64decode(report_struct['result'])
1543 if report_struct.get('code') == 'zlib':
1544 report = zlib.decompress(report)
1545 report_mimetype = self.TYPES_MAPPING.get(
1546 report_struct['format'], 'octet-stream')
1547 return req.make_response(report,
1549 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
1550 ('Content-Type', report_mimetype),
1551 ('Content-Length', len(report))],
1552 cookies={'fileToken': int(token)})
1555 _cp_path = "/web/import"
1557 def fields_get(self, req, model):
1558 Model = req.session.model(model)
1559 fields = Model.fields_get(False, req.session.eval_context(req.context))
1562 @openerpweb.httprequest
1563 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1565 data = list(csv.reader(
1566 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1567 except csv.Error, e:
1569 return '<script>window.top.%s(%s);</script>' % (
1570 jsonp, simplejson.dumps({'error': {
1571 'message': 'Error parsing CSV file: %s' % e,
1572 # decodes each byte to a unicode character, which may or
1573 # may not be printable, but decoding will succeed.
1574 # Otherwise simplejson will try to decode the `str` using
1575 # utf-8, which is very likely to blow up on characters out
1576 # of the ascii range (in range [128, 256))
1577 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1580 return '<script>window.top.%s(%s);</script>' % (
1581 jsonp, simplejson.dumps(
1582 {'records': data[:10]}, encoding=csvcode))
1583 except UnicodeDecodeError:
1584 return '<script>window.top.%s(%s);</script>' % (
1585 jsonp, simplejson.dumps({
1586 'message': u"Failed to decode CSV file using encoding %s, "
1587 u"try switching to a different encoding" % csvcode
1590 @openerpweb.httprequest
1591 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1593 modle_obj = req.session.model(model)
1594 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1595 simplejson.loads(meta))
1598 if not (csvdel and len(csvdel) == 1):
1599 error = u"The CSV delimiter must be a single character"
1601 if not indices and fields:
1602 error = u"You must select at least one field to import"
1605 return '<script>window.top.%s(%s);</script>' % (
1606 jsonp, simplejson.dumps({'error': {'message': error}}))
1608 # skip ignored records
1609 data_record = itertools.islice(
1610 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1613 # if only one index, itemgetter will return an atom rather than a tuple
1614 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1615 else: mapper = operator.itemgetter(*indices)
1620 # decode each data row
1622 [record.decode(csvcode) for record in row]
1623 for row in itertools.imap(mapper, data_record)
1624 # don't insert completely empty rows (can happen due to fields
1625 # filtering in case of e.g. o2m content rows)
1628 except UnicodeDecodeError:
1629 error = u"Failed to decode CSV file using encoding %s" % csvcode
1630 except csv.Error, e:
1631 error = u"Could not process CSV file: %s" % e
1633 # If the file contains nothing,
1635 error = u"File to import is empty"
1637 return '<script>window.top.%s(%s);</script>' % (
1638 jsonp, simplejson.dumps({'error': {'message': error}}))
1641 (code, record, message, _nope) = modle_obj.import_data(
1642 fields, data, 'init', '', False,
1643 req.session.eval_context(req.context))
1644 except xmlrpclib.Fault, e:
1645 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1646 return '<script>window.top.%s(%s);</script>' % (
1647 jsonp, simplejson.dumps({'error':error}))
1650 return '<script>window.top.%s(%s);</script>' % (
1651 jsonp, simplejson.dumps({'success':True}))
1653 msg = u"Error during import: %s\n\nTrying to import record %r" % (
1655 return '<script>window.top.%s(%s);</script>' % (
1656 jsonp, simplejson.dumps({'error': {'message':msg}}))