1 # -*- coding: utf-8 -*-
19 from xml.etree import ElementTree
20 from cStringIO import StringIO
22 import babel.messages.pofile
24 import werkzeug.wrappers
31 openerpweb = common.http
33 #----------------------------------------------------------
34 # OpenERP Web web Controllers
35 #----------------------------------------------------------
38 def concat_xml(file_list):
39 """Concatenate xml files
41 :param list(str) file_list: list of files to check
42 :returns: (concatenation_result, checksum)
45 checksum = hashlib.new('sha1')
47 return '', checksum.hexdigest()
50 for fname in file_list:
51 with open(fname, 'rb') as fp:
53 checksum.update(contents)
55 xml = ElementTree.parse(fp).getroot()
58 root = ElementTree.Element(xml.tag)
59 #elif root.tag != xml.tag:
60 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
62 for child in xml.getchildren():
64 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
67 def concat_files(file_list, reader=None, intersperse=""):
68 """ Concatenates contents of all provided files
70 :param list(str) file_list: list of files to check
71 :param function reader: reading procedure for each file
72 :param str intersperse: string to intersperse between file contents
73 :returns: (concatenation_result, checksum)
76 checksum = hashlib.new('sha1')
78 return '', checksum.hexdigest()
86 for fname in file_list:
87 contents = reader(fname)
88 checksum.update(contents)
89 files_content.append(contents)
91 files_concat = intersperse.join(files_content)
92 return files_concat, checksum.hexdigest()
94 html_template = """<!DOCTYPE html>
95 <html style="height: 100%%">
97 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
98 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
99 <title>OpenERP</title>
100 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
103 <script type="text/javascript">
105 var s = new openerp.init(%(modules)s);
110 <body class="openerp" id="oe"></body>
114 class WebClient(openerpweb.Controller):
115 _cp_path = "/web/webclient"
117 def server_wide_modules(self, req):
118 addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest]
121 def manifest_glob(self, req, addons, key):
123 addons = self.server_wide_modules(req)
125 addons = addons.split(',')
128 manifest = openerpweb.addons_manifest.get(addon, None)
131 # ensure does not ends with /
132 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
133 globlist = manifest.get(key, [])
134 for pattern in globlist:
135 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
136 r.append( (path, path[len(addons_path):]))
139 def manifest_list(self, req, mods, extension):
141 path = '/web/webclient/' + extension
143 path += '?mods=' + mods
145 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in self.manifest_glob(req, mods, extension)]
147 @openerpweb.jsonrequest
148 def csslist(self, req, mods=None):
149 return self.manifest_list(req, mods, 'css')
151 @openerpweb.jsonrequest
152 def jslist(self, req, mods=None):
153 return self.manifest_list(req, mods, 'js')
155 @openerpweb.jsonrequest
156 def qweblist(self, req, mods=None):
157 return self.manifest_list(req, mods, 'qweb')
159 def get_last_modified(self, files):
160 """ Returns the modification time of the most recently modified
163 :param list(str) files: names of files to check
164 :return: most recent modification time amongst the fileset
165 :rtype: datetime.datetime
169 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
171 return datetime.datetime(1970, 1, 1)
173 def make_conditional(self, req, response, last_modified=None, etag=None):
174 """ Makes the provided response conditional based upon the request,
175 and mandates revalidation from clients
177 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
178 setting ``last_modified`` and ``etag`` correctly on the response object
180 :param req: OpenERP request
181 :type req: web.common.http.WebRequest
182 :param response: Werkzeug response
183 :type response: werkzeug.wrappers.Response
184 :param datetime.datetime last_modified: last modification date of the response content
185 :param str etag: some sort of checksum of the content (deep etag)
186 :return: the response object provided
187 :rtype: werkzeug.wrappers.Response
189 response.cache_control.must_revalidate = True
190 response.cache_control.max_age = 0
192 response.last_modified = last_modified
194 response.set_etag(etag)
195 return response.make_conditional(req.httprequest)
197 @openerpweb.httprequest
198 def css(self, req, mods=None):
199 files = list(self.manifest_glob(req, mods, 'css'))
200 last_modified = self.get_last_modified(f[0] for f in files)
201 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
202 return werkzeug.wrappers.Response(status=304)
204 file_map = dict(files)
206 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
207 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://)""", re.U)
211 """read the a css file and absolutify all relative uris"""
216 # convert FS path into web path
217 web_dir = '/'.join(os.path.dirname(path).split(os.path.sep))
221 r"""@import \1%s/""" % (web_dir,),
227 r"""url(\1%s/""" % (web_dir,),
232 content, checksum = concat_files((f[0] for f in files), reader)
234 return self.make_conditional(
235 req, req.make_response(content, [('Content-Type', 'text/css')]),
236 last_modified, checksum)
238 @openerpweb.httprequest
239 def js(self, req, mods=None):
240 files = [f[0] for f in self.manifest_glob(req, mods, 'js')]
241 last_modified = self.get_last_modified(files)
242 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
243 return werkzeug.wrappers.Response(status=304)
245 content, checksum = concat_files(files, intersperse=';')
247 return self.make_conditional(
248 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
249 last_modified, checksum)
251 @openerpweb.httprequest
252 def qweb(self, req, mods=None):
253 files = [f[0] for f in self.manifest_glob(req, mods, 'qweb')]
254 last_modified = self.get_last_modified(files)
255 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
256 return werkzeug.wrappers.Response(status=304)
258 content,checksum = concat_xml(files)
260 return self.make_conditional(
261 req, req.make_response(content, [('Content-Type', 'text/xml')]),
262 last_modified, checksum)
264 @openerpweb.httprequest
265 def home(self, req, s_action=None, **kw):
266 js = "\n ".join('<script type="text/javascript" src="%s"></script>'%i for i in self.manifest_list(req, None, 'js'))
267 css = "\n ".join('<link rel="stylesheet" href="%s">'%i for i in self.manifest_list(req, None, 'css'))
269 r = html_template % {
272 'modules': simplejson.dumps(self.server_wide_modules(req)),
273 'init': 'new s.web.WebClient().start();',
277 @openerpweb.httprequest
278 def login(self, req, db, login, key):
279 req.session.authenticate(db, login, key, {})
280 redirect = werkzeug.utils.redirect('/web/webclient/home', 303)
281 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
282 redirect.set_cookie('session0|session_id', cookie_val)
285 @openerpweb.jsonrequest
286 def translations(self, req, mods, lang):
287 lang_model = req.session.model('res.lang')
288 ids = lang_model.search([("code", "=", lang)])
290 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
291 "grouping", "decimal_point", "thousands_sep"])
299 langs = lang.split(separator)
300 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
303 for addon_name in mods:
304 transl = {"messages":[]}
305 transs[addon_name] = transl
306 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
308 f_name = os.path.join(addons_path, addon_name, "i18n", l + ".po")
309 if not os.path.exists(f_name):
312 with open(f_name) as t_file:
313 po = babel.messages.pofile.read_po(t_file)
317 if x.id and x.string and "openerp-web" in x.auto_comments:
318 transl["messages"].append({'id': x.id, 'string': x.string})
319 return {"modules": transs,
320 "lang_parameters": lang_obj}
322 @openerpweb.jsonrequest
323 def version_info(self, req):
325 "version": common.release.version
328 class Proxy(openerpweb.Controller):
329 _cp_path = '/web/proxy'
331 @openerpweb.jsonrequest
332 def load(self, req, path):
333 """ Proxies an HTTP request through a JSON request.
335 It is strongly recommended to not request binary files through this,
336 as the result will be a binary data blob as well.
338 :param req: OpenERP request
339 :param path: actual request path
340 :return: file content
342 from werkzeug.test import Client
343 from werkzeug.wrappers import BaseResponse
345 return Client(req.httprequest.app, BaseResponse).get(path).data
347 class Database(openerpweb.Controller):
348 _cp_path = "/web/database"
350 @openerpweb.jsonrequest
351 def get_list(self, req):
352 proxy = req.session.proxy("db")
354 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
356 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
357 dbs = [i for i in dbs if re.match(r, i)]
358 return {"db_list": dbs}
360 @openerpweb.jsonrequest
361 def progress(self, req, password, id):
362 return req.session.proxy('db').get_progress(password, id)
364 @openerpweb.jsonrequest
365 def create(self, req, fields):
367 params = dict(map(operator.itemgetter('name', 'value'), fields))
369 params['super_admin_pwd'],
371 bool(params.get('demo_data')),
373 params['create_admin_pwd']
377 return req.session.proxy("db").create(*create_attrs)
378 except xmlrpclib.Fault, e:
379 if e.faultCode and isinstance(e.faultCode, str)\
380 and e.faultCode.split(':')[0] == 'AccessDenied':
381 return {'error': e.faultCode, 'title': 'Database creation error'}
383 'error': "Could not create database '%s': %s" % (
384 params['db_name'], e.faultString),
385 'title': 'Database creation error'
388 @openerpweb.jsonrequest
389 def drop(self, req, fields):
390 password, db = operator.itemgetter(
391 'drop_pwd', 'drop_db')(
392 dict(map(operator.itemgetter('name', 'value'), fields)))
395 return req.session.proxy("db").drop(password, db)
396 except xmlrpclib.Fault, e:
397 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
398 return {'error': e.faultCode, 'title': 'Drop Database'}
399 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
401 @openerpweb.httprequest
402 def backup(self, req, backup_db, backup_pwd, token):
403 db_dump = base64.b64decode(
404 req.session.proxy("db").dump(backup_pwd, backup_db))
405 filename = "%(db)s_%(timestamp)s.dump" % {
407 'timestamp': datetime.datetime.utcnow().strftime(
408 "%Y-%m-%d_%H-%M-%SZ")
410 return req.make_response(db_dump,
411 [('Content-Type', 'application/octet-stream; charset=binary'),
412 ('Content-Disposition', 'attachment; filename="' + filename + '"')],
413 {'fileToken': int(token)}
416 @openerpweb.httprequest
417 def restore(self, req, db_file, restore_pwd, new_db):
419 data = base64.b64encode(db_file.read())
420 req.session.proxy("db").restore(restore_pwd, new_db, data)
422 except xmlrpclib.Fault, e:
423 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
424 raise Exception("AccessDenied")
426 @openerpweb.jsonrequest
427 def change_password(self, req, fields):
428 old_password, new_password = operator.itemgetter(
429 'old_pwd', 'new_pwd')(
430 dict(map(operator.itemgetter('name', 'value'), fields)))
432 return req.session.proxy("db").change_admin_password(old_password, new_password)
433 except xmlrpclib.Fault, e:
434 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
435 return {'error': e.faultCode, 'title': 'Change Password'}
436 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
438 class Session(openerpweb.Controller):
439 _cp_path = "/web/session"
441 def session_info(self, req):
442 req.session.ensure_valid()
444 "session_id": req.session_id,
445 "uid": req.session._uid,
446 "context": req.session.get_context() if req.session._uid else {},
447 "db": req.session._db,
448 "login": req.session._login,
449 "openerp_entreprise": req.session.openerp_entreprise(),
452 @openerpweb.jsonrequest
453 def get_session_info(self, req):
454 return self.session_info(req)
456 @openerpweb.jsonrequest
457 def authenticate(self, req, db, login, password, base_location=None):
458 wsgienv = req.httprequest.environ
459 release = common.release
461 base_location=base_location,
462 HTTP_HOST=wsgienv['HTTP_HOST'],
463 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
464 user_agent="%s / %s" % (release.name, release.version),
466 req.session.authenticate(db, login, password, env)
468 return self.session_info(req)
470 @openerpweb.jsonrequest
471 def change_password (self,req,fields):
472 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
473 dict(map(operator.itemgetter('name', 'value'), fields)))
474 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
475 return {'error':'All passwords have to be filled.','title': 'Change Password'}
476 if new_password != confirm_password:
477 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
479 if req.session.model('res.users').change_password(
480 old_password, new_password):
481 return {'new_password':new_password}
483 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
484 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
486 @openerpweb.jsonrequest
487 def sc_list(self, req):
488 return req.session.model('ir.ui.view_sc').get_sc(
489 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
491 @openerpweb.jsonrequest
492 def get_lang_list(self, req):
495 'lang_list': (req.session.proxy("db").list_lang() or []),
499 return {"error": e, "title": "Languages"}
501 @openerpweb.jsonrequest
502 def modules(self, req):
503 # Compute available candidates module
504 loadable = openerpweb.addons_manifest
505 loaded = req.config.server_wide_modules
506 candidates = [mod for mod in loadable if mod not in loaded]
508 # Compute active true modules that might be on the web side only
509 active = set(name for name in candidates
510 if openerpweb.addons_manifest[name].get('active'))
512 # Retrieve database installed modules
513 Modules = req.session.model('ir.module.module')
514 installed = set(module['name'] for module in Modules.search_read(
515 [('state','=','installed'), ('name','in', candidates)], ['name']))
518 return list(active | installed)
520 @openerpweb.jsonrequest
521 def eval_domain_and_context(self, req, contexts, domains,
523 """ Evaluates sequences of domains and contexts, composing them into
524 a single context, domain or group_by sequence.
526 :param list contexts: list of contexts to merge together. Contexts are
527 evaluated in sequence, all previous contexts
528 are part of their own evaluation context
529 (starting at the session context).
530 :param list domains: list of domains to merge together. Domains are
531 evaluated in sequence and appended to one another
532 (implicit AND), their evaluation domain is the
533 result of merging all contexts.
534 :param list group_by_seq: list of domains (which may be in a different
535 order than the ``contexts`` parameter),
536 evaluated in sequence, their ``'group_by'``
537 key is extracted if they have one.
542 the global context created by merging all of
546 the concatenation of all domains
549 a list of fields to group by, potentially empty (in which case
550 no group by should be performed)
552 context, domain = eval_context_and_domain(req.session,
553 common.nonliterals.CompoundContext(*(contexts or [])),
554 common.nonliterals.CompoundDomain(*(domains or [])))
556 group_by_sequence = []
557 for candidate in (group_by_seq or []):
558 ctx = req.session.eval_context(candidate, context)
559 group_by = ctx.get('group_by')
562 elif isinstance(group_by, basestring):
563 group_by_sequence.append(group_by)
565 group_by_sequence.extend(group_by)
570 'group_by': group_by_sequence
573 @openerpweb.jsonrequest
574 def save_session_action(self, req, the_action):
576 This method store an action object in the session object and returns an integer
577 identifying that action. The method get_session_action() can be used to get
580 :param the_action: The action to save in the session.
581 :type the_action: anything
582 :return: A key identifying the saved action.
585 saved_actions = req.httpsession.get('saved_actions')
586 if not saved_actions:
587 saved_actions = {"next":0, "actions":{}}
588 req.httpsession['saved_actions'] = saved_actions
589 # we don't allow more than 10 stored actions
590 if len(saved_actions["actions"]) >= 10:
591 del saved_actions["actions"][min(saved_actions["actions"])]
592 key = saved_actions["next"]
593 saved_actions["actions"][key] = the_action
594 saved_actions["next"] = key + 1
597 @openerpweb.jsonrequest
598 def get_session_action(self, req, key):
600 Gets back a previously saved action. This method can return None if the action
601 was saved since too much time (this case should be handled in a smart way).
603 :param key: The key given by save_session_action()
605 :return: The saved action or None.
608 saved_actions = req.httpsession.get('saved_actions')
609 if not saved_actions:
611 return saved_actions["actions"].get(key)
613 @openerpweb.jsonrequest
614 def check(self, req):
615 req.session.assert_valid()
618 @openerpweb.jsonrequest
619 def destroy(self, req):
620 req.session._suicide = True
622 def eval_context_and_domain(session, context, domain=None):
623 e_context = session.eval_context(context)
624 # should we give the evaluated context as an evaluation context to the domain?
625 e_domain = session.eval_domain(domain or [])
627 return e_context, e_domain
629 def load_actions_from_ir_values(req, key, key2, models, meta):
630 context = req.session.eval_context(req.context)
631 Values = req.session.model('ir.values')
632 actions = Values.get(key, key2, models, meta, context)
634 return [(id, name, clean_action(req, action))
635 for id, name, action in actions]
637 def clean_action(req, action, do_not_eval=False):
638 action.setdefault('flags', {})
640 context = req.session.eval_context(req.context)
641 eval_ctx = req.session.evaluation_context(context)
644 # values come from the server, we can just eval them
645 if isinstance(action.get('context'), basestring):
646 action['context'] = eval( action['context'], eval_ctx ) or {}
648 if isinstance(action.get('domain'), basestring):
649 action['domain'] = eval( action['domain'], eval_ctx ) or []
651 if 'context' in action:
652 action['context'] = parse_context(action['context'], req.session)
653 if 'domain' in action:
654 action['domain'] = parse_domain(action['domain'], req.session)
656 action_type = action.setdefault('type', 'ir.actions.act_window_close')
657 if action_type == 'ir.actions.act_window':
658 return fix_view_modes(action)
661 # I think generate_views,fix_view_modes should go into js ActionManager
662 def generate_views(action):
664 While the server generates a sequence called "views" computing dependencies
665 between a bunch of stuff for views coming directly from the database
666 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
667 to return custom view dictionaries generated on the fly.
669 In that case, there is no ``views`` key available on the action.
671 Since the web client relies on ``action['views']``, generate it here from
672 ``view_mode`` and ``view_id``.
674 Currently handles two different cases:
676 * no view_id, multiple view_mode
677 * single view_id, single view_mode
679 :param dict action: action descriptor dictionary to generate a views key for
681 view_id = action.get('view_id', False)
682 if isinstance(view_id, (list, tuple)):
685 # providing at least one view mode is a requirement, not an option
686 view_modes = action['view_mode'].split(',')
688 if len(view_modes) > 1:
690 raise ValueError('Non-db action dictionaries should provide '
691 'either multiple view modes or a single view '
692 'mode and an optional view id.\n\n Got view '
693 'modes %r and view id %r for action %r' % (
694 view_modes, view_id, action))
695 action['views'] = [(False, mode) for mode in view_modes]
697 action['views'] = [(view_id, view_modes[0])]
699 def fix_view_modes(action):
700 """ For historical reasons, OpenERP has weird dealings in relation to
701 view_mode and the view_type attribute (on window actions):
703 * one of the view modes is ``tree``, which stands for both list views
705 * the choice is made by checking ``view_type``, which is either
706 ``form`` for a list view or ``tree`` for an actual tree view
708 This methods simply folds the view_type into view_mode by adding a
709 new view mode ``list`` which is the result of the ``tree`` view_mode
710 in conjunction with the ``form`` view_type.
712 This method also adds a ``page`` view mode in case there is a ``form`` in
715 TODO: this should go into the doc, some kind of "peculiarities" section
717 :param dict action: an action descriptor
718 :returns: nothing, the action is modified in place
720 if not action.get('views'):
721 generate_views(action)
724 for index, (id, mode) in enumerate(action['views']):
728 if id_form is not None:
729 action['views'].insert(index + 1, (id_form, 'page'))
731 if action.pop('view_type', 'form') != 'form':
735 [id, mode if mode != 'tree' else 'list']
736 for id, mode in action['views']
741 class Menu(openerpweb.Controller):
742 _cp_path = "/web/menu"
744 @openerpweb.jsonrequest
746 return {'data': self.do_load(req)}
748 @openerpweb.jsonrequest
749 def get_user_roots(self, req):
750 return self.do_get_user_roots(req)
752 def do_get_user_roots(self, req):
753 """ Return all root menu ids visible for the session user.
755 :param req: A request object, with an OpenERP session attribute
756 :type req: < session -> OpenERPSession >
757 :return: the root menu ids
761 context = s.eval_context(req.context)
762 Menus = s.model('ir.ui.menu')
763 # If a menu action is defined use its domain to get the root menu items
764 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
766 menu_domain = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
767 menu_domain = ast.literal_eval(menu_domain)
769 menu_domain = [('parent_id', '=', False)]
770 return Menus.search(menu_domain, 0, False, False, context)
772 def do_load(self, req):
773 """ Loads all menu items (all applications and their sub-menus).
775 :param req: A request object, with an OpenERP session attribute
776 :type req: < session -> OpenERPSession >
777 :return: the menu root
778 :rtype: dict('children': menu_nodes)
780 context = req.session.eval_context(req.context)
781 Menus = req.session.model('ir.ui.menu')
783 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id'], context)
784 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
786 # menus are loaded fully unlike a regular tree view, cause there are a
787 # limited number of items (752 when all 6.1 addons are installed)
788 menu_ids = Menus.search([], 0, False, False, context)
789 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
790 # adds roots at the end of the sequence, so that they will overwrite
791 # equivalent menu items from full menu read when put into id:item
792 # mapping, resulting in children being correctly set on the roots.
793 menu_items.extend(menu_roots)
795 # make a tree using parent_id
796 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
797 for menu_item in menu_items:
798 if menu_item['parent_id']:
799 parent = menu_item['parent_id'][0]
802 if parent in menu_items_map:
803 menu_items_map[parent].setdefault(
804 'children', []).append(menu_item)
806 # sort by sequence a tree using parent_id
807 for menu_item in menu_items:
808 menu_item.setdefault('children', []).sort(
809 key=operator.itemgetter('sequence'))
813 @openerpweb.jsonrequest
814 def action(self, req, menu_id):
815 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
816 [('ir.ui.menu', menu_id)], False)
817 return {"action": actions}
819 class DataSet(openerpweb.Controller):
820 _cp_path = "/web/dataset"
822 @openerpweb.jsonrequest
823 def fields(self, req, model):
824 return {'fields': req.session.model(model).fields_get(False,
825 req.session.eval_context(req.context))}
827 @openerpweb.jsonrequest
828 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
829 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
830 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
832 """ Performs a search() followed by a read() (if needed) using the
833 provided search criteria
835 :param req: a JSON-RPC request object
836 :type req: openerpweb.JsonRequest
837 :param str model: the name of the model to search on
838 :param fields: a list of the fields to return in the result records
840 :param int offset: from which index should the results start being returned
841 :param int limit: the maximum number of records to return
842 :param list domain: the search domain for the query
843 :param list sort: sorting directives
844 :returns: A structure (dict) with two keys: ids (all the ids matching
845 the (domain, context) pair) and records (paginated records
846 matching fields selection set)
849 Model = req.session.model(model)
851 context, domain = eval_context_and_domain(
852 req.session, req.context, domain)
854 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
855 if limit and len(ids) == limit:
856 length = Model.search_count(domain, context)
858 length = len(ids) + (offset or 0)
859 if fields and fields == ['id']:
860 # shortcut read if we only want the ids
864 'records': [{'id': id} for id in ids]
867 records = Model.read(ids, fields or False, context)
868 records.sort(key=lambda obj: ids.index(obj['id']))
876 @openerpweb.jsonrequest
877 def read(self, req, model, ids, fields=False):
878 return self.do_search_read(req, model, ids, fields)
880 @openerpweb.jsonrequest
881 def get(self, req, model, ids, fields=False):
882 return self.do_get(req, model, ids, fields)
884 def do_get(self, req, model, ids, fields=False):
885 """ Fetches and returns the records of the model ``model`` whose ids
888 The results are in the same order as the inputs, but elements may be
889 missing (if there is no record left for the id)
891 :param req: the JSON-RPC2 request object
892 :type req: openerpweb.JsonRequest
893 :param model: the model to read from
895 :param ids: a list of identifiers
897 :param fields: a list of fields to fetch, ``False`` or empty to fetch
898 all fields in the model
899 :type fields: list | False
900 :returns: a list of records, in the same order as the list of ids
903 Model = req.session.model(model)
904 records = Model.read(ids, fields, req.session.eval_context(req.context))
906 record_map = dict((record['id'], record) for record in records)
908 return [record_map[id] for id in ids if record_map.get(id)]
910 @openerpweb.jsonrequest
911 def load(self, req, model, id, fields):
912 m = req.session.model(model)
914 r = m.read([id], False, req.session.eval_context(req.context))
917 return {'value': value}
919 @openerpweb.jsonrequest
920 def create(self, req, model, data):
921 m = req.session.model(model)
922 r = m.create(data, req.session.eval_context(req.context))
925 @openerpweb.jsonrequest
926 def save(self, req, model, id, data):
927 m = req.session.model(model)
928 r = m.write([id], data, req.session.eval_context(req.context))
931 @openerpweb.jsonrequest
932 def unlink(self, req, model, ids=()):
933 Model = req.session.model(model)
934 return Model.unlink(ids, req.session.eval_context(req.context))
936 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
937 has_domain = domain_id is not None and domain_id < len(args)
938 has_context = context_id is not None and context_id < len(args)
940 domain = args[domain_id] if has_domain else []
941 context = args[context_id] if has_context else {}
942 c, d = eval_context_and_domain(req.session, context, domain)
948 return self._call_kw(req, model, method, args, {})
950 def _call_kw(self, req, model, method, args, kwargs):
951 for i in xrange(len(args)):
952 if isinstance(args[i], common.nonliterals.BaseContext):
953 args[i] = req.session.eval_context(args[i])
954 elif isinstance(args[i], common.nonliterals.BaseDomain):
955 args[i] = req.session.eval_domain(args[i])
956 for k in kwargs.keys():
957 if isinstance(kwargs[k], common.nonliterals.BaseContext):
958 kwargs[k] = req.session.eval_context(kwargs[k])
959 elif isinstance(kwargs[k], common.nonliterals.BaseDomain):
960 kwargs[k] = req.session.eval_domain(kwargs[k])
962 return getattr(req.session.model(model), method)(*args, **kwargs)
964 @openerpweb.jsonrequest
965 def onchange(self, req, model, method, args, context_id=None):
966 """ Support method for handling onchange calls: behaves much like call
967 with the following differences:
969 * Does not take a domain_id
970 * Is aware of the return value's structure, and will parse the domains
971 if needed in order to return either parsed literal domains (in JSON)
972 or non-literal domain instances, allowing those domains to be used
976 :type req: web.common.http.JsonRequest
977 :param str model: object type on which to call the method
978 :param str method: name of the onchange handler method
979 :param list args: arguments to call the onchange handler with
980 :param int context_id: index of the context object in the list of
982 :return: result of the onchange call with all domains parsed
984 result = self.call_common(req, model, method, args, context_id=context_id)
985 if not result or 'domain' not in result:
988 result['domain'] = dict(
989 (k, parse_domain(v, req.session))
990 for k, v in result['domain'].iteritems())
994 @openerpweb.jsonrequest
995 def call(self, req, model, method, args, domain_id=None, context_id=None):
996 return self.call_common(req, model, method, args, domain_id, context_id)
998 @openerpweb.jsonrequest
999 def call_kw(self, req, model, method, args, kwargs):
1000 return self._call_kw(req, model, method, args, kwargs)
1002 @openerpweb.jsonrequest
1003 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1004 action = self.call_common(req, model, method, args, domain_id, context_id)
1005 if isinstance(action, dict) and action.get('type') != '':
1006 return {'result': clean_action(req, action)}
1007 return {'result': False}
1009 @openerpweb.jsonrequest
1010 def exec_workflow(self, req, model, id, signal):
1011 r = req.session.exec_workflow(model, id, signal)
1012 return {'result': r}
1014 @openerpweb.jsonrequest
1015 def default_get(self, req, model, fields):
1016 Model = req.session.model(model)
1017 return Model.default_get(fields, req.session.eval_context(req.context))
1019 @openerpweb.jsonrequest
1020 def name_search(self, req, model, search_str, domain=[], context={}):
1021 m = req.session.model(model)
1022 r = m.name_search(search_str+'%', domain, '=ilike', context)
1023 return {'result': r}
1025 class DataGroup(openerpweb.Controller):
1026 _cp_path = "/web/group"
1027 @openerpweb.jsonrequest
1028 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1029 Model = req.session.model(model)
1030 context, domain = eval_context_and_domain(req.session, req.context, domain)
1032 return Model.read_group(
1033 domain or [], fields, group_by_fields, 0, False,
1034 dict(context, group_by=group_by_fields), sort or False)
1036 class View(openerpweb.Controller):
1037 _cp_path = "/web/view"
1039 def fields_view_get(self, req, model, view_id, view_type,
1040 transform=True, toolbar=False, submenu=False):
1041 Model = req.session.model(model)
1042 context = req.session.eval_context(req.context)
1043 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1044 # todo fme?: check that we should pass the evaluated context here
1045 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1046 if toolbar and transform:
1047 self.process_toolbar(req, fvg['toolbar'])
1050 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1051 # depending on how it feels, xmlrpclib.ServerProxy can translate
1052 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1053 # enjoy unicode strings which can not be trivially converted to
1054 # strings, and it blows up during parsing.
1056 # So ensure we fix this retardation by converting view xml back to
1058 if isinstance(fvg['arch'], unicode):
1059 arch = fvg['arch'].encode('utf-8')
1064 evaluation_context = session.evaluation_context(context or {})
1065 xml = self.transform_view(arch, session, evaluation_context)
1067 xml = ElementTree.fromstring(arch)
1068 fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
1070 if 'id' in fvg['fields']:
1071 # Special case for id's
1072 id_field = fvg['fields']['id']
1073 id_field['original_type'] = id_field['type']
1074 id_field['type'] = 'id'
1076 for field in fvg['fields'].itervalues():
1077 if field.get('views'):
1078 for view in field["views"].itervalues():
1079 self.process_view(session, view, None, transform)
1080 if field.get('domain'):
1081 field["domain"] = parse_domain(field["domain"], session)
1082 if field.get('context'):
1083 field["context"] = parse_context(field["context"], session)
1085 def process_toolbar(self, req, toolbar):
1087 The toolbar is a mapping of section_key: [action_descriptor]
1089 We need to clean all those actions in order to ensure correct
1092 for actions in toolbar.itervalues():
1093 for action in actions:
1094 if 'context' in action:
1095 action['context'] = parse_context(
1096 action['context'], req.session)
1097 if 'domain' in action:
1098 action['domain'] = parse_domain(
1099 action['domain'], req.session)
1101 @openerpweb.jsonrequest
1102 def add_custom(self, req, view_id, arch):
1103 CustomView = req.session.model('ir.ui.view.custom')
1105 'user_id': req.session._uid,
1108 }, req.session.eval_context(req.context))
1109 return {'result': True}
1111 @openerpweb.jsonrequest
1112 def undo_custom(self, req, view_id, reset=False):
1113 CustomView = req.session.model('ir.ui.view.custom')
1114 context = req.session.eval_context(req.context)
1115 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1116 0, False, False, context)
1119 CustomView.unlink(vcustom, context)
1121 CustomView.unlink([vcustom[0]], context)
1122 return {'result': True}
1123 return {'result': False}
1125 def transform_view(self, view_string, session, context=None):
1126 # transform nodes on the fly via iterparse, instead of
1127 # doing it statically on the parsing result
1128 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1130 for event, elem in parser:
1131 if event == "start":
1134 self.parse_domains_and_contexts(elem, session)
1137 def parse_domains_and_contexts(self, elem, session):
1138 """ Converts domains and contexts from the view into Python objects,
1139 either literals if they can be parsed by literal_eval or a special
1140 placeholder object if the domain or context refers to free variables.
1142 :param elem: the current node being parsed
1143 :type param: xml.etree.ElementTree.Element
1144 :param session: OpenERP session object, used to store and retrieve
1146 :type session: openerpweb.openerpweb.OpenERPSession
1148 for el in ['domain', 'filter_domain']:
1149 domain = elem.get(el, '').strip()
1151 elem.set(el, parse_domain(domain, session))
1152 elem.set(el + '_string', domain)
1153 for el in ['context', 'default_get']:
1154 context_string = elem.get(el, '').strip()
1156 elem.set(el, parse_context(context_string, session))
1157 elem.set(el + '_string', context_string)
1159 @openerpweb.jsonrequest
1160 def load(self, req, model, view_id, view_type, toolbar=False):
1161 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1163 def parse_domain(domain, session):
1164 """ Parses an arbitrary string containing a domain, transforms it
1165 to either a literal domain or a :class:`common.nonliterals.Domain`
1167 :param domain: the domain to parse, if the domain is not a string it
1168 is assumed to be a literal domain and is returned as-is
1169 :param session: Current OpenERP session
1170 :type session: openerpweb.openerpweb.OpenERPSession
1172 if not isinstance(domain, basestring):
1175 return ast.literal_eval(domain)
1178 return common.nonliterals.Domain(session, domain)
1180 def parse_context(context, session):
1181 """ Parses an arbitrary string containing a context, transforms it
1182 to either a literal context or a :class:`common.nonliterals.Context`
1184 :param context: the context to parse, if the context is not a string it
1185 is assumed to be a literal domain and is returned as-is
1186 :param session: Current OpenERP session
1187 :type session: openerpweb.openerpweb.OpenERPSession
1189 if not isinstance(context, basestring):
1192 return ast.literal_eval(context)
1194 return common.nonliterals.Context(session, context)
1196 class ListView(View):
1197 _cp_path = "/web/listview"
1199 def process_colors(self, view, row, context):
1200 colors = view['arch']['attrs'].get('colors')
1207 for pair in colors.split(';')
1208 if eval(pair.split(':')[1], dict(context, **row))
1213 elif len(color) == 1:
1217 class TreeView(View):
1218 _cp_path = "/web/treeview"
1220 @openerpweb.jsonrequest
1221 def action(self, req, model, id):
1222 return load_actions_from_ir_values(
1223 req,'action', 'tree_but_open',[(model, id)],
1226 class SearchView(View):
1227 _cp_path = "/web/searchview"
1229 @openerpweb.jsonrequest
1230 def load(self, req, model, view_id):
1231 fields_view = self.fields_view_get(req, model, view_id, 'search')
1232 return {'fields_view': fields_view}
1234 @openerpweb.jsonrequest
1235 def fields_get(self, req, model):
1236 Model = req.session.model(model)
1237 fields = Model.fields_get(False, req.session.eval_context(req.context))
1238 for field in fields.values():
1239 # shouldn't convert the views too?
1240 if field.get('domain'):
1241 field["domain"] = parse_domain(field["domain"], req.session)
1242 if field.get('context'):
1243 field["context"] = parse_context(field["context"], req.session)
1244 return {'fields': fields}
1246 @openerpweb.jsonrequest
1247 def get_filters(self, req, model):
1248 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1249 Model = req.session.model("ir.filters")
1250 filters = Model.get_filters(model)
1251 for filter in filters:
1253 filter["context"] = req.session.eval_context(
1254 parse_context(filter["context"], req.session))
1255 filter["domain"] = req.session.eval_domain(
1256 parse_domain(filter["domain"], req.session))
1258 logger.exception("Failed to parse custom filter %s in %s",
1259 filter['name'], model)
1260 filter['disabled'] = True
1261 del filter['context']
1262 del filter['domain']
1265 @openerpweb.jsonrequest
1266 def save_filter(self, req, model, name, context_to_save, domain):
1267 Model = req.session.model("ir.filters")
1268 ctx = common.nonliterals.CompoundContext(context_to_save)
1269 ctx.session = req.session
1270 ctx = ctx.evaluate()
1271 domain = common.nonliterals.CompoundDomain(domain)
1272 domain.session = req.session
1273 domain = domain.evaluate()
1274 uid = req.session._uid
1275 context = req.session.eval_context(req.context)
1276 to_return = Model.create_or_replace({"context": ctx,
1284 @openerpweb.jsonrequest
1285 def add_to_dashboard(self, req, menu_id, action_id, context_to_save, domain, view_mode, name=''):
1286 ctx = common.nonliterals.CompoundContext(context_to_save)
1287 ctx.session = req.session
1288 ctx = ctx.evaluate()
1289 domain = common.nonliterals.CompoundDomain(domain)
1290 domain.session = req.session
1291 domain = domain.evaluate()
1293 dashboard_action = load_actions_from_ir_values(req, 'action', 'tree_but_open',
1294 [('ir.ui.menu', menu_id)], False)
1295 if dashboard_action:
1296 action = dashboard_action[0][2]
1297 if action['res_model'] == 'board.board' and action['views'][0][1] == 'form':
1298 # Maybe should check the content instead of model board.board ?
1299 view_id = action['views'][0][0]
1300 board = req.session.model(action['res_model']).fields_view_get(view_id, 'form')
1301 if board and 'arch' in board:
1302 xml = ElementTree.fromstring(board['arch'])
1303 column = xml.find('./board/column')
1305 new_action = ElementTree.Element('action', {
1306 'name' : str(action_id),
1308 'view_mode' : view_mode,
1309 'context' : str(ctx),
1310 'domain' : str(domain)
1312 column.insert(0, new_action)
1313 arch = ElementTree.tostring(xml, 'utf-8')
1314 return req.session.model('ir.ui.view.custom').create({
1315 'user_id': req.session._uid,
1318 }, req.session.eval_context(req.context))
1322 class Binary(openerpweb.Controller):
1323 _cp_path = "/web/binary"
1325 @openerpweb.httprequest
1326 def image(self, req, model, id, field, **kw):
1327 Model = req.session.model(model)
1328 context = req.session.eval_context(req.context)
1332 res = Model.default_get([field], context).get(field)
1334 res = Model.read([int(id)], [field], context)[0].get(field)
1335 image_data = base64.b64decode(res)
1336 except (TypeError, xmlrpclib.Fault):
1337 image_data = self.placeholder(req)
1338 return req.make_response(image_data, [
1339 ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
1340 def placeholder(self, req):
1341 addons_path = openerpweb.addons_manifest['web']['addons_path']
1342 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1344 @openerpweb.httprequest
1345 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1346 """ Download link for files stored as binary fields.
1348 If the ``id`` parameter is omitted, fetches the default value for the
1349 binary field (via ``default_get``), otherwise fetches the field for
1350 that precise record.
1352 :param req: OpenERP request
1353 :type req: :class:`web.common.http.HttpRequest`
1354 :param str model: name of the model to fetch the binary from
1355 :param str field: binary field
1356 :param str id: id of the record from which to fetch the binary
1357 :param str filename_field: field holding the file's name, if any
1358 :returns: :class:`werkzeug.wrappers.Response`
1360 Model = req.session.model(model)
1361 context = req.session.eval_context(req.context)
1364 fields.append(filename_field)
1366 res = Model.read([int(id)], fields, context)[0]
1368 res = Model.default_get(fields, context)
1369 filecontent = base64.b64decode(res.get(field, ''))
1371 return req.not_found()
1373 filename = '%s_%s' % (model.replace('.', '_'), id)
1375 filename = res.get(filename_field, '') or filename
1376 return req.make_response(filecontent,
1377 [('Content-Type', 'application/octet-stream'),
1378 ('Content-Disposition', 'attachment; filename="%s"' % filename)])
1380 @openerpweb.httprequest
1381 def saveas_ajax(self, req, data, token):
1382 jdata = simplejson.loads(data)
1383 model = jdata['model']
1384 field = jdata['field']
1385 id = jdata.get('id', None)
1386 filename_field = jdata.get('filename_field', None)
1387 context = jdata.get('context', dict())
1389 context = req.session.eval_context(context)
1390 Model = req.session.model(model)
1393 fields.append(filename_field)
1395 res = Model.read([int(id)], fields, context)[0]
1397 res = Model.default_get(fields, context)
1398 filecontent = base64.b64decode(res.get(field, ''))
1400 raise ValueError("No content found for field '%s' on '%s:%s'" %
1403 filename = '%s_%s' % (model.replace('.', '_'), id)
1405 filename = res.get(filename_field, '') or filename
1406 return req.make_response(filecontent,
1407 headers=[('Content-Type', 'application/octet-stream'),
1408 ('Content-Disposition', 'attachment; filename="%s"' % filename)],
1409 cookies={'fileToken': int(token)})
1411 @openerpweb.httprequest
1412 def upload(self, req, callback, ufile):
1413 # TODO: might be useful to have a configuration flag for max-length file uploads
1415 out = """<script language="javascript" type="text/javascript">
1416 var win = window.top.window,
1418 if (typeof(callback) === 'function') {
1419 callback.apply(this, %s);
1421 win.jQuery('#oe_notification', win.document).notify('create', {
1422 title: "Ajax File Upload",
1423 text: "Could not find callback"
1428 args = [len(data), ufile.filename,
1429 ufile.content_type, base64.b64encode(data)]
1430 except Exception, e:
1431 args = [False, e.message]
1432 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1434 @openerpweb.httprequest
1435 def upload_attachment(self, req, callback, model, id, ufile):
1436 context = req.session.eval_context(req.context)
1437 Model = req.session.model('ir.attachment')
1439 out = """<script language="javascript" type="text/javascript">
1440 var win = window.top.window,
1442 if (typeof(callback) === 'function') {
1443 callback.call(this, %s);
1446 attachment_id = Model.create({
1447 'name': ufile.filename,
1448 'datas': base64.encodestring(ufile.read()),
1449 'datas_fname': ufile.filename,
1454 'filename': ufile.filename,
1457 except Exception, e:
1458 args = { 'error': e.message }
1459 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1461 class Action(openerpweb.Controller):
1462 _cp_path = "/web/action"
1464 @openerpweb.jsonrequest
1465 def load(self, req, action_id, do_not_eval=False):
1466 Actions = req.session.model('ir.actions.actions')
1468 context = req.session.eval_context(req.context)
1469 action_type = Actions.read([action_id], ['type'], context)
1472 if action_type[0]['type'] == 'ir.actions.report.xml':
1473 ctx.update({'bin_size': True})
1475 action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
1477 value = clean_action(req, action[0], do_not_eval)
1478 return {'result': value}
1480 @openerpweb.jsonrequest
1481 def run(self, req, action_id):
1482 return clean_action(req, req.session.model('ir.actions.server').run(
1483 [action_id], req.session.eval_context(req.context)))
1486 _cp_path = "/web/export"
1488 @openerpweb.jsonrequest
1489 def formats(self, req):
1490 """ Returns all valid export formats
1492 :returns: for each export format, a pair of identifier and printable name
1493 :rtype: [(str, str)]
1497 for path, controller in openerpweb.controllers_path.iteritems()
1498 if path.startswith(self._cp_path)
1499 if hasattr(controller, 'fmt')
1500 ], key=operator.itemgetter("label"))
1502 def fields_get(self, req, model):
1503 Model = req.session.model(model)
1504 fields = Model.fields_get(False, req.session.eval_context(req.context))
1507 @openerpweb.jsonrequest
1508 def get_fields(self, req, model, prefix='', parent_name= '',
1509 import_compat=True, parent_field_type=None,
1512 if import_compat and parent_field_type == "many2one":
1515 fields = self.fields_get(req, model)
1518 fields.pop('id', None)
1520 fields['.id'] = fields.pop('id', {'string': 'ID'})
1522 fields_sequence = sorted(fields.iteritems(),
1523 key=lambda field: field[1].get('string', ''))
1526 for field_name, field in fields_sequence:
1528 if exclude and field_name in exclude:
1530 if field.get('readonly'):
1531 # If none of the field's states unsets readonly, skip the field
1532 if all(dict(attrs).get('readonly', True)
1533 for attrs in field.get('states', {}).values()):
1536 id = prefix + (prefix and '/'or '') + field_name
1537 name = parent_name + (parent_name and '/' or '') + field['string']
1538 record = {'id': id, 'string': name,
1539 'value': id, 'children': False,
1540 'field_type': field.get('type'),
1541 'required': field.get('required'),
1542 'relation_field': field.get('relation_field')}
1543 records.append(record)
1545 if len(name.split('/')) < 3 and 'relation' in field:
1546 ref = field.pop('relation')
1547 record['value'] += '/id'
1548 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1550 if not import_compat or field['type'] == 'one2many':
1551 # m2m field in import_compat is childless
1552 record['children'] = True
1556 @openerpweb.jsonrequest
1557 def namelist(self,req, model, export_id):
1558 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1559 export = req.session.model("ir.exports").read([export_id])[0]
1560 export_fields_list = req.session.model("ir.exports.line").read(
1561 export['export_fields'])
1563 fields_data = self.fields_info(
1564 req, model, map(operator.itemgetter('name'), export_fields_list))
1567 {'name': field['name'], 'label': fields_data[field['name']]}
1568 for field in export_fields_list
1571 def fields_info(self, req, model, export_fields):
1573 fields = self.fields_get(req, model)
1575 # To make fields retrieval more efficient, fetch all sub-fields of a
1576 # given field at the same time. Because the order in the export list is
1577 # arbitrary, this requires ordering all sub-fields of a given field
1578 # together so they can be fetched at the same time
1580 # Works the following way:
1581 # * sort the list of fields to export, the default sorting order will
1582 # put the field itself (if present, for xmlid) and all of its
1583 # sub-fields right after it
1584 # * then, group on: the first field of the path (which is the same for
1585 # a field and for its subfields and the length of splitting on the
1586 # first '/', which basically means grouping the field on one side and
1587 # all of the subfields on the other. This way, we have the field (for
1588 # the xmlid) with length 1, and all of the subfields with the same
1589 # base but a length "flag" of 2
1590 # * if we have a normal field (length 1), just add it to the info
1591 # mapping (with its string) as-is
1592 # * otherwise, recursively call fields_info via graft_subfields.
1593 # all graft_subfields does is take the result of fields_info (on the
1594 # field's model) and prepend the current base (current field), which
1595 # rebuilds the whole sub-tree for the field
1597 # result: because we're not fetching the fields_get for half the
1598 # database models, fetching a namelist with a dozen fields (including
1599 # relational data) falls from ~6s to ~300ms (on the leads model).
1600 # export lists with no sub-fields (e.g. import_compatible lists with
1601 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1602 # there's a single fields_get to execute)
1603 for (base, length), subfields in itertools.groupby(
1604 sorted(export_fields),
1605 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1606 subfields = list(subfields)
1608 # subfields is a seq of $base/*rest, and not loaded yet
1609 info.update(self.graft_subfields(
1610 req, fields[base]['relation'], base, fields[base]['string'],
1614 info[base] = fields[base]['string']
1618 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1619 export_fields = [field.split('/', 1)[1] for field in fields]
1621 (prefix + '/' + k, prefix_string + '/' + v)
1622 for k, v in self.fields_info(req, model, export_fields).iteritems())
1624 #noinspection PyPropertyDefinition
1626 def content_type(self):
1627 """ Provides the format's content type """
1628 raise NotImplementedError()
1630 def filename(self, base):
1631 """ Creates a valid filename for the format (with extension) from the
1632 provided base name (exension-less)
1634 raise NotImplementedError()
1636 def from_data(self, fields, rows):
1637 """ Conversion method from OpenERP's export data to whatever the
1638 current export class outputs
1640 :params list fields: a list of fields to export
1641 :params list rows: a list of records to export
1645 raise NotImplementedError()
1647 @openerpweb.httprequest
1648 def index(self, req, data, token):
1649 model, fields, ids, domain, import_compat = \
1650 operator.itemgetter('model', 'fields', 'ids', 'domain',
1652 simplejson.loads(data))
1654 context = req.session.eval_context(req.context)
1655 Model = req.session.model(model)
1656 ids = ids or Model.search(domain, 0, False, False, context)
1658 field_names = map(operator.itemgetter('name'), fields)
1659 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1662 columns_headers = field_names
1664 columns_headers = [val['label'].strip() for val in fields]
1667 return req.make_response(self.from_data(columns_headers, import_data),
1668 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1669 ('Content-Type', self.content_type)],
1670 cookies={'fileToken': int(token)})
1672 class CSVExport(Export):
1673 _cp_path = '/web/export/csv'
1674 fmt = {'tag': 'csv', 'label': 'CSV'}
1677 def content_type(self):
1678 return 'text/csv;charset=utf8'
1680 def filename(self, base):
1681 return base + '.csv'
1683 def from_data(self, fields, rows):
1685 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1687 writer.writerow([name.encode('utf-8') for name in fields])
1692 if isinstance(d, basestring):
1693 d = d.replace('\n',' ').replace('\t',' ')
1695 d = d.encode('utf-8')
1696 except UnicodeError:
1698 if d is False: d = None
1700 writer.writerow(row)
1707 class ExcelExport(Export):
1708 _cp_path = '/web/export/xls'
1712 'error': None if xlwt else "XLWT required"
1716 def content_type(self):
1717 return 'application/vnd.ms-excel'
1719 def filename(self, base):
1720 return base + '.xls'
1722 def from_data(self, fields, rows):
1723 workbook = xlwt.Workbook()
1724 worksheet = workbook.add_sheet('Sheet 1')
1726 for i, fieldname in enumerate(fields):
1727 worksheet.write(0, i, fieldname)
1728 worksheet.col(i).width = 8000 # around 220 pixels
1730 style = xlwt.easyxf('align: wrap yes')
1732 for row_index, row in enumerate(rows):
1733 for cell_index, cell_value in enumerate(row):
1734 if isinstance(cell_value, basestring):
1735 cell_value = re.sub("\r", " ", cell_value)
1736 if cell_value is False: cell_value = None
1737 worksheet.write(row_index + 1, cell_index, cell_value, style)
1746 class Reports(View):
1747 _cp_path = "/web/report"
1748 POLLING_DELAY = 0.25
1750 'doc': 'application/vnd.ms-word',
1751 'html': 'text/html',
1752 'odt': 'application/vnd.oasis.opendocument.text',
1753 'pdf': 'application/pdf',
1754 'sxw': 'application/vnd.sun.xml.writer',
1755 'xls': 'application/vnd.ms-excel',
1758 @openerpweb.httprequest
1759 def index(self, req, action, token):
1760 action = simplejson.loads(action)
1762 report_srv = req.session.proxy("report")
1763 context = req.session.eval_context(
1764 common.nonliterals.CompoundContext(
1765 req.context or {}, action[ "context"]))
1768 report_ids = context["active_ids"]
1769 if 'report_type' in action:
1770 report_data['report_type'] = action['report_type']
1771 if 'datas' in action:
1772 if 'ids' in action['datas']:
1773 report_ids = action['datas'].pop('ids')
1774 report_data.update(action['datas'])
1776 report_id = report_srv.report(
1777 req.session._db, req.session._uid, req.session._password,
1778 action["report_name"], report_ids,
1779 report_data, context)
1781 report_struct = None
1783 report_struct = report_srv.report_get(
1784 req.session._db, req.session._uid, req.session._password, report_id)
1785 if report_struct["state"]:
1788 time.sleep(self.POLLING_DELAY)
1790 report = base64.b64decode(report_struct['result'])
1791 if report_struct.get('code') == 'zlib':
1792 report = zlib.decompress(report)
1793 report_mimetype = self.TYPES_MAPPING.get(
1794 report_struct['format'], 'octet-stream')
1795 return req.make_response(report,
1797 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
1798 ('Content-Type', report_mimetype),
1799 ('Content-Length', len(report))],
1800 cookies={'fileToken': int(token)})
1803 _cp_path = "/web/import"
1805 def fields_get(self, req, model):
1806 Model = req.session.model(model)
1807 fields = Model.fields_get(False, req.session.eval_context(req.context))
1810 @openerpweb.httprequest
1811 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1813 data = list(csv.reader(
1814 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1815 except csv.Error, e:
1817 return '<script>window.top.%s(%s);</script>' % (
1818 jsonp, simplejson.dumps({'error': {
1819 'message': 'Error parsing CSV file: %s' % e,
1820 # decodes each byte to a unicode character, which may or
1821 # may not be printable, but decoding will succeed.
1822 # Otherwise simplejson will try to decode the `str` using
1823 # utf-8, which is very likely to blow up on characters out
1824 # of the ascii range (in range [128, 256))
1825 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1828 return '<script>window.top.%s(%s);</script>' % (
1829 jsonp, simplejson.dumps(
1830 {'records': data[:10]}, encoding=csvcode))
1831 except UnicodeDecodeError:
1832 return '<script>window.top.%s(%s);</script>' % (
1833 jsonp, simplejson.dumps({
1834 'message': u"Failed to decode CSV file using encoding %s, "
1835 u"try switching to a different encoding" % csvcode
1838 @openerpweb.httprequest
1839 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1841 modle_obj = req.session.model(model)
1842 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1843 simplejson.loads(meta))
1846 if not (csvdel and len(csvdel) == 1):
1847 error = u"The CSV delimiter must be a single character"
1849 if not indices and fields:
1850 error = u"You must select at least one field to import"
1853 return '<script>window.top.%s(%s);</script>' % (
1854 jsonp, simplejson.dumps({'error': {'message': error}}))
1856 # skip ignored records
1857 data_record = itertools.islice(
1858 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1861 # if only one index, itemgetter will return an atom rather than a tuple
1862 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1863 else: mapper = operator.itemgetter(*indices)
1868 # decode each data row
1870 [record.decode(csvcode) for record in row]
1871 for row in itertools.imap(mapper, data_record)
1872 # don't insert completely empty rows (can happen due to fields
1873 # filtering in case of e.g. o2m content rows)
1876 except UnicodeDecodeError:
1877 error = u"Failed to decode CSV file using encoding %s" % csvcode
1878 except csv.Error, e:
1879 error = u"Could not process CSV file: %s" % e
1881 # If the file contains nothing,
1883 error = u"File to import is empty"
1885 return '<script>window.top.%s(%s);</script>' % (
1886 jsonp, simplejson.dumps({'error': {'message': error}}))
1889 (code, record, message, _nope) = modle_obj.import_data(
1890 fields, data, 'init', '', False,
1891 req.session.eval_context(req.context))
1892 except xmlrpclib.Fault, e:
1893 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1894 return '<script>window.top.%s(%s);</script>' % (
1895 jsonp, simplejson.dumps({'error':error}))
1898 return '<script>window.top.%s(%s);</script>' % (
1899 jsonp, simplejson.dumps({'success':True}))
1901 msg = u"Error during import: %s\n\nTrying to import record %r" % (
1903 return '<script>window.top.%s(%s);</script>' % (
1904 jsonp, simplejson.dumps({'error': {'message':msg}}))