1 # -*- coding: utf-8 -*-
16 from xml.etree import ElementTree
17 from cStringIO import StringIO
19 import babel.messages.pofile
27 openerpweb = web.common.http
29 #----------------------------------------------------------
30 # OpenERP Web web Controllers
31 #----------------------------------------------------------
34 def concat_xml(file_list):
35 """Concatenate xml files
36 return (concat,timestamp)
37 concat: concatenation of file content
38 timestamp: max(os.path.getmtime of file_list)
45 for fname in file_list:
46 ftime = os.path.getmtime(fname)
47 if ftime > files_timestamp:
48 files_timestamp = ftime
50 xml = ElementTree.parse(fname).getroot()
53 root = ElementTree.Element(xml.tag)
54 #elif root.tag != xml.tag:
55 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
57 for child in xml.getchildren():
59 return ElementTree.tostring(root, 'utf-8'), files_timestamp
62 def concat_files(file_list, reader=None, intersperse=""):
63 """ Concatenate file content
64 return (concat,timestamp)
65 concat: concatenation of file content, read by `reader`
66 timestamp: max(os.path.getmtime of file_list)
78 for fname in file_list:
79 ftime = os.path.getmtime(fname)
80 if ftime > files_timestamp:
81 files_timestamp = ftime
83 files_content.append(reader(fname))
84 files_concat = intersperse.join(files_content)
85 return files_concat,files_timestamp
87 html_template = """<!DOCTYPE html>
88 <html style="height: 100%%">
90 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
91 <title>OpenERP</title>
92 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
95 <script type="text/javascript">
97 var s = new openerp.init(%(modules)s);
106 class WebClient(openerpweb.Controller):
107 _cp_path = "/web/webclient"
109 def server_wide_modules(self, req):
110 addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest]
113 def manifest_glob(self, req, addons, key):
115 addons = self.server_wide_modules(req)
117 addons = addons.split(',')
120 manifest = openerpweb.addons_manifest.get(addon, None)
123 # ensure does not ends with /
124 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
125 globlist = manifest.get(key, [])
126 for pattern in globlist:
127 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
128 r.append( (path, path[len(addons_path):]))
131 def manifest_list(self, req, mods, extension):
133 path = '/web/webclient/' + extension
135 path += '?mods=' + mods
137 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in self.manifest_glob(req, mods, extension)]
139 @openerpweb.jsonrequest
140 def csslist(self, req, mods=None):
141 return self.manifest_list(req, mods, 'css')
143 @openerpweb.jsonrequest
144 def jslist(self, req, mods=None):
145 return self.manifest_list(req, mods, 'js')
147 @openerpweb.jsonrequest
148 def qweblist(self, req, mods=None):
149 return self.manifest_list(req, mods, 'qweb')
151 @openerpweb.httprequest
152 def css(self, req, mods=None):
154 files = list(self.manifest_glob(req, mods, 'css'))
155 file_map = dict(files)
157 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
158 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://)""", re.U)
162 """read the a css file and absolutify all relative uris"""
166 web_path = file_map[f]
167 web_dir = os.path.dirname(web_path)
171 r"""@import \1%s/""" % (web_dir,),
177 r"""url(\1%s/""" % (web_dir,),
182 content,timestamp = concat_files((f[0] for f in files), reader)
183 # TODO use timestamp to set Last mofified date and E-tag
184 return req.make_response(content, [('Content-Type', 'text/css')])
186 @openerpweb.httprequest
187 def js(self, req, mods=None):
188 files = [f[0] for f in self.manifest_glob(req, mods, 'js')]
189 content, timestamp = concat_files(files, intersperse=';')
190 # TODO use timestamp to set Last mofified date and E-tag
191 return req.make_response(content, [('Content-Type', 'application/javascript')])
193 @openerpweb.httprequest
194 def qweb(self, req, mods=None):
195 files = [f[0] for f in self.manifest_glob(req, mods, 'qweb')]
196 content,timestamp = concat_xml(files)
197 # TODO use timestamp to set Last mofified date and E-tag
198 return req.make_response(content, [('Content-Type', 'text/xml')])
201 @openerpweb.httprequest
202 def home(self, req, s_action=None, **kw):
203 js = "\n ".join('<script type="text/javascript" src="%s"></script>'%i for i in self.manifest_list(req, None, 'js'))
204 css = "\n ".join('<link rel="stylesheet" href="%s">'%i for i in self.manifest_list(req, None, 'css'))
206 r = html_template % {
209 'modules': simplejson.dumps(self.server_wide_modules(req)),
210 'init': 'new s.web.WebClient().replace($("body"));',
214 @openerpweb.httprequest
215 def login(self, req, db, login, key):
216 req.session.authenticate(db, login, key, {})
217 redirect = werkzeug.utils.redirect('/web/webclient/home', 303)
218 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
219 redirect.set_cookie('session0|session_id', cookie_val)
222 @openerpweb.jsonrequest
223 def translations(self, req, mods, lang):
224 lang_model = req.session.model('res.lang')
225 ids = lang_model.search([("code", "=", lang)])
227 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
228 "grouping", "decimal_point", "thousands_sep"])
232 if lang.count("_") > 0:
236 langs = lang.split(separator)
237 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
240 for addon_name in mods:
241 transl = {"messages":[]}
242 transs[addon_name] = transl
244 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
245 f_name = os.path.join(addons_path, addon_name, "po", l + ".po")
246 if not os.path.exists(f_name):
249 with open(f_name) as t_file:
250 po = babel.messages.pofile.read_po(t_file)
254 if x.id and x.string:
255 transl["messages"].append({'id': x.id, 'string': x.string})
256 return {"modules": transs,
257 "lang_parameters": lang_obj}
259 @openerpweb.jsonrequest
260 def version_info(self, req):
262 "version": web.common.release.version
265 class Proxy(openerpweb.Controller):
266 _cp_path = '/web/proxy'
268 @openerpweb.jsonrequest
269 def load(self, req, path):
270 """ Proxies an HTTP request through a JSON request.
272 It is strongly recommended to not request binary files through this,
273 as the result will be a binary data blob as well.
275 :param req: OpenERP request
276 :param path: actual request path
277 :return: file content
279 from werkzeug.test import Client
280 from werkzeug.wrappers import BaseResponse
282 return Client(req.httprequest.app, BaseResponse).get(path).data
284 class Database(openerpweb.Controller):
285 _cp_path = "/web/database"
287 @openerpweb.jsonrequest
288 def get_list(self, req):
289 proxy = req.session.proxy("db")
291 h = req.httprequest.headers['Host'].split(':')[0]
293 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
294 dbs = [i for i in dbs if re.match(r, i)]
295 return {"db_list": dbs}
297 @openerpweb.jsonrequest
298 def progress(self, req, password, id):
299 return req.session.proxy('db').get_progress(password, id)
301 @openerpweb.jsonrequest
302 def create(self, req, fields):
304 params = dict(map(operator.itemgetter('name', 'value'), fields))
306 params['super_admin_pwd'],
308 bool(params.get('demo_data')),
310 params['create_admin_pwd']
314 return req.session.proxy("db").create(*create_attrs)
315 except xmlrpclib.Fault, e:
316 if e.faultCode and isinstance(e.faultCode, str)\
317 and e.faultCode.split(':')[0] == 'AccessDenied':
318 return {'error': e.faultCode, 'title': 'Database creation error'}
320 'error': "Could not create database '%s': %s" % (
321 params['db_name'], e.faultString),
322 'title': 'Database creation error'
325 @openerpweb.jsonrequest
326 def drop(self, req, fields):
327 password, db = operator.itemgetter(
328 'drop_pwd', 'drop_db')(
329 dict(map(operator.itemgetter('name', 'value'), fields)))
332 return req.session.proxy("db").drop(password, db)
333 except xmlrpclib.Fault, e:
334 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
335 return {'error': e.faultCode, 'title': 'Drop Database'}
336 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
338 @openerpweb.httprequest
339 def backup(self, req, backup_db, backup_pwd, token):
341 db_dump = base64.b64decode(
342 req.session.proxy("db").dump(backup_pwd, backup_db))
343 return req.make_response(db_dump,
344 [('Content-Type', 'application/octet-stream; charset=binary'),
345 ('Content-Disposition', 'attachment; filename="' + backup_db + '.dump"')],
346 {'fileToken': int(token)}
348 except xmlrpclib.Fault, e:
349 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
350 return 'Backup Database|' + e.faultCode
351 return 'Backup Database|Could not generate database backup'
353 @openerpweb.httprequest
354 def restore(self, req, db_file, restore_pwd, new_db):
356 data = base64.b64encode(db_file.read())
357 req.session.proxy("db").restore(restore_pwd, new_db, data)
359 except xmlrpclib.Fault, e:
360 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
361 raise Exception("AccessDenied")
363 @openerpweb.jsonrequest
364 def change_password(self, req, fields):
365 old_password, new_password = operator.itemgetter(
366 'old_pwd', 'new_pwd')(
367 dict(map(operator.itemgetter('name', 'value'), fields)))
369 return req.session.proxy("db").change_admin_password(old_password, new_password)
370 except xmlrpclib.Fault, e:
371 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
372 return {'error': e.faultCode, 'title': 'Change Password'}
373 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
375 class Session(openerpweb.Controller):
376 _cp_path = "/web/session"
378 def session_info(self, req):
380 "session_id": req.session_id,
381 "uid": req.session._uid,
382 "context": req.session.get_context() if req.session._uid else {},
383 "db": req.session._db,
384 "login": req.session._login,
385 "openerp_entreprise": req.session.openerp_entreprise(),
388 @openerpweb.jsonrequest
389 def get_session_info(self, req):
390 return self.session_info(req)
392 @openerpweb.jsonrequest
393 def authenticate(self, req, db, login, password, base_location=None):
394 wsgienv = req.httprequest.environ
395 release = web.common.release
397 base_location=base_location,
398 HTTP_HOST=wsgienv['HTTP_HOST'],
399 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
400 user_agent="%s / %s" % (release.name, release.version),
402 req.session.authenticate(db, login, password, env)
404 return self.session_info(req)
406 @openerpweb.jsonrequest
407 def change_password (self,req,fields):
408 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
409 dict(map(operator.itemgetter('name', 'value'), fields)))
410 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
411 return {'error':'All passwords have to be filled.','title': 'Change Password'}
412 if new_password != confirm_password:
413 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
415 if req.session.model('res.users').change_password(
416 old_password, new_password):
417 return {'new_password':new_password}
419 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
420 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
422 @openerpweb.jsonrequest
423 def sc_list(self, req):
424 return req.session.model('ir.ui.view_sc').get_sc(
425 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
427 @openerpweb.jsonrequest
428 def get_lang_list(self, req):
431 'lang_list': (req.session.proxy("db").list_lang() or []),
435 return {"error": e, "title": "Languages"}
437 @openerpweb.jsonrequest
438 def modules(self, req):
439 # Compute available candidates module
440 loadable = openerpweb.addons_manifest
441 loaded = req.config.server_wide_modules
442 candidates = [mod for mod in loadable if mod not in loaded]
444 # Compute active true modules that might be on the web side only
445 active = set(name for name in candidates
446 if openerpweb.addons_manifest[name].get('active'))
448 # Retrieve database installed modules
449 Modules = req.session.model('ir.module.module')
450 installed = set(module['name'] for module in Modules.search_read(
451 [('state','=','installed'), ('name','in', candidates)], ['name']))
454 return list(active | installed)
456 @openerpweb.jsonrequest
457 def eval_domain_and_context(self, req, contexts, domains,
459 """ Evaluates sequences of domains and contexts, composing them into
460 a single context, domain or group_by sequence.
462 :param list contexts: list of contexts to merge together. Contexts are
463 evaluated in sequence, all previous contexts
464 are part of their own evaluation context
465 (starting at the session context).
466 :param list domains: list of domains to merge together. Domains are
467 evaluated in sequence and appended to one another
468 (implicit AND), their evaluation domain is the
469 result of merging all contexts.
470 :param list group_by_seq: list of domains (which may be in a different
471 order than the ``contexts`` parameter),
472 evaluated in sequence, their ``'group_by'``
473 key is extracted if they have one.
478 the global context created by merging all of
482 the concatenation of all domains
485 a list of fields to group by, potentially empty (in which case
486 no group by should be performed)
488 context, domain = eval_context_and_domain(req.session,
489 web.common.nonliterals.CompoundContext(*(contexts or [])),
490 web.common.nonliterals.CompoundDomain(*(domains or [])))
492 group_by_sequence = []
493 for candidate in (group_by_seq or []):
494 ctx = req.session.eval_context(candidate, context)
495 group_by = ctx.get('group_by')
498 elif isinstance(group_by, basestring):
499 group_by_sequence.append(group_by)
501 group_by_sequence.extend(group_by)
506 'group_by': group_by_sequence
509 @openerpweb.jsonrequest
510 def save_session_action(self, req, the_action):
512 This method store an action object in the session object and returns an integer
513 identifying that action. The method get_session_action() can be used to get
516 :param the_action: The action to save in the session.
517 :type the_action: anything
518 :return: A key identifying the saved action.
521 saved_actions = req.httpsession.get('saved_actions')
522 if not saved_actions:
523 saved_actions = {"next":0, "actions":{}}
524 req.httpsession['saved_actions'] = saved_actions
525 # we don't allow more than 10 stored actions
526 if len(saved_actions["actions"]) >= 10:
527 del saved_actions["actions"][min(saved_actions["actions"].keys())]
528 key = saved_actions["next"]
529 saved_actions["actions"][key] = the_action
530 saved_actions["next"] = key + 1
533 @openerpweb.jsonrequest
534 def get_session_action(self, req, key):
536 Gets back a previously saved action. This method can return None if the action
537 was saved since too much time (this case should be handled in a smart way).
539 :param key: The key given by save_session_action()
541 :return: The saved action or None.
544 saved_actions = req.httpsession.get('saved_actions')
545 if not saved_actions:
547 return saved_actions["actions"].get(key)
549 @openerpweb.jsonrequest
550 def check(self, req):
551 req.session.assert_valid()
554 def eval_context_and_domain(session, context, domain=None):
555 e_context = session.eval_context(context)
556 # should we give the evaluated context as an evaluation context to the domain?
557 e_domain = session.eval_domain(domain or [])
559 return e_context, e_domain
561 def load_actions_from_ir_values(req, key, key2, models, meta):
562 context = req.session.eval_context(req.context)
563 Values = req.session.model('ir.values')
564 actions = Values.get(key, key2, models, meta, context)
566 return [(id, name, clean_action(req, action))
567 for id, name, action in actions]
569 def clean_action(req, action, do_not_eval=False):
570 action.setdefault('flags', {})
572 context = req.session.eval_context(req.context)
573 eval_ctx = req.session.evaluation_context(context)
576 # values come from the server, we can just eval them
577 if isinstance(action.get('context'), basestring):
578 action['context'] = eval( action['context'], eval_ctx ) or {}
580 if isinstance(action.get('domain'), basestring):
581 action['domain'] = eval( action['domain'], eval_ctx ) or []
583 if 'context' in action:
584 action['context'] = parse_context(action['context'], req.session)
585 if 'domain' in action:
586 action['domain'] = parse_domain(action['domain'], req.session)
588 if 'type' not in action:
589 action['type'] = 'ir.actions.act_window_close'
591 if action['type'] == 'ir.actions.act_window':
592 return fix_view_modes(action)
595 # I think generate_views,fix_view_modes should go into js ActionManager
596 def generate_views(action):
598 While the server generates a sequence called "views" computing dependencies
599 between a bunch of stuff for views coming directly from the database
600 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
601 to return custom view dictionaries generated on the fly.
603 In that case, there is no ``views`` key available on the action.
605 Since the web client relies on ``action['views']``, generate it here from
606 ``view_mode`` and ``view_id``.
608 Currently handles two different cases:
610 * no view_id, multiple view_mode
611 * single view_id, single view_mode
613 :param dict action: action descriptor dictionary to generate a views key for
615 view_id = action.get('view_id', False)
616 if isinstance(view_id, (list, tuple)):
619 # providing at least one view mode is a requirement, not an option
620 view_modes = action['view_mode'].split(',')
622 if len(view_modes) > 1:
624 raise ValueError('Non-db action dictionaries should provide '
625 'either multiple view modes or a single view '
626 'mode and an optional view id.\n\n Got view '
627 'modes %r and view id %r for action %r' % (
628 view_modes, view_id, action))
629 action['views'] = [(False, mode) for mode in view_modes]
631 action['views'] = [(view_id, view_modes[0])]
633 def fix_view_modes(action):
634 """ For historical reasons, OpenERP has weird dealings in relation to
635 view_mode and the view_type attribute (on window actions):
637 * one of the view modes is ``tree``, which stands for both list views
639 * the choice is made by checking ``view_type``, which is either
640 ``form`` for a list view or ``tree`` for an actual tree view
642 This methods simply folds the view_type into view_mode by adding a
643 new view mode ``list`` which is the result of the ``tree`` view_mode
644 in conjunction with the ``form`` view_type.
646 TODO: this should go into the doc, some kind of "peculiarities" section
648 :param dict action: an action descriptor
649 :returns: nothing, the action is modified in place
651 if not action.get('views'):
652 generate_views(action)
655 for index, (id, mode) in enumerate(action['views']):
659 if id_form is not None:
660 action['views'].insert(index + 1, (id_form, 'page'))
662 if action.pop('view_type', 'form') != 'form':
666 [id, mode if mode != 'tree' else 'list']
667 for id, mode in action['views']
672 class Menu(openerpweb.Controller):
673 _cp_path = "/web/menu"
675 @openerpweb.jsonrequest
677 return {'data': self.do_load(req)}
679 def do_load(self, req):
680 """ Loads all menu items (all applications and their sub-menus).
682 :param req: A request object, with an OpenERP session attribute
683 :type req: < session -> OpenERPSession >
684 :return: the menu root
685 :rtype: dict('children': menu_nodes)
687 Menus = req.session.model('ir.ui.menu')
688 # menus are loaded fully unlike a regular tree view, cause there are
689 # less than 512 items
690 context = req.session.eval_context(req.context)
691 menu_ids = Menus.search([], 0, False, False, context)
692 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
693 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
694 menu_items.append(menu_root)
696 # make a tree using parent_id
697 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
698 for menu_item in menu_items:
699 if menu_item['parent_id']:
700 parent = menu_item['parent_id'][0]
703 if parent in menu_items_map:
704 menu_items_map[parent].setdefault(
705 'children', []).append(menu_item)
707 # sort by sequence a tree using parent_id
708 for menu_item in menu_items:
709 menu_item.setdefault('children', []).sort(
710 key=lambda x:x["sequence"])
714 @openerpweb.jsonrequest
715 def action(self, req, menu_id):
716 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
717 [('ir.ui.menu', menu_id)], False)
718 return {"action": actions}
720 class DataSet(openerpweb.Controller):
721 _cp_path = "/web/dataset"
723 @openerpweb.jsonrequest
724 def fields(self, req, model):
725 return {'fields': req.session.model(model).fields_get(False,
726 req.session.eval_context(req.context))}
728 @openerpweb.jsonrequest
729 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
730 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
731 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
733 """ Performs a search() followed by a read() (if needed) using the
734 provided search criteria
736 :param req: a JSON-RPC request object
737 :type req: openerpweb.JsonRequest
738 :param str model: the name of the model to search on
739 :param fields: a list of the fields to return in the result records
741 :param int offset: from which index should the results start being returned
742 :param int limit: the maximum number of records to return
743 :param list domain: the search domain for the query
744 :param list sort: sorting directives
745 :returns: A structure (dict) with two keys: ids (all the ids matching
746 the (domain, context) pair) and records (paginated records
747 matching fields selection set)
750 Model = req.session.model(model)
752 context, domain = eval_context_and_domain(
753 req.session, req.context, domain)
755 ids = Model.search(domain, 0, False, sort or False, context)
756 # need to fill the dataset with all ids for the (domain, context) pair,
757 # so search un-paginated and paginate manually before reading
758 paginated_ids = ids[offset:(offset + limit if limit else None)]
759 if fields and fields == ['id']:
760 # shortcut read if we only want the ids
763 'records': map(lambda id: {'id': id}, paginated_ids)
766 records = Model.read(paginated_ids, fields or False, context)
767 records.sort(key=lambda obj: ids.index(obj['id']))
774 @openerpweb.jsonrequest
775 def read(self, req, model, ids, fields=False):
776 return self.do_search_read(req, model, ids, fields)
778 @openerpweb.jsonrequest
779 def get(self, req, model, ids, fields=False):
780 return self.do_get(req, model, ids, fields)
782 def do_get(self, req, model, ids, fields=False):
783 """ Fetches and returns the records of the model ``model`` whose ids
786 The results are in the same order as the inputs, but elements may be
787 missing (if there is no record left for the id)
789 :param req: the JSON-RPC2 request object
790 :type req: openerpweb.JsonRequest
791 :param model: the model to read from
793 :param ids: a list of identifiers
795 :param fields: a list of fields to fetch, ``False`` or empty to fetch
796 all fields in the model
797 :type fields: list | False
798 :returns: a list of records, in the same order as the list of ids
801 Model = req.session.model(model)
802 records = Model.read(ids, fields, req.session.eval_context(req.context))
804 record_map = dict((record['id'], record) for record in records)
806 return [record_map[id] for id in ids if record_map.get(id)]
808 @openerpweb.jsonrequest
809 def load(self, req, model, id, fields):
810 m = req.session.model(model)
812 r = m.read([id], False, req.session.eval_context(req.context))
815 return {'value': value}
817 @openerpweb.jsonrequest
818 def create(self, req, model, data):
819 m = req.session.model(model)
820 r = m.create(data, req.session.eval_context(req.context))
823 @openerpweb.jsonrequest
824 def save(self, req, model, id, data):
825 m = req.session.model(model)
826 r = m.write([id], data, req.session.eval_context(req.context))
829 @openerpweb.jsonrequest
830 def unlink(self, req, model, ids=()):
831 Model = req.session.model(model)
832 return Model.unlink(ids, req.session.eval_context(req.context))
834 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
835 has_domain = domain_id is not None and domain_id < len(args)
836 has_context = context_id is not None and context_id < len(args)
838 domain = args[domain_id] if has_domain else []
839 context = args[context_id] if has_context else {}
840 c, d = eval_context_and_domain(req.session, context, domain)
846 return self._call_kw(req, model, method, args, {})
848 def _call_kw(self, req, model, method, args, kwargs):
849 for i in xrange(len(args)):
850 if isinstance(args[i], web.common.nonliterals.BaseContext):
851 args[i] = req.session.eval_context(args[i])
852 elif isinstance(args[i], web.common.nonliterals.BaseDomain):
853 args[i] = req.session.eval_domain(args[i])
854 for k in kwargs.keys():
855 if isinstance(kwargs[k], web.common.nonliterals.BaseContext):
856 kwargs[k] = req.session.eval_context(kwargs[k])
857 elif isinstance(kwargs[k], web.common.nonliterals.BaseDomain):
858 kwargs[k] = req.session.eval_domain(kwargs[k])
860 return getattr(req.session.model(model), method)(*args, **kwargs)
862 @openerpweb.jsonrequest
863 def call(self, req, model, method, args, domain_id=None, context_id=None):
864 return self.call_common(req, model, method, args, domain_id, context_id)
866 @openerpweb.jsonrequest
867 def call_kw(self, req, model, method, args, kwargs):
868 return self._call_kw(req, model, method, args, kwargs)
870 @openerpweb.jsonrequest
871 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
872 action = self.call_common(req, model, method, args, domain_id, context_id)
873 if isinstance(action, dict) and action.get('type') != '':
874 return {'result': clean_action(req, action)}
875 return {'result': False}
877 @openerpweb.jsonrequest
878 def exec_workflow(self, req, model, id, signal):
879 r = req.session.exec_workflow(model, id, signal)
882 @openerpweb.jsonrequest
883 def default_get(self, req, model, fields):
884 Model = req.session.model(model)
885 return Model.default_get(fields, req.session.eval_context(req.context))
887 @openerpweb.jsonrequest
888 def name_search(self, req, model, search_str, domain=[], context={}):
889 m = req.session.model(model)
890 r = m.name_search(search_str+'%', domain, '=ilike', context)
893 class DataGroup(openerpweb.Controller):
894 _cp_path = "/web/group"
895 @openerpweb.jsonrequest
896 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
897 Model = req.session.model(model)
898 context, domain = eval_context_and_domain(req.session, req.context, domain)
900 return Model.read_group(
901 domain or [], fields, group_by_fields, 0, False,
902 dict(context, group_by=group_by_fields), sort or False)
904 class View(openerpweb.Controller):
905 _cp_path = "/web/view"
907 def fields_view_get(self, req, model, view_id, view_type,
908 transform=True, toolbar=False, submenu=False):
909 Model = req.session.model(model)
910 context = req.session.eval_context(req.context)
911 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
912 # todo fme?: check that we should pass the evaluated context here
913 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
914 if toolbar and transform:
915 self.process_toolbar(req, fvg['toolbar'])
918 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
919 # depending on how it feels, xmlrpclib.ServerProxy can translate
920 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
921 # enjoy unicode strings which can not be trivially converted to
922 # strings, and it blows up during parsing.
924 # So ensure we fix this retardation by converting view xml back to
926 if isinstance(fvg['arch'], unicode):
927 arch = fvg['arch'].encode('utf-8')
932 evaluation_context = session.evaluation_context(context or {})
933 xml = self.transform_view(arch, session, evaluation_context)
935 xml = ElementTree.fromstring(arch)
936 fvg['arch'] = web.common.xml2json.from_elementtree(xml, preserve_whitespaces)
938 for field in fvg['fields'].itervalues():
939 if field.get('views'):
940 for view in field["views"].itervalues():
941 self.process_view(session, view, None, transform)
942 if field.get('domain'):
943 field["domain"] = parse_domain(field["domain"], session)
944 if field.get('context'):
945 field["context"] = parse_context(field["context"], session)
947 def process_toolbar(self, req, toolbar):
949 The toolbar is a mapping of section_key: [action_descriptor]
951 We need to clean all those actions in order to ensure correct
954 for actions in toolbar.itervalues():
955 for action in actions:
956 if 'context' in action:
957 action['context'] = parse_context(
958 action['context'], req.session)
959 if 'domain' in action:
960 action['domain'] = parse_domain(
961 action['domain'], req.session)
963 @openerpweb.jsonrequest
964 def add_custom(self, req, view_id, arch):
965 CustomView = req.session.model('ir.ui.view.custom')
967 'user_id': req.session._uid,
970 }, req.session.eval_context(req.context))
971 return {'result': True}
973 @openerpweb.jsonrequest
974 def undo_custom(self, req, view_id, reset=False):
975 CustomView = req.session.model('ir.ui.view.custom')
976 context = req.session.eval_context(req.context)
977 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
978 0, False, False, context)
981 CustomView.unlink(vcustom, context)
983 CustomView.unlink([vcustom[0]], context)
984 return {'result': True}
985 return {'result': False}
987 def transform_view(self, view_string, session, context=None):
988 # transform nodes on the fly via iterparse, instead of
989 # doing it statically on the parsing result
990 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
992 for event, elem in parser:
996 self.parse_domains_and_contexts(elem, session)
999 def parse_domains_and_contexts(self, elem, session):
1000 """ Converts domains and contexts from the view into Python objects,
1001 either literals if they can be parsed by literal_eval or a special
1002 placeholder object if the domain or context refers to free variables.
1004 :param elem: the current node being parsed
1005 :type param: xml.etree.ElementTree.Element
1006 :param session: OpenERP session object, used to store and retrieve
1008 :type session: openerpweb.openerpweb.OpenERPSession
1010 for el in ['domain', 'filter_domain']:
1011 domain = elem.get(el, '').strip()
1013 elem.set(el, parse_domain(domain, session))
1014 elem.set(el + '_string', domain)
1015 for el in ['context', 'default_get']:
1016 context_string = elem.get(el, '').strip()
1018 elem.set(el, parse_context(context_string, session))
1019 elem.set(el + '_string', context_string)
1021 @openerpweb.jsonrequest
1022 def load(self, req, model, view_id, view_type, toolbar=False):
1023 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1025 def parse_domain(domain, session):
1026 """ Parses an arbitrary string containing a domain, transforms it
1027 to either a literal domain or a :class:`web.common.nonliterals.Domain`
1029 :param domain: the domain to parse, if the domain is not a string it
1030 is assumed to be a literal domain and is returned as-is
1031 :param session: Current OpenERP session
1032 :type session: openerpweb.openerpweb.OpenERPSession
1034 if not isinstance(domain, (str, unicode)):
1037 return ast.literal_eval(domain)
1040 return web.common.nonliterals.Domain(session, domain)
1042 def parse_context(context, session):
1043 """ Parses an arbitrary string containing a context, transforms it
1044 to either a literal context or a :class:`web.common.nonliterals.Context`
1046 :param context: the context to parse, if the context is not a string it
1047 is assumed to be a literal domain and is returned as-is
1048 :param session: Current OpenERP session
1049 :type session: openerpweb.openerpweb.OpenERPSession
1051 if not isinstance(context, (str, unicode)):
1054 return ast.literal_eval(context)
1056 return web.common.nonliterals.Context(session, context)
1058 class ListView(View):
1059 _cp_path = "/web/listview"
1061 def process_colors(self, view, row, context):
1062 colors = view['arch']['attrs'].get('colors')
1069 for pair in colors.split(';')
1070 if eval(pair.split(':')[1], dict(context, **row))
1075 elif len(color) == 1:
1079 class TreeView(View):
1080 _cp_path = "/web/treeview"
1082 @openerpweb.jsonrequest
1083 def action(self, req, model, id):
1084 return load_actions_from_ir_values(
1085 req,'action', 'tree_but_open',[(model, id)],
1088 class SearchView(View):
1089 _cp_path = "/web/searchview"
1091 @openerpweb.jsonrequest
1092 def load(self, req, model, view_id):
1093 fields_view = self.fields_view_get(req, model, view_id, 'search')
1094 return {'fields_view': fields_view}
1096 @openerpweb.jsonrequest
1097 def fields_get(self, req, model):
1098 Model = req.session.model(model)
1099 fields = Model.fields_get(False, req.session.eval_context(req.context))
1100 for field in fields.values():
1101 # shouldn't convert the views too?
1102 if field.get('domain'):
1103 field["domain"] = parse_domain(field["domain"], req.session)
1104 if field.get('context'):
1105 field["context"] = parse_context(field["context"], req.session)
1106 return {'fields': fields}
1108 @openerpweb.jsonrequest
1109 def get_filters(self, req, model):
1110 Model = req.session.model("ir.filters")
1111 filters = Model.get_filters(model)
1112 for filter in filters:
1113 filter["context"] = req.session.eval_context(parse_context(filter["context"], req.session))
1114 filter["domain"] = req.session.eval_domain(parse_domain(filter["domain"], req.session))
1117 @openerpweb.jsonrequest
1118 def save_filter(self, req, model, name, context_to_save, domain):
1119 Model = req.session.model("ir.filters")
1120 ctx = web.common.nonliterals.CompoundContext(context_to_save)
1121 ctx.session = req.session
1122 ctx = ctx.evaluate()
1123 domain = web.common.nonliterals.CompoundDomain(domain)
1124 domain.session = req.session
1125 domain = domain.evaluate()
1126 uid = req.session._uid
1127 context = req.session.eval_context(req.context)
1128 to_return = Model.create_or_replace({"context": ctx,
1136 @openerpweb.jsonrequest
1137 def add_to_dashboard(self, req, menu_id, action_id, context_to_save, domain, view_mode, name=''):
1138 ctx = web.common.nonliterals.CompoundContext(context_to_save)
1139 ctx.session = req.session
1140 ctx = ctx.evaluate()
1141 domain = web.common.nonliterals.CompoundDomain(domain)
1142 domain.session = req.session
1143 domain = domain.evaluate()
1145 dashboard_action = load_actions_from_ir_values(req, 'action', 'tree_but_open',
1146 [('ir.ui.menu', menu_id)], False)
1147 if dashboard_action:
1148 action = dashboard_action[0][2]
1149 if action['res_model'] == 'board.board' and action['views'][0][1] == 'form':
1150 # Maybe should check the content instead of model board.board ?
1151 view_id = action['views'][0][0]
1152 board = req.session.model(action['res_model']).fields_view_get(view_id, 'form')
1153 if board and 'arch' in board:
1154 xml = ElementTree.fromstring(board['arch'])
1155 column = xml.find('./board/column')
1157 new_action = ElementTree.Element('action', {
1158 'name' : str(action_id),
1160 'view_mode' : view_mode,
1161 'context' : str(ctx),
1162 'domain' : str(domain)
1164 column.insert(0, new_action)
1165 arch = ElementTree.tostring(xml, 'utf-8')
1166 return req.session.model('ir.ui.view.custom').create({
1167 'user_id': req.session._uid,
1170 }, req.session.eval_context(req.context))
1174 class Binary(openerpweb.Controller):
1175 _cp_path = "/web/binary"
1177 @openerpweb.httprequest
1178 def image(self, req, model, id, field, **kw):
1179 Model = req.session.model(model)
1180 context = req.session.eval_context(req.context)
1184 res = Model.default_get([field], context).get(field)
1186 res = Model.read([int(id)], [field], context)[0].get(field)
1187 image_data = base64.b64decode(res)
1188 except (TypeError, xmlrpclib.Fault):
1189 image_data = self.placeholder(req)
1190 return req.make_response(image_data, [
1191 ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
1192 def placeholder(self, req):
1193 addons_path = openerpweb.addons_manifest['web']['addons_path']
1194 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1196 @openerpweb.httprequest
1197 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1198 """ Download link for files stored as binary fields.
1200 If the ``id`` parameter is omitted, fetches the default value for the
1201 binary field (via ``default_get``), otherwise fetches the field for
1202 that precise record.
1204 :param req: OpenERP request
1205 :type req: :class:`web.common.http.HttpRequest`
1206 :param str model: name of the model to fetch the binary from
1207 :param str field: binary field
1208 :param str id: id of the record from which to fetch the binary
1209 :param str filename_field: field holding the file's name, if any
1210 :returns: :class:`werkzeug.wrappers.Response`
1212 Model = req.session.model(model)
1213 context = req.session.eval_context(req.context)
1216 fields.append(filename_field)
1218 res = Model.read([int(id)], fields, context)[0]
1220 res = Model.default_get(fields, context)
1221 filecontent = base64.b64decode(res.get(field, ''))
1223 return req.not_found()
1225 filename = '%s_%s' % (model.replace('.', '_'), id)
1227 filename = res.get(filename_field, '') or filename
1228 return req.make_response(filecontent,
1229 [('Content-Type', 'application/octet-stream'),
1230 ('Content-Disposition', 'attachment; filename="%s"' % filename)])
1232 @openerpweb.httprequest
1233 def upload(self, req, callback, ufile):
1234 # TODO: might be useful to have a configuration flag for max-length file uploads
1236 out = """<script language="javascript" type="text/javascript">
1237 var win = window.top.window,
1239 if (typeof(callback) === 'function') {
1240 callback.apply(this, %s);
1242 win.jQuery('#oe_notification', win.document).notify('create', {
1243 title: "Ajax File Upload",
1244 text: "Could not find callback"
1249 args = [len(data), ufile.filename,
1250 ufile.content_type, base64.b64encode(data)]
1251 except Exception, e:
1252 args = [False, e.message]
1253 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1255 @openerpweb.httprequest
1256 def upload_attachment(self, req, callback, model, id, ufile):
1257 context = req.session.eval_context(req.context)
1258 Model = req.session.model('ir.attachment')
1260 out = """<script language="javascript" type="text/javascript">
1261 var win = window.top.window,
1263 if (typeof(callback) === 'function') {
1264 callback.call(this, %s);
1267 attachment_id = Model.create({
1268 'name': ufile.filename,
1269 'datas': base64.encodestring(ufile.read()),
1270 'datas_fname': ufile.filename,
1275 'filename': ufile.filename,
1278 except Exception, e:
1279 args = { 'error': e.message }
1280 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1282 class Action(openerpweb.Controller):
1283 _cp_path = "/web/action"
1285 @openerpweb.jsonrequest
1286 def load(self, req, action_id, do_not_eval=False):
1287 Actions = req.session.model('ir.actions.actions')
1289 context = req.session.eval_context(req.context)
1290 action_type = Actions.read([action_id], ['type'], context)
1293 if action_type[0]['type'] == 'ir.actions.report.xml':
1294 ctx.update({'bin_size': True})
1296 action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
1298 value = clean_action(req, action[0], do_not_eval)
1299 return {'result': value}
1301 @openerpweb.jsonrequest
1302 def run(self, req, action_id):
1303 return clean_action(req, req.session.model('ir.actions.server').run(
1304 [action_id], req.session.eval_context(req.context)))
1307 _cp_path = "/web/export"
1309 @openerpweb.jsonrequest
1310 def formats(self, req):
1311 """ Returns all valid export formats
1313 :returns: for each export format, a pair of identifier and printable name
1314 :rtype: [(str, str)]
1318 for path, controller in openerpweb.controllers_path.iteritems()
1319 if path.startswith(self._cp_path)
1320 if hasattr(controller, 'fmt')
1321 ], key=operator.itemgetter("label"))
1323 def fields_get(self, req, model):
1324 Model = req.session.model(model)
1325 fields = Model.fields_get(False, req.session.eval_context(req.context))
1328 @openerpweb.jsonrequest
1329 def get_fields(self, req, model, prefix='', parent_name= '',
1330 import_compat=True, parent_field_type=None,
1333 if import_compat and parent_field_type == "many2one":
1336 fields = self.fields_get(req, model)
1339 fields.pop('id', None)
1341 fields['.id'] = fields.pop('id', {'string': 'ID'})
1343 fields_sequence = sorted(fields.iteritems(),
1344 key=lambda field: field[1].get('string', ''))
1347 for field_name, field in fields_sequence:
1349 if exclude and field_name in exclude:
1351 if 'function' in field:
1353 if field.get('readonly'):
1354 # If none of the field's states unsets readonly, skip the field
1355 if all(dict(attrs).get('readonly', True)
1356 for attrs in field.get('states', {}).values()):
1359 id = prefix + (prefix and '/'or '') + field_name
1360 name = parent_name + (parent_name and '/' or '') + field['string']
1361 record = {'id': id, 'string': name,
1362 'value': id, 'children': False,
1363 'field_type': field.get('type'),
1364 'required': field.get('required'),
1365 'relation_field': field.get('relation_field')}
1366 records.append(record)
1368 if len(name.split('/')) < 3 and 'relation' in field:
1369 ref = field.pop('relation')
1370 record['value'] += '/id'
1371 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1373 if not import_compat or field['type'] == 'one2many':
1374 # m2m field in import_compat is childless
1375 record['children'] = True
1379 @openerpweb.jsonrequest
1380 def namelist(self,req, model, export_id):
1381 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1382 export = req.session.model("ir.exports").read([export_id])[0]
1383 export_fields_list = req.session.model("ir.exports.line").read(
1384 export['export_fields'])
1386 fields_data = self.fields_info(
1387 req, model, map(operator.itemgetter('name'), export_fields_list))
1390 {'name': field['name'], 'label': fields_data[field['name']]}
1391 for field in export_fields_list
1394 def fields_info(self, req, model, export_fields):
1396 fields = self.fields_get(req, model)
1398 # To make fields retrieval more efficient, fetch all sub-fields of a
1399 # given field at the same time. Because the order in the export list is
1400 # arbitrary, this requires ordering all sub-fields of a given field
1401 # together so they can be fetched at the same time
1403 # Works the following way:
1404 # * sort the list of fields to export, the default sorting order will
1405 # put the field itself (if present, for xmlid) and all of its
1406 # sub-fields right after it
1407 # * then, group on: the first field of the path (which is the same for
1408 # a field and for its subfields and the length of splitting on the
1409 # first '/', which basically means grouping the field on one side and
1410 # all of the subfields on the other. This way, we have the field (for
1411 # the xmlid) with length 1, and all of the subfields with the same
1412 # base but a length "flag" of 2
1413 # * if we have a normal field (length 1), just add it to the info
1414 # mapping (with its string) as-is
1415 # * otherwise, recursively call fields_info via graft_subfields.
1416 # all graft_subfields does is take the result of fields_info (on the
1417 # field's model) and prepend the current base (current field), which
1418 # rebuilds the whole sub-tree for the field
1420 # result: because we're not fetching the fields_get for half the
1421 # database models, fetching a namelist with a dozen fields (including
1422 # relational data) falls from ~6s to ~300ms (on the leads model).
1423 # export lists with no sub-fields (e.g. import_compatible lists with
1424 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1425 # there's a single fields_get to execute)
1426 for (base, length), subfields in itertools.groupby(
1427 sorted(export_fields),
1428 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1429 subfields = list(subfields)
1431 # subfields is a seq of $base/*rest, and not loaded yet
1432 info.update(self.graft_subfields(
1433 req, fields[base]['relation'], base, fields[base]['string'],
1437 info[base] = fields[base]['string']
1441 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1442 export_fields = [field.split('/', 1)[1] for field in fields]
1444 (prefix + '/' + k, prefix_string + '/' + v)
1445 for k, v in self.fields_info(req, model, export_fields).iteritems())
1447 #noinspection PyPropertyDefinition
1449 def content_type(self):
1450 """ Provides the format's content type """
1451 raise NotImplementedError()
1453 def filename(self, base):
1454 """ Creates a valid filename for the format (with extension) from the
1455 provided base name (exension-less)
1457 raise NotImplementedError()
1459 def from_data(self, fields, rows):
1460 """ Conversion method from OpenERP's export data to whatever the
1461 current export class outputs
1463 :params list fields: a list of fields to export
1464 :params list rows: a list of records to export
1468 raise NotImplementedError()
1470 @openerpweb.httprequest
1471 def index(self, req, data, token):
1472 model, fields, ids, domain, import_compat = \
1473 operator.itemgetter('model', 'fields', 'ids', 'domain',
1475 simplejson.loads(data))
1477 context = req.session.eval_context(req.context)
1478 Model = req.session.model(model)
1479 ids = ids or Model.search(domain, 0, False, False, context)
1481 field_names = map(operator.itemgetter('name'), fields)
1482 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1485 columns_headers = field_names
1487 columns_headers = [val['label'].strip() for val in fields]
1490 return req.make_response(self.from_data(columns_headers, import_data),
1491 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1492 ('Content-Type', self.content_type)],
1493 cookies={'fileToken': int(token)})
1495 class CSVExport(Export):
1496 _cp_path = '/web/export/csv'
1497 fmt = {'tag': 'csv', 'label': 'CSV'}
1500 def content_type(self):
1501 return 'text/csv;charset=utf8'
1503 def filename(self, base):
1504 return base + '.csv'
1506 def from_data(self, fields, rows):
1508 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1510 writer.writerow(fields)
1515 if isinstance(d, basestring):
1516 d = d.replace('\n',' ').replace('\t',' ')
1518 d = d.encode('utf-8')
1521 if d is False: d = None
1523 writer.writerow(row)
1530 class ExcelExport(Export):
1531 _cp_path = '/web/export/xls'
1535 'error': None if xlwt else "XLWT required"
1539 def content_type(self):
1540 return 'application/vnd.ms-excel'
1542 def filename(self, base):
1543 return base + '.xls'
1545 def from_data(self, fields, rows):
1546 workbook = xlwt.Workbook()
1547 worksheet = workbook.add_sheet('Sheet 1')
1549 for i, fieldname in enumerate(fields):
1550 worksheet.write(0, i, str(fieldname))
1551 worksheet.col(i).width = 8000 # around 220 pixels
1553 style = xlwt.easyxf('align: wrap yes')
1555 for row_index, row in enumerate(rows):
1556 for cell_index, cell_value in enumerate(row):
1557 if isinstance(cell_value, basestring):
1558 cell_value = re.sub("\r", " ", cell_value)
1559 if cell_value is False: cell_value = None
1560 worksheet.write(row_index + 1, cell_index, cell_value, style)
1569 class Reports(View):
1570 _cp_path = "/web/report"
1571 POLLING_DELAY = 0.25
1573 'doc': 'application/vnd.ms-word',
1574 'html': 'text/html',
1575 'odt': 'application/vnd.oasis.opendocument.text',
1576 'pdf': 'application/pdf',
1577 'sxw': 'application/vnd.sun.xml.writer',
1578 'xls': 'application/vnd.ms-excel',
1581 @openerpweb.httprequest
1582 def index(self, req, action, token):
1583 action = simplejson.loads(action)
1585 report_srv = req.session.proxy("report")
1586 context = req.session.eval_context(
1587 web.common.nonliterals.CompoundContext(
1588 req.context or {}, action[ "context"]))
1591 report_ids = context["active_ids"]
1592 if 'report_type' in action:
1593 report_data['report_type'] = action['report_type']
1594 if 'datas' in action:
1595 if 'ids' in action['datas']:
1596 report_ids = action['datas'].pop('ids')
1597 report_data.update(action['datas'])
1599 report_id = report_srv.report(
1600 req.session._db, req.session._uid, req.session._password,
1601 action["report_name"], report_ids,
1602 report_data, context)
1604 report_struct = None
1606 report_struct = report_srv.report_get(
1607 req.session._db, req.session._uid, req.session._password, report_id)
1608 if report_struct["state"]:
1611 time.sleep(self.POLLING_DELAY)
1613 report = base64.b64decode(report_struct['result'])
1614 if report_struct.get('code') == 'zlib':
1615 report = zlib.decompress(report)
1616 report_mimetype = self.TYPES_MAPPING.get(
1617 report_struct['format'], 'octet-stream')
1618 return req.make_response(report,
1620 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
1621 ('Content-Type', report_mimetype),
1622 ('Content-Length', len(report))],
1623 cookies={'fileToken': int(token)})
1626 _cp_path = "/web/import"
1628 def fields_get(self, req, model):
1629 Model = req.session.model(model)
1630 fields = Model.fields_get(False, req.session.eval_context(req.context))
1633 @openerpweb.httprequest
1634 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1636 data = list(csv.reader(
1637 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1638 except csv.Error, e:
1640 return '<script>window.top.%s(%s);</script>' % (
1641 jsonp, simplejson.dumps({'error': {
1642 'message': 'Error parsing CSV file: %s' % e,
1643 # decodes each byte to a unicode character, which may or
1644 # may not be printable, but decoding will succeed.
1645 # Otherwise simplejson will try to decode the `str` using
1646 # utf-8, which is very likely to blow up on characters out
1647 # of the ascii range (in range [128, 256))
1648 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1651 return '<script>window.top.%s(%s);</script>' % (
1652 jsonp, simplejson.dumps(
1653 {'records': data[:10]}, encoding=csvcode))
1654 except UnicodeDecodeError:
1655 return '<script>window.top.%s(%s);</script>' % (
1656 jsonp, simplejson.dumps({
1657 'message': u"Failed to decode CSV file using encoding %s, "
1658 u"try switching to a different encoding" % csvcode
1661 @openerpweb.httprequest
1662 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1664 modle_obj = req.session.model(model)
1665 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1666 simplejson.loads(meta))
1669 if not (csvdel and len(csvdel) == 1):
1670 error = u"The CSV delimiter must be a single character"
1672 if not indices and fields:
1673 error = u"You must select at least one field to import"
1676 return '<script>window.top.%s(%s);</script>' % (
1677 jsonp, simplejson.dumps({'error': {'message': error}}))
1679 # skip ignored records
1680 data_record = itertools.islice(
1681 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1684 # if only one index, itemgetter will return an atom rather than a tuple
1685 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1686 else: mapper = operator.itemgetter(*indices)
1691 # decode each data row
1693 [record.decode(csvcode) for record in row]
1694 for row in itertools.imap(mapper, data_record)
1695 # don't insert completely empty rows (can happen due to fields
1696 # filtering in case of e.g. o2m content rows)
1699 except UnicodeDecodeError:
1700 error = u"Failed to decode CSV file using encoding %s" % csvcode
1701 except csv.Error, e:
1702 error = u"Could not process CSV file: %s" % e
1704 # If the file contains nothing,
1706 error = u"File to import is empty"
1708 return '<script>window.top.%s(%s);</script>' % (
1709 jsonp, simplejson.dumps({'error': {'message': error}}))
1712 (code, record, message, _nope) = modle_obj.import_data(
1713 fields, data, 'init', '', False,
1714 req.session.eval_context(req.context))
1715 except xmlrpclib.Fault, e:
1716 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1717 return '<script>window.top.%s(%s);</script>' % (
1718 jsonp, simplejson.dumps({'error':error}))
1721 return '<script>window.top.%s(%s);</script>' % (
1722 jsonp, simplejson.dumps({'success':True}))
1724 msg = u"Error during import: %s\n\nTrying to import record %r" % (
1726 return '<script>window.top.%s(%s);</script>' % (
1727 jsonp, simplejson.dumps({'error': {'message':msg}}))