1 # -*- coding: utf-8 -*-
16 from xml.etree import ElementTree
17 from cStringIO import StringIO
19 import babel.messages.pofile
23 openerpweb = web.common.http
25 #----------------------------------------------------------
26 # OpenERP Web web Controllers
27 #----------------------------------------------------------
30 def concat_xml(file_list):
31 """Concatenate xml files
32 return (concat,timestamp)
33 concat: concatenation of file content
34 timestamp: max(os.path.getmtime of file_list)
38 for fname in file_list:
39 ftime = os.path.getmtime(fname)
40 if ftime > files_timestamp:
41 files_timestamp = ftime
43 xml = ElementTree.parse(fname).getroot()
46 root = ElementTree.Element(xml.tag)
47 #elif root.tag != xml.tag:
48 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
50 for child in xml.getchildren():
52 return ElementTree.tostring(root, 'utf-8'), files_timestamp
55 def concat_files(file_list, reader=None):
56 """ Concatenate file content
57 return (concat,timestamp)
58 concat: concatenation of file content, read by `reader`
59 timestamp: max(os.path.getmtime of file_list)
68 for fname in file_list:
69 ftime = os.path.getmtime(fname)
70 if ftime > files_timestamp:
71 files_timestamp = ftime
73 files_content.append(reader(fname))
74 files_concat = "".join(files_content)
75 return files_concat,files_timestamp
77 html_template = """<!DOCTYPE html>
78 <html style="height: 100%%">
80 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
81 <title>OpenERP</title>
82 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
85 <script type="text/javascript">
87 var s = new openerp.init(%(modules)s);
96 class WebClient(openerpweb.Controller):
97 _cp_path = "/web/webclient"
99 def server_wide_modules(self, req):
100 addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest]
103 def manifest_glob(self, req, addons, key):
105 addons = self.server_wide_modules(req)
107 addons = addons.split(',')
110 manifest = openerpweb.addons_manifest.get(addon, None)
113 # ensure does not ends with /
114 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
115 globlist = manifest.get(key, [])
116 for pattern in globlist:
117 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
118 r.append( (path, path[len(addons_path):]))
121 def manifest_list(self, req, mods, extension):
123 path = '/web/webclient/' + extension
125 path += '?mods=' + mods
127 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in self.manifest_glob(req, mods, extension)]
129 @openerpweb.jsonrequest
130 def csslist(self, req, mods=None):
131 return self.manifest_list(req, mods, 'css')
133 @openerpweb.jsonrequest
134 def jslist(self, req, mods=None):
135 return self.manifest_list(req, mods, 'js')
137 @openerpweb.jsonrequest
138 def qweblist(self, req, mods=None):
139 return self.manifest_list(req, mods, 'qweb')
141 @openerpweb.httprequest
142 def css(self, req, mods=None):
144 files = list(self.manifest_glob(req, mods, 'css'))
145 file_map = dict(files)
147 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
148 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://)""", re.U)
152 """read the a css file and absolutify all relative uris"""
156 web_path = file_map[f]
157 web_dir = os.path.dirname(web_path)
161 r"""@import \1%s/""" % (web_dir,),
167 r"""url(\1%s/""" % (web_dir,),
172 content,timestamp = concat_files((f[0] for f in files), reader)
173 # TODO use timestamp to set Last mofified date and E-tag
174 return req.make_response(content, [('Content-Type', 'text/css')])
176 @openerpweb.httprequest
177 def js(self, req, mods=None):
178 files = [f[0] for f in self.manifest_glob(req, mods, 'js')]
179 content,timestamp = concat_files(files)
180 # TODO use timestamp to set Last mofified date and E-tag
181 return req.make_response(content, [('Content-Type', 'application/javascript')])
183 @openerpweb.httprequest
184 def qweb(self, req, mods=None):
185 files = [f[0] for f in self.manifest_glob(req, mods, 'qweb')]
186 content,timestamp = concat_xml(files)
187 # TODO use timestamp to set Last mofified date and E-tag
188 return req.make_response(content, [('Content-Type', 'text/xml')])
191 @openerpweb.httprequest
192 def home(self, req, s_action=None, **kw):
193 js = "\n ".join('<script type="text/javascript" src="%s"></script>'%i for i in self.manifest_list(req, None, 'js'))
194 css = "\n ".join('<link rel="stylesheet" href="%s">'%i for i in self.manifest_list(req, None, 'css'))
196 r = html_template % {
199 'modules': simplejson.dumps(self.server_wide_modules(req)),
200 'init': 'new s.web.WebClient().replace($("body"));',
204 @openerpweb.httprequest
205 def login(self, req, db, login, key):
206 req.session.authenticate(db, login, key, {})
207 redirect = werkzeug.utils.redirect('/web/webclient/home', 303)
208 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
209 redirect.set_cookie('session0|session_id', cookie_val)
212 @openerpweb.jsonrequest
213 def translations(self, req, mods, lang):
214 lang_model = req.session.model('res.lang')
215 ids = lang_model.search([("code", "=", lang)])
217 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
218 "grouping", "decimal_point", "thousands_sep"])
222 if lang.count("_") > 0:
226 langs = lang.split(separator)
227 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
230 for addon_name in mods:
231 transl = {"messages":[]}
232 transs[addon_name] = transl
234 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
235 f_name = os.path.join(addons_path, addon_name, "po", l + ".po")
236 if not os.path.exists(f_name):
239 with open(f_name) as t_file:
240 po = babel.messages.pofile.read_po(t_file)
244 if x.id and x.string:
245 transl["messages"].append({'id': x.id, 'string': x.string})
246 return {"modules": transs,
247 "lang_parameters": lang_obj}
249 @openerpweb.jsonrequest
250 def version_info(self, req):
252 "version": web.common.release.version
255 class Proxy(openerpweb.Controller):
256 _cp_path = '/web/proxy'
258 @openerpweb.jsonrequest
259 def load(self, req, path):
260 """ Proxies an HTTP request through a JSON request.
262 It is strongly recommended to not request binary files through this,
263 as the result will be a binary data blob as well.
265 :param req: OpenERP request
266 :param path: actual request path
267 :return: file content
269 from werkzeug.test import Client
270 from werkzeug.wrappers import BaseResponse
272 return Client(req.httprequest.app, BaseResponse).get(path).data
274 class Database(openerpweb.Controller):
275 _cp_path = "/web/database"
277 @openerpweb.jsonrequest
278 def get_list(self, req):
279 proxy = req.session.proxy("db")
281 h = req.httprequest.headers['Host'].split(':')[0]
283 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
284 dbs = [i for i in dbs if re.match(r, i)]
285 return {"db_list": dbs}
287 @openerpweb.jsonrequest
288 def progress(self, req, password, id):
289 return req.session.proxy('db').get_progress(password, id)
291 @openerpweb.jsonrequest
292 def create(self, req, fields):
294 params = dict(map(operator.itemgetter('name', 'value'), fields))
296 params['super_admin_pwd'],
298 bool(params.get('demo_data')),
300 params['create_admin_pwd']
304 return req.session.proxy("db").create(*create_attrs)
305 except xmlrpclib.Fault, e:
306 if e.faultCode and isinstance(e.faultCode, str)\
307 and e.faultCode.split(':')[0] == 'AccessDenied':
308 return {'error': e.faultCode, 'title': 'Database creation error'}
310 'error': "Could not create database '%s': %s" % (
311 params['db_name'], e.faultString),
312 'title': 'Database creation error'
315 @openerpweb.jsonrequest
316 def drop(self, req, fields):
317 password, db = operator.itemgetter(
318 'drop_pwd', 'drop_db')(
319 dict(map(operator.itemgetter('name', 'value'), fields)))
322 return req.session.proxy("db").drop(password, db)
323 except xmlrpclib.Fault, e:
324 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
325 return {'error': e.faultCode, 'title': 'Drop Database'}
326 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
328 @openerpweb.httprequest
329 def backup(self, req, backup_db, backup_pwd, token):
331 db_dump = base64.b64decode(
332 req.session.proxy("db").dump(backup_pwd, backup_db))
333 return req.make_response(db_dump,
334 [('Content-Type', 'application/octet-stream; charset=binary'),
335 ('Content-Disposition', 'attachment; filename="' + backup_db + '.dump"')],
336 {'fileToken': int(token)}
338 except xmlrpclib.Fault, e:
339 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
340 return 'Backup Database|' + e.faultCode
341 return 'Backup Database|Could not generate database backup'
343 @openerpweb.httprequest
344 def restore(self, req, db_file, restore_pwd, new_db):
346 data = base64.b64encode(db_file.read())
347 req.session.proxy("db").restore(restore_pwd, new_db, data)
349 except xmlrpclib.Fault, e:
350 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
351 raise Exception("AccessDenied")
353 @openerpweb.jsonrequest
354 def change_password(self, req, fields):
355 old_password, new_password = operator.itemgetter(
356 'old_pwd', 'new_pwd')(
357 dict(map(operator.itemgetter('name', 'value'), fields)))
359 return req.session.proxy("db").change_admin_password(old_password, new_password)
360 except xmlrpclib.Fault, e:
361 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
362 return {'error': e.faultCode, 'title': 'Change Password'}
363 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
365 class Session(openerpweb.Controller):
366 _cp_path = "/web/session"
368 def session_info(self, req):
370 "session_id": req.session_id,
371 "uid": req.session._uid,
372 "context": req.session.get_context() if req.session._uid else {},
373 "db": req.session._db,
374 "login": req.session._login,
375 "openerp_entreprise": req.session.openerp_entreprise(),
378 @openerpweb.jsonrequest
379 def get_session_info(self, req):
380 return self.session_info(req)
382 @openerpweb.jsonrequest
383 def authenticate(self, req, db, login, password, base_location=None):
384 wsgienv = req.httprequest.environ
385 release = web.common.release
387 base_location=base_location,
388 HTTP_HOST=wsgienv['HTTP_HOST'],
389 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
390 user_agent="%s / %s" % (release.name, release.version),
392 req.session.authenticate(db, login, password, env)
394 return self.session_info(req)
396 @openerpweb.jsonrequest
397 def change_password (self,req,fields):
398 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
399 dict(map(operator.itemgetter('name', 'value'), fields)))
400 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
401 return {'error':'All passwords have to be filled.','title': 'Change Password'}
402 if new_password != confirm_password:
403 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
405 if req.session.model('res.users').change_password(
406 old_password, new_password):
407 return {'new_password':new_password}
409 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
410 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
412 @openerpweb.jsonrequest
413 def sc_list(self, req):
414 return req.session.model('ir.ui.view_sc').get_sc(
415 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
417 @openerpweb.jsonrequest
418 def get_lang_list(self, req):
421 'lang_list': (req.session.proxy("db").list_lang() or []),
425 return {"error": e, "title": "Languages"}
427 @openerpweb.jsonrequest
428 def modules(self, req):
429 # Compute available candidates module
430 loadable = openerpweb.addons_manifest.iterkeys()
431 loaded = req.config.server_wide_modules
432 candidates = [mod for mod in loadable if mod not in loaded]
434 # Compute active true modules that might be on the web side only
435 active = set(name for name in candidates
436 if openerpweb.addons_manifest[name].get('active'))
438 # Retrieve database installed modules
439 Modules = req.session.model('ir.module.module')
440 installed = set(module['name'] for module in Modules.search_read(
441 [('state','=','installed'), ('name','in', candidates)], ['name']))
444 return list(active | installed)
446 @openerpweb.jsonrequest
447 def eval_domain_and_context(self, req, contexts, domains,
449 """ Evaluates sequences of domains and contexts, composing them into
450 a single context, domain or group_by sequence.
452 :param list contexts: list of contexts to merge together. Contexts are
453 evaluated in sequence, all previous contexts
454 are part of their own evaluation context
455 (starting at the session context).
456 :param list domains: list of domains to merge together. Domains are
457 evaluated in sequence and appended to one another
458 (implicit AND), their evaluation domain is the
459 result of merging all contexts.
460 :param list group_by_seq: list of domains (which may be in a different
461 order than the ``contexts`` parameter),
462 evaluated in sequence, their ``'group_by'``
463 key is extracted if they have one.
468 the global context created by merging all of
472 the concatenation of all domains
475 a list of fields to group by, potentially empty (in which case
476 no group by should be performed)
478 context, domain = eval_context_and_domain(req.session,
479 web.common.nonliterals.CompoundContext(*(contexts or [])),
480 web.common.nonliterals.CompoundDomain(*(domains or [])))
482 group_by_sequence = []
483 for candidate in (group_by_seq or []):
484 ctx = req.session.eval_context(candidate, context)
485 group_by = ctx.get('group_by')
488 elif isinstance(group_by, basestring):
489 group_by_sequence.append(group_by)
491 group_by_sequence.extend(group_by)
496 'group_by': group_by_sequence
499 @openerpweb.jsonrequest
500 def save_session_action(self, req, the_action):
502 This method store an action object in the session object and returns an integer
503 identifying that action. The method get_session_action() can be used to get
506 :param the_action: The action to save in the session.
507 :type the_action: anything
508 :return: A key identifying the saved action.
511 saved_actions = req.httpsession.get('saved_actions')
512 if not saved_actions:
513 saved_actions = {"next":0, "actions":{}}
514 req.httpsession['saved_actions'] = saved_actions
515 # we don't allow more than 10 stored actions
516 if len(saved_actions["actions"]) >= 10:
517 del saved_actions["actions"][min(saved_actions["actions"].keys())]
518 key = saved_actions["next"]
519 saved_actions["actions"][key] = the_action
520 saved_actions["next"] = key + 1
523 @openerpweb.jsonrequest
524 def get_session_action(self, req, key):
526 Gets back a previously saved action. This method can return None if the action
527 was saved since too much time (this case should be handled in a smart way).
529 :param key: The key given by save_session_action()
531 :return: The saved action or None.
534 saved_actions = req.httpsession.get('saved_actions')
535 if not saved_actions:
537 return saved_actions["actions"].get(key)
539 @openerpweb.jsonrequest
540 def check(self, req):
541 req.session.assert_valid()
544 def eval_context_and_domain(session, context, domain=None):
545 e_context = session.eval_context(context)
546 # should we give the evaluated context as an evaluation context to the domain?
547 e_domain = session.eval_domain(domain or [])
549 return e_context, e_domain
551 def load_actions_from_ir_values(req, key, key2, models, meta):
552 context = req.session.eval_context(req.context)
553 Values = req.session.model('ir.values')
554 actions = Values.get(key, key2, models, meta, context)
556 return [(id, name, clean_action(req, action))
557 for id, name, action in actions]
559 def clean_action(req, action, do_not_eval=False):
560 action.setdefault('flags', {})
562 context = req.session.eval_context(req.context)
563 eval_ctx = req.session.evaluation_context(context)
566 # values come from the server, we can just eval them
567 if isinstance(action.get('context'), basestring):
568 action['context'] = eval( action['context'], eval_ctx ) or {}
570 if isinstance(action.get('domain'), basestring):
571 action['domain'] = eval( action['domain'], eval_ctx ) or []
573 if 'context' in action:
574 action['context'] = parse_context(action['context'], req.session)
575 if 'domain' in action:
576 action['domain'] = parse_domain(action['domain'], req.session)
578 if 'type' not in action:
579 action['type'] = 'ir.actions.act_window_close'
581 if action['type'] == 'ir.actions.act_window':
582 return fix_view_modes(action)
585 # I think generate_views,fix_view_modes should go into js ActionManager
586 def generate_views(action):
588 While the server generates a sequence called "views" computing dependencies
589 between a bunch of stuff for views coming directly from the database
590 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
591 to return custom view dictionaries generated on the fly.
593 In that case, there is no ``views`` key available on the action.
595 Since the web client relies on ``action['views']``, generate it here from
596 ``view_mode`` and ``view_id``.
598 Currently handles two different cases:
600 * no view_id, multiple view_mode
601 * single view_id, single view_mode
603 :param dict action: action descriptor dictionary to generate a views key for
605 view_id = action.get('view_id', False)
606 if isinstance(view_id, (list, tuple)):
609 # providing at least one view mode is a requirement, not an option
610 view_modes = action['view_mode'].split(',')
612 if len(view_modes) > 1:
614 raise ValueError('Non-db action dictionaries should provide '
615 'either multiple view modes or a single view '
616 'mode and an optional view id.\n\n Got view '
617 'modes %r and view id %r for action %r' % (
618 view_modes, view_id, action))
619 action['views'] = [(False, mode) for mode in view_modes]
621 action['views'] = [(view_id, view_modes[0])]
623 def fix_view_modes(action):
624 """ For historical reasons, OpenERP has weird dealings in relation to
625 view_mode and the view_type attribute (on window actions):
627 * one of the view modes is ``tree``, which stands for both list views
629 * the choice is made by checking ``view_type``, which is either
630 ``form`` for a list view or ``tree`` for an actual tree view
632 This methods simply folds the view_type into view_mode by adding a
633 new view mode ``list`` which is the result of the ``tree`` view_mode
634 in conjunction with the ``form`` view_type.
636 TODO: this should go into the doc, some kind of "peculiarities" section
638 :param dict action: an action descriptor
639 :returns: nothing, the action is modified in place
641 if not action.get('views'):
642 generate_views(action)
645 for index, (id, mode) in enumerate(action['views']):
649 if id_form is not None:
650 action['views'].insert(index + 1, (id_form, 'page'))
652 if action.pop('view_type', 'form') != 'form':
656 [id, mode if mode != 'tree' else 'list']
657 for id, mode in action['views']
662 class Menu(openerpweb.Controller):
663 _cp_path = "/web/menu"
665 @openerpweb.jsonrequest
667 return {'data': self.do_load(req)}
669 def do_load(self, req):
670 """ Loads all menu items (all applications and their sub-menus).
672 :param req: A request object, with an OpenERP session attribute
673 :type req: < session -> OpenERPSession >
674 :return: the menu root
675 :rtype: dict('children': menu_nodes)
677 Menus = req.session.model('ir.ui.menu')
678 # menus are loaded fully unlike a regular tree view, cause there are
679 # less than 512 items
680 context = req.session.eval_context(req.context)
681 menu_ids = Menus.search([], 0, False, False, context)
682 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
683 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
684 menu_items.append(menu_root)
686 # make a tree using parent_id
687 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
688 for menu_item in menu_items:
689 if menu_item['parent_id']:
690 parent = menu_item['parent_id'][0]
693 if parent in menu_items_map:
694 menu_items_map[parent].setdefault(
695 'children', []).append(menu_item)
697 # sort by sequence a tree using parent_id
698 for menu_item in menu_items:
699 menu_item.setdefault('children', []).sort(
700 key=lambda x:x["sequence"])
704 @openerpweb.jsonrequest
705 def action(self, req, menu_id):
706 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
707 [('ir.ui.menu', menu_id)], False)
708 return {"action": actions}
710 class DataSet(openerpweb.Controller):
711 _cp_path = "/web/dataset"
713 @openerpweb.jsonrequest
714 def fields(self, req, model):
715 return {'fields': req.session.model(model).fields_get(False,
716 req.session.eval_context(req.context))}
718 @openerpweb.jsonrequest
719 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
720 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
721 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
723 """ Performs a search() followed by a read() (if needed) using the
724 provided search criteria
726 :param req: a JSON-RPC request object
727 :type req: openerpweb.JsonRequest
728 :param str model: the name of the model to search on
729 :param fields: a list of the fields to return in the result records
731 :param int offset: from which index should the results start being returned
732 :param int limit: the maximum number of records to return
733 :param list domain: the search domain for the query
734 :param list sort: sorting directives
735 :returns: A structure (dict) with two keys: ids (all the ids matching
736 the (domain, context) pair) and records (paginated records
737 matching fields selection set)
740 Model = req.session.model(model)
742 context, domain = eval_context_and_domain(
743 req.session, req.context, domain)
745 ids = Model.search(domain, 0, False, sort or False, context)
746 # need to fill the dataset with all ids for the (domain, context) pair,
747 # so search un-paginated and paginate manually before reading
748 paginated_ids = ids[offset:(offset + limit if limit else None)]
749 if fields and fields == ['id']:
750 # shortcut read if we only want the ids
753 'records': map(lambda id: {'id': id}, paginated_ids)
756 records = Model.read(paginated_ids, fields or False, context)
757 records.sort(key=lambda obj: ids.index(obj['id']))
764 @openerpweb.jsonrequest
765 def read(self, req, model, ids, fields=False):
766 return self.do_search_read(req, model, ids, fields)
768 @openerpweb.jsonrequest
769 def get(self, req, model, ids, fields=False):
770 return self.do_get(req, model, ids, fields)
772 def do_get(self, req, model, ids, fields=False):
773 """ Fetches and returns the records of the model ``model`` whose ids
776 The results are in the same order as the inputs, but elements may be
777 missing (if there is no record left for the id)
779 :param req: the JSON-RPC2 request object
780 :type req: openerpweb.JsonRequest
781 :param model: the model to read from
783 :param ids: a list of identifiers
785 :param fields: a list of fields to fetch, ``False`` or empty to fetch
786 all fields in the model
787 :type fields: list | False
788 :returns: a list of records, in the same order as the list of ids
791 Model = req.session.model(model)
792 records = Model.read(ids, fields, req.session.eval_context(req.context))
794 record_map = dict((record['id'], record) for record in records)
796 return [record_map[id] for id in ids if record_map.get(id)]
798 @openerpweb.jsonrequest
799 def load(self, req, model, id, fields):
800 m = req.session.model(model)
802 r = m.read([id], False, req.session.eval_context(req.context))
805 return {'value': value}
807 @openerpweb.jsonrequest
808 def create(self, req, model, data):
809 m = req.session.model(model)
810 r = m.create(data, req.session.eval_context(req.context))
813 @openerpweb.jsonrequest
814 def save(self, req, model, id, data):
815 m = req.session.model(model)
816 r = m.write([id], data, req.session.eval_context(req.context))
819 @openerpweb.jsonrequest
820 def unlink(self, req, model, ids=()):
821 Model = req.session.model(model)
822 return Model.unlink(ids, req.session.eval_context(req.context))
824 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
825 has_domain = domain_id is not None and domain_id < len(args)
826 has_context = context_id is not None and context_id < len(args)
828 domain = args[domain_id] if has_domain else []
829 context = args[context_id] if has_context else {}
830 c, d = eval_context_and_domain(req.session, context, domain)
836 return self._call_kw(req, model, method, args, {})
838 def _call_kw(self, req, model, method, args, kwargs):
839 for i in xrange(len(args)):
840 if isinstance(args[i], web.common.nonliterals.BaseContext):
841 args[i] = req.session.eval_context(args[i])
842 elif isinstance(args[i], web.common.nonliterals.BaseDomain):
843 args[i] = req.session.eval_domain(args[i])
844 for k in kwargs.keys():
845 if isinstance(kwargs[k], web.common.nonliterals.BaseContext):
846 kwargs[k] = req.session.eval_context(kwargs[k])
847 elif isinstance(kwargs[k], web.common.nonliterals.BaseDomain):
848 kwargs[k] = req.session.eval_domain(kwargs[k])
850 return getattr(req.session.model(model), method)(*args, **kwargs)
852 @openerpweb.jsonrequest
853 def call(self, req, model, method, args, domain_id=None, context_id=None):
854 return self.call_common(req, model, method, args, domain_id, context_id)
856 @openerpweb.jsonrequest
857 def call_kw(self, req, model, method, args, kwargs):
858 return self._call_kw(req, model, method, args, kwargs)
860 @openerpweb.jsonrequest
861 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
862 action = self.call_common(req, model, method, args, domain_id, context_id)
863 if isinstance(action, dict) and action.get('type') != '':
864 return {'result': clean_action(req, action)}
865 return {'result': False}
867 @openerpweb.jsonrequest
868 def exec_workflow(self, req, model, id, signal):
869 r = req.session.exec_workflow(model, id, signal)
872 @openerpweb.jsonrequest
873 def default_get(self, req, model, fields):
874 Model = req.session.model(model)
875 return Model.default_get(fields, req.session.eval_context(req.context))
877 @openerpweb.jsonrequest
878 def name_search(self, req, model, search_str, domain=[], context={}):
879 m = req.session.model(model)
880 r = m.name_search(search_str+'%', domain, '=ilike', context)
883 class DataGroup(openerpweb.Controller):
884 _cp_path = "/web/group"
885 @openerpweb.jsonrequest
886 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
887 Model = req.session.model(model)
888 context, domain = eval_context_and_domain(req.session, req.context, domain)
890 return Model.read_group(
891 domain or [], fields, group_by_fields, 0, False,
892 dict(context, group_by=group_by_fields), sort or False)
894 class View(openerpweb.Controller):
895 _cp_path = "/web/view"
897 def fields_view_get(self, req, model, view_id, view_type,
898 transform=True, toolbar=False, submenu=False):
899 Model = req.session.model(model)
900 context = req.session.eval_context(req.context)
901 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
902 # todo fme?: check that we should pass the evaluated context here
903 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
904 if toolbar and transform:
905 self.process_toolbar(req, fvg['toolbar'])
908 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
909 # depending on how it feels, xmlrpclib.ServerProxy can translate
910 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
911 # enjoy unicode strings which can not be trivially converted to
912 # strings, and it blows up during parsing.
914 # So ensure we fix this retardation by converting view xml back to
916 if isinstance(fvg['arch'], unicode):
917 arch = fvg['arch'].encode('utf-8')
922 evaluation_context = session.evaluation_context(context or {})
923 xml = self.transform_view(arch, session, evaluation_context)
925 xml = ElementTree.fromstring(arch)
926 fvg['arch'] = web.common.xml2json.Xml2Json.convert_element(xml, preserve_whitespaces)
928 for field in fvg['fields'].itervalues():
929 if field.get('views'):
930 for view in field["views"].itervalues():
931 self.process_view(session, view, None, transform)
932 if field.get('domain'):
933 field["domain"] = parse_domain(field["domain"], session)
934 if field.get('context'):
935 field["context"] = parse_context(field["context"], session)
937 def process_toolbar(self, req, toolbar):
939 The toolbar is a mapping of section_key: [action_descriptor]
941 We need to clean all those actions in order to ensure correct
944 for actions in toolbar.itervalues():
945 for action in actions:
946 if 'context' in action:
947 action['context'] = parse_context(
948 action['context'], req.session)
949 if 'domain' in action:
950 action['domain'] = parse_domain(
951 action['domain'], req.session)
953 @openerpweb.jsonrequest
954 def add_custom(self, req, view_id, arch):
955 CustomView = req.session.model('ir.ui.view.custom')
957 'user_id': req.session._uid,
960 }, req.session.eval_context(req.context))
961 return {'result': True}
963 @openerpweb.jsonrequest
964 def undo_custom(self, req, view_id, reset=False):
965 CustomView = req.session.model('ir.ui.view.custom')
966 context = req.session.eval_context(req.context)
967 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
968 0, False, False, context)
971 CustomView.unlink(vcustom, context)
973 CustomView.unlink([vcustom[0]], context)
974 return {'result': True}
975 return {'result': False}
977 def transform_view(self, view_string, session, context=None):
978 # transform nodes on the fly via iterparse, instead of
979 # doing it statically on the parsing result
980 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
982 for event, elem in parser:
986 self.parse_domains_and_contexts(elem, session)
989 def parse_domains_and_contexts(self, elem, session):
990 """ Converts domains and contexts from the view into Python objects,
991 either literals if they can be parsed by literal_eval or a special
992 placeholder object if the domain or context refers to free variables.
994 :param elem: the current node being parsed
995 :type param: xml.etree.ElementTree.Element
996 :param session: OpenERP session object, used to store and retrieve
998 :type session: openerpweb.openerpweb.OpenERPSession
1000 for el in ['domain', 'filter_domain']:
1001 domain = elem.get(el, '').strip()
1003 elem.set(el, parse_domain(domain, session))
1004 elem.set(el + '_string', domain)
1005 for el in ['context', 'default_get']:
1006 context_string = elem.get(el, '').strip()
1008 elem.set(el, parse_context(context_string, session))
1009 elem.set(el + '_string', context_string)
1011 @openerpweb.jsonrequest
1012 def load(self, req, model, view_id, view_type, toolbar=False):
1013 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1015 def parse_domain(domain, session):
1016 """ Parses an arbitrary string containing a domain, transforms it
1017 to either a literal domain or a :class:`web.common.nonliterals.Domain`
1019 :param domain: the domain to parse, if the domain is not a string it
1020 is assumed to be a literal domain and is returned as-is
1021 :param session: Current OpenERP session
1022 :type session: openerpweb.openerpweb.OpenERPSession
1024 if not isinstance(domain, (str, unicode)):
1027 return ast.literal_eval(domain)
1030 return web.common.nonliterals.Domain(session, domain)
1032 def parse_context(context, session):
1033 """ Parses an arbitrary string containing a context, transforms it
1034 to either a literal context or a :class:`web.common.nonliterals.Context`
1036 :param context: the context to parse, if the context is not a string it
1037 is assumed to be a literal domain and is returned as-is
1038 :param session: Current OpenERP session
1039 :type session: openerpweb.openerpweb.OpenERPSession
1041 if not isinstance(context, (str, unicode)):
1044 return ast.literal_eval(context)
1046 return web.common.nonliterals.Context(session, context)
1048 class ListView(View):
1049 _cp_path = "/web/listview"
1051 def process_colors(self, view, row, context):
1052 colors = view['arch']['attrs'].get('colors')
1059 for pair in colors.split(';')
1060 if eval(pair.split(':')[1], dict(context, **row))
1065 elif len(color) == 1:
1069 class TreeView(View):
1070 _cp_path = "/web/treeview"
1072 @openerpweb.jsonrequest
1073 def action(self, req, model, id):
1074 return load_actions_from_ir_values(
1075 req,'action', 'tree_but_open',[(model, id)],
1078 class SearchView(View):
1079 _cp_path = "/web/searchview"
1081 @openerpweb.jsonrequest
1082 def load(self, req, model, view_id):
1083 fields_view = self.fields_view_get(req, model, view_id, 'search')
1084 return {'fields_view': fields_view}
1086 @openerpweb.jsonrequest
1087 def fields_get(self, req, model):
1088 Model = req.session.model(model)
1089 fields = Model.fields_get(False, req.session.eval_context(req.context))
1090 for field in fields.values():
1091 # shouldn't convert the views too?
1092 if field.get('domain'):
1093 field["domain"] = parse_domain(field["domain"], req.session)
1094 if field.get('context'):
1095 field["context"] = parse_context(field["context"], req.session)
1096 return {'fields': fields}
1098 @openerpweb.jsonrequest
1099 def get_filters(self, req, model):
1100 Model = req.session.model("ir.filters")
1101 filters = Model.get_filters(model)
1102 for filter in filters:
1103 filter["context"] = req.session.eval_context(parse_context(filter["context"], req.session))
1104 filter["domain"] = req.session.eval_domain(parse_domain(filter["domain"], req.session))
1107 @openerpweb.jsonrequest
1108 def save_filter(self, req, model, name, context_to_save, domain):
1109 Model = req.session.model("ir.filters")
1110 ctx = web.common.nonliterals.CompoundContext(context_to_save)
1111 ctx.session = req.session
1112 ctx = ctx.evaluate()
1113 domain = web.common.nonliterals.CompoundDomain(domain)
1114 domain.session = req.session
1115 domain = domain.evaluate()
1116 uid = req.session._uid
1117 context = req.session.eval_context(req.context)
1118 to_return = Model.create_or_replace({"context": ctx,
1126 @openerpweb.jsonrequest
1127 def add_to_dashboard(self, req, menu_id, action_id, context_to_save, domain, view_mode, name=''):
1128 ctx = web.common.nonliterals.CompoundContext(context_to_save)
1129 ctx.session = req.session
1130 ctx = ctx.evaluate()
1131 domain = web.common.nonliterals.CompoundDomain(domain)
1132 domain.session = req.session
1133 domain = domain.evaluate()
1135 dashboard_action = load_actions_from_ir_values(req, 'action', 'tree_but_open',
1136 [('ir.ui.menu', menu_id)], False)
1137 if dashboard_action:
1138 action = dashboard_action[0][2]
1139 if action['res_model'] == 'board.board' and action['views'][0][1] == 'form':
1140 # Maybe should check the content instead of model board.board ?
1141 view_id = action['views'][0][0]
1142 board = req.session.model(action['res_model']).fields_view_get(view_id, 'form')
1143 if board and 'arch' in board:
1144 xml = ElementTree.fromstring(board['arch'])
1145 column = xml.find('./board/column')
1147 new_action = ElementTree.Element('action', {
1148 'name' : str(action_id),
1150 'view_mode' : view_mode,
1151 'context' : str(ctx),
1152 'domain' : str(domain)
1154 column.insert(0, new_action)
1155 arch = ElementTree.tostring(xml, 'utf-8')
1156 return req.session.model('ir.ui.view.custom').create({
1157 'user_id': req.session._uid,
1160 }, req.session.eval_context(req.context))
1164 class Binary(openerpweb.Controller):
1165 _cp_path = "/web/binary"
1167 @openerpweb.httprequest
1168 def image(self, req, model, id, field, **kw):
1169 Model = req.session.model(model)
1170 context = req.session.eval_context(req.context)
1174 res = Model.default_get([field], context).get(field)
1176 res = Model.read([int(id)], [field], context)[0].get(field)
1177 image_data = base64.b64decode(res)
1178 except (TypeError, xmlrpclib.Fault):
1179 image_data = self.placeholder(req)
1180 return req.make_response(image_data, [
1181 ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
1182 def placeholder(self, req):
1183 addons_path = openerpweb.addons_manifest['web']['addons_path']
1184 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1186 @openerpweb.httprequest
1187 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1188 """ Download link for files stored as binary fields.
1190 If the ``id`` parameter is omitted, fetches the default value for the
1191 binary field (via ``default_get``), otherwise fetches the field for
1192 that precise record.
1194 :param req: OpenERP request
1195 :type req: :class:`web.common.http.HttpRequest`
1196 :param str model: name of the model to fetch the binary from
1197 :param str field: binary field
1198 :param str id: id of the record from which to fetch the binary
1199 :param str filename_field: field holding the file's name, if any
1200 :returns: :class:`werkzeug.wrappers.Response`
1202 Model = req.session.model(model)
1203 context = req.session.eval_context(req.context)
1206 fields.append(filename_field)
1208 res = Model.read([int(id)], fields, context)[0]
1210 res = Model.default_get(fields, context)
1211 filecontent = base64.b64decode(res.get(field, ''))
1213 return req.not_found()
1215 filename = '%s_%s' % (model.replace('.', '_'), id)
1217 filename = res.get(filename_field, '') or filename
1218 return req.make_response(filecontent,
1219 [('Content-Type', 'application/octet-stream'),
1220 ('Content-Disposition', 'attachment; filename=' + filename)])
1222 @openerpweb.httprequest
1223 def upload(self, req, callback, ufile):
1224 # TODO: might be useful to have a configuration flag for max-length file uploads
1226 out = """<script language="javascript" type="text/javascript">
1227 var win = window.top.window,
1229 if (typeof(callback) === 'function') {
1230 callback.apply(this, %s);
1232 win.jQuery('#oe_notification', win.document).notify('create', {
1233 title: "Ajax File Upload",
1234 text: "Could not find callback"
1239 args = [len(data), ufile.filename,
1240 ufile.content_type, base64.b64encode(data)]
1241 except Exception, e:
1242 args = [False, e.message]
1243 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1245 @openerpweb.httprequest
1246 def upload_attachment(self, req, callback, model, id, ufile):
1247 context = req.session.eval_context(req.context)
1248 Model = req.session.model('ir.attachment')
1250 out = """<script language="javascript" type="text/javascript">
1251 var win = window.top.window,
1253 if (typeof(callback) === 'function') {
1254 callback.call(this, %s);
1257 attachment_id = Model.create({
1258 'name': ufile.filename,
1259 'datas': base64.encodestring(ufile.read()),
1264 'filename': ufile.filename,
1267 except Exception, e:
1268 args = { 'error': e.message }
1269 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1271 class Action(openerpweb.Controller):
1272 _cp_path = "/web/action"
1274 @openerpweb.jsonrequest
1275 def load(self, req, action_id, do_not_eval=False):
1276 Actions = req.session.model('ir.actions.actions')
1278 context = req.session.eval_context(req.context)
1279 action_type = Actions.read([action_id], ['type'], context)
1282 if action_type[0]['type'] == 'ir.actions.report.xml':
1283 ctx.update({'bin_size': True})
1285 action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
1287 value = clean_action(req, action[0], do_not_eval)
1288 return {'result': value}
1290 @openerpweb.jsonrequest
1291 def run(self, req, action_id):
1292 return clean_action(req, req.session.model('ir.actions.server').run(
1293 [action_id], req.session.eval_context(req.context)))
1296 _cp_path = "/web/export"
1298 @openerpweb.jsonrequest
1299 def formats(self, req):
1300 """ Returns all valid export formats
1302 :returns: for each export format, a pair of identifier and printable name
1303 :rtype: [(str, str)]
1307 for path, controller in openerpweb.controllers_path.iteritems()
1308 if path.startswith(self._cp_path)
1309 if hasattr(controller, 'fmt')
1310 ], key=operator.itemgetter(1))
1312 def fields_get(self, req, model):
1313 Model = req.session.model(model)
1314 fields = Model.fields_get(False, req.session.eval_context(req.context))
1317 @openerpweb.jsonrequest
1318 def get_fields(self, req, model, prefix='', parent_name= '',
1319 import_compat=True, parent_field_type=None,
1322 if import_compat and parent_field_type == "many2one":
1325 fields = self.fields_get(req, model)
1328 fields.pop('id', None)
1330 fields['.id'] = fields.pop('id', {'string': 'ID'})
1332 fields_sequence = sorted(fields.iteritems(),
1333 key=lambda field: field[1].get('string', ''))
1336 for field_name, field in fields_sequence:
1338 if exclude and field_name in exclude:
1340 if 'function' in field:
1342 if field.get('readonly'):
1343 # If none of the field's states unsets readonly, skip the field
1344 if all(dict(attrs).get('readonly', True)
1345 for attrs in field.get('states', {}).values()):
1348 id = prefix + (prefix and '/'or '') + field_name
1349 name = parent_name + (parent_name and '/' or '') + field['string']
1350 record = {'id': id, 'string': name,
1351 'value': id, 'children': False,
1352 'field_type': field.get('type'),
1353 'required': field.get('required'),
1354 'relation_field': field.get('relation_field')}
1355 records.append(record)
1357 if len(name.split('/')) < 3 and 'relation' in field:
1358 ref = field.pop('relation')
1359 record['value'] += '/id'
1360 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1362 if not import_compat or field['type'] == 'one2many':
1363 # m2m field in import_compat is childless
1364 record['children'] = True
1368 @openerpweb.jsonrequest
1369 def namelist(self,req, model, export_id):
1370 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1371 export = req.session.model("ir.exports").read([export_id])[0]
1372 export_fields_list = req.session.model("ir.exports.line").read(
1373 export['export_fields'])
1375 fields_data = self.fields_info(
1376 req, model, map(operator.itemgetter('name'), export_fields_list))
1379 {'name': field['name'], 'label': fields_data[field['name']]}
1380 for field in export_fields_list
1383 def fields_info(self, req, model, export_fields):
1385 fields = self.fields_get(req, model)
1387 # To make fields retrieval more efficient, fetch all sub-fields of a
1388 # given field at the same time. Because the order in the export list is
1389 # arbitrary, this requires ordering all sub-fields of a given field
1390 # together so they can be fetched at the same time
1392 # Works the following way:
1393 # * sort the list of fields to export, the default sorting order will
1394 # put the field itself (if present, for xmlid) and all of its
1395 # sub-fields right after it
1396 # * then, group on: the first field of the path (which is the same for
1397 # a field and for its subfields and the length of splitting on the
1398 # first '/', which basically means grouping the field on one side and
1399 # all of the subfields on the other. This way, we have the field (for
1400 # the xmlid) with length 1, and all of the subfields with the same
1401 # base but a length "flag" of 2
1402 # * if we have a normal field (length 1), just add it to the info
1403 # mapping (with its string) as-is
1404 # * otherwise, recursively call fields_info via graft_subfields.
1405 # all graft_subfields does is take the result of fields_info (on the
1406 # field's model) and prepend the current base (current field), which
1407 # rebuilds the whole sub-tree for the field
1409 # result: because we're not fetching the fields_get for half the
1410 # database models, fetching a namelist with a dozen fields (including
1411 # relational data) falls from ~6s to ~300ms (on the leads model).
1412 # export lists with no sub-fields (e.g. import_compatible lists with
1413 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1414 # there's a single fields_get to execute)
1415 for (base, length), subfields in itertools.groupby(
1416 sorted(export_fields),
1417 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1418 subfields = list(subfields)
1420 # subfields is a seq of $base/*rest, and not loaded yet
1421 info.update(self.graft_subfields(
1422 req, fields[base]['relation'], base, fields[base]['string'],
1426 info[base] = fields[base]['string']
1430 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1431 export_fields = [field.split('/', 1)[1] for field in fields]
1433 (prefix + '/' + k, prefix_string + '/' + v)
1434 for k, v in self.fields_info(req, model, export_fields).iteritems())
1436 #noinspection PyPropertyDefinition
1438 def content_type(self):
1439 """ Provides the format's content type """
1440 raise NotImplementedError()
1442 def filename(self, base):
1443 """ Creates a valid filename for the format (with extension) from the
1444 provided base name (exension-less)
1446 raise NotImplementedError()
1448 def from_data(self, fields, rows):
1449 """ Conversion method from OpenERP's export data to whatever the
1450 current export class outputs
1452 :params list fields: a list of fields to export
1453 :params list rows: a list of records to export
1457 raise NotImplementedError()
1459 @openerpweb.httprequest
1460 def index(self, req, data, token):
1461 model, fields, ids, domain, import_compat = \
1462 operator.itemgetter('model', 'fields', 'ids', 'domain',
1464 simplejson.loads(data))
1466 context = req.session.eval_context(req.context)
1467 Model = req.session.model(model)
1468 ids = ids or Model.search(domain, 0, False, False, context)
1470 field_names = map(operator.itemgetter('name'), fields)
1471 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1474 columns_headers = field_names
1476 columns_headers = [val['label'].strip() for val in fields]
1479 return req.make_response(self.from_data(columns_headers, import_data),
1480 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1481 ('Content-Type', self.content_type)],
1482 cookies={'fileToken': int(token)})
1484 class CSVExport(Export):
1485 _cp_path = '/web/export/csv'
1486 fmt = ('csv', 'CSV')
1489 def content_type(self):
1490 return 'text/csv;charset=utf8'
1492 def filename(self, base):
1493 return base + '.csv'
1495 def from_data(self, fields, rows):
1497 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1499 writer.writerow(fields)
1504 if isinstance(d, basestring):
1505 d = d.replace('\n',' ').replace('\t',' ')
1507 d = d.encode('utf-8')
1510 if d is False: d = None
1512 writer.writerow(row)
1519 class ExcelExport(Export):
1520 _cp_path = '/web/export/xls'
1521 fmt = ('xls', 'Excel')
1524 def content_type(self):
1525 return 'application/vnd.ms-excel'
1527 def filename(self, base):
1528 return base + '.xls'
1530 def from_data(self, fields, rows):
1533 workbook = xlwt.Workbook()
1534 worksheet = workbook.add_sheet('Sheet 1')
1536 for i, fieldname in enumerate(fields):
1537 worksheet.write(0, i, str(fieldname))
1538 worksheet.col(i).width = 8000 # around 220 pixels
1540 style = xlwt.easyxf('align: wrap yes')
1542 for row_index, row in enumerate(rows):
1543 for cell_index, cell_value in enumerate(row):
1544 if isinstance(cell_value, basestring):
1545 cell_value = re.sub("\r", " ", cell_value)
1546 if cell_value is False: cell_value = None
1547 worksheet.write(row_index + 1, cell_index, cell_value, style)
1556 class Reports(View):
1557 _cp_path = "/web/report"
1558 POLLING_DELAY = 0.25
1560 'doc': 'application/vnd.ms-word',
1561 'html': 'text/html',
1562 'odt': 'application/vnd.oasis.opendocument.text',
1563 'pdf': 'application/pdf',
1564 'sxw': 'application/vnd.sun.xml.writer',
1565 'xls': 'application/vnd.ms-excel',
1568 @openerpweb.httprequest
1569 def index(self, req, action, token):
1570 action = simplejson.loads(action)
1572 report_srv = req.session.proxy("report")
1573 context = req.session.eval_context(
1574 web.common.nonliterals.CompoundContext(
1575 req.context or {}, action[ "context"]))
1578 report_ids = context["active_ids"]
1579 if 'report_type' in action:
1580 report_data['report_type'] = action['report_type']
1581 if 'datas' in action:
1582 if 'ids' in action['datas']:
1583 report_ids = action['datas'].pop('ids')
1584 report_data.update(action['datas'])
1586 report_id = report_srv.report(
1587 req.session._db, req.session._uid, req.session._password,
1588 action["report_name"], report_ids,
1589 report_data, context)
1591 report_struct = None
1593 report_struct = report_srv.report_get(
1594 req.session._db, req.session._uid, req.session._password, report_id)
1595 if report_struct["state"]:
1598 time.sleep(self.POLLING_DELAY)
1600 report = base64.b64decode(report_struct['result'])
1601 if report_struct.get('code') == 'zlib':
1602 report = zlib.decompress(report)
1603 report_mimetype = self.TYPES_MAPPING.get(
1604 report_struct['format'], 'octet-stream')
1605 return req.make_response(report,
1607 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
1608 ('Content-Type', report_mimetype),
1609 ('Content-Length', len(report))],
1610 cookies={'fileToken': int(token)})
1613 _cp_path = "/web/import"
1615 def fields_get(self, req, model):
1616 Model = req.session.model(model)
1617 fields = Model.fields_get(False, req.session.eval_context(req.context))
1620 @openerpweb.httprequest
1621 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1623 data = list(csv.reader(
1624 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1625 except csv.Error, e:
1627 return '<script>window.top.%s(%s);</script>' % (
1628 jsonp, simplejson.dumps({'error': {
1629 'message': 'Error parsing CSV file: %s' % e,
1630 # decodes each byte to a unicode character, which may or
1631 # may not be printable, but decoding will succeed.
1632 # Otherwise simplejson will try to decode the `str` using
1633 # utf-8, which is very likely to blow up on characters out
1634 # of the ascii range (in range [128, 256))
1635 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1638 return '<script>window.top.%s(%s);</script>' % (
1639 jsonp, simplejson.dumps(
1640 {'records': data[:10]}, encoding=csvcode))
1641 except UnicodeDecodeError:
1642 return '<script>window.top.%s(%s);</script>' % (
1643 jsonp, simplejson.dumps({
1644 'message': u"Failed to decode CSV file using encoding %s, "
1645 u"try switching to a different encoding" % csvcode
1648 @openerpweb.httprequest
1649 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1651 modle_obj = req.session.model(model)
1652 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1653 simplejson.loads(meta))
1656 if not (csvdel and len(csvdel) == 1):
1657 error = u"The CSV delimiter must be a single character"
1659 if not indices and fields:
1660 error = u"You must select at least one field to import"
1663 return '<script>window.top.%s(%s);</script>' % (
1664 jsonp, simplejson.dumps({'error': {'message': error}}))
1666 # skip ignored records
1667 data_record = itertools.islice(
1668 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1671 # if only one index, itemgetter will return an atom rather than a tuple
1672 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1673 else: mapper = operator.itemgetter(*indices)
1678 # decode each data row
1680 [record.decode(csvcode) for record in row]
1681 for row in itertools.imap(mapper, data_record)
1682 # don't insert completely empty rows (can happen due to fields
1683 # filtering in case of e.g. o2m content rows)
1686 except UnicodeDecodeError:
1687 error = u"Failed to decode CSV file using encoding %s" % csvcode
1688 except csv.Error, e:
1689 error = u"Could not process CSV file: %s" % e
1691 # If the file contains nothing,
1693 error = u"File to import is empty"
1695 return '<script>window.top.%s(%s);</script>' % (
1696 jsonp, simplejson.dumps({'error': {'message': error}}))
1699 (code, record, message, _nope) = modle_obj.import_data(
1700 fields, data, 'init', '', False,
1701 req.session.eval_context(req.context))
1702 except xmlrpclib.Fault, e:
1703 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1704 return '<script>window.top.%s(%s);</script>' % (
1705 jsonp, simplejson.dumps({'error':error}))
1708 return '<script>window.top.%s(%s);</script>' % (
1709 jsonp, simplejson.dumps({'success':True}))
1711 msg = u"Error during import: %s\n\nTrying to import record %r" % (
1713 return '<script>window.top.%s(%s);</script>' % (
1714 jsonp, simplejson.dumps({'error': {'message':msg}}))