1 # -*- coding: utf-8 -*-
20 from xml.etree import ElementTree
21 from cStringIO import StringIO
23 import babel.messages.pofile
25 import werkzeug.wrappers
32 import openerp.modules.registry
33 from openerp.tools.translate import _
38 #----------------------------------------------------------
40 #----------------------------------------------------------
43 """ Minify js with a clever regex.
44 Taken from http://opensource.perlig.de/rjsmin
45 Apache License, Version 2.0 """
47 """ Substitution callback """
48 groups = match.groups()
54 (groups[4] and '\n') or
55 (groups[5] and ' ') or
56 (groups[6] and ' ') or
57 (groups[7] and ' ') or
62 r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
63 r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
64 r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
65 r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
66 r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
67 r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
68 r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
69 r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
70 r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
71 r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
72 r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
73 r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
74 r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
75 r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
76 r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
77 r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
78 r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
79 r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
80 r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
81 r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
82 r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
87 proxy = req.session.proxy("db")
89 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
91 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
92 dbs = [i for i in dbs if re.match(r, i)]
96 # if only one db exists, return it else return False
101 except xmlrpclib.Fault:
102 # ignore access denied
106 def module_topological_sort(modules):
107 """ Return a list of module names sorted so that their dependencies of the
108 modules are listed before the module itself
110 modules is a dict of {module_name: dependencies}
112 :param modules: modules to sort
117 dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
118 # incoming edge: dependency on other module (if a depends on b, a has an
119 # incoming edge from b, aka there's an edge from b to a)
120 # outgoing edge: other module depending on this one
122 # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
123 #L ← Empty list that will contain the sorted nodes
125 #S ← Set of all nodes with no outgoing edges (modules on which no other
127 S = set(module for module in modules if module not in dependencies)
130 #function visit(node n)
132 #if n has not been visited yet then
136 #change: n not web module, can not be resolved, ignore
137 if n not in modules: return
138 #for each node m with an edge from m to n do (dependencies of n)
144 #for each node n in S do
150 def module_installed(req):
151 # Candidates module the current heuristic is the /static dir
152 loadable = openerpweb.addons_manifest.keys()
155 # Retrieve database installed modules
156 # TODO The following code should move to ir.module.module.list_installed_modules()
157 Modules = req.session.model('ir.module.module')
158 domain = [('state','=','installed'), ('name','in', loadable)]
159 for module in Modules.search_read(domain, ['name', 'dependencies_id']):
160 modules[module['name']] = []
161 deps = module.get('dependencies_id')
163 deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
164 dependencies = [i['name'] for i in deps_read]
165 modules[module['name']] = dependencies
167 sorted_modules = module_topological_sort(modules)
168 return sorted_modules
170 def module_installed_bypass_session(dbname):
171 loadable = openerpweb.addons_manifest.keys()
174 registry = openerp.modules.registry.RegistryManager.get(dbname)
175 with registry.cursor() as cr:
176 m = registry.get('ir.module.module')
177 # TODO The following code should move to ir.module.module.list_installed_modules()
178 domain = [('state','=','installed'), ('name','in', loadable)]
179 ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
180 for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
181 modules[module['name']] = []
182 deps = module.get('dependencies_id')
184 deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
185 dependencies = [i['name'] for i in deps_read]
186 modules[module['name']] = dependencies
189 sorted_modules = module_topological_sort(modules)
190 return sorted_modules
192 def module_boot(req, db=None):
193 server_wide_modules = openerp.conf.server_wide_modules or ['web']
196 for i in server_wide_modules:
197 if i in openerpweb.addons_manifest:
199 monodb = db or db_monodb(req)
201 dbside = module_installed_bypass_session(monodb)
202 dbside = [i for i in dbside if i not in serverside]
203 addons = serverside + dbside
206 def concat_xml(file_list):
207 """Concatenate xml files
209 :param list(str) file_list: list of files to check
210 :returns: (concatenation_result, checksum)
213 checksum = hashlib.new('sha1')
215 return '', checksum.hexdigest()
218 for fname in file_list:
219 with open(fname, 'rb') as fp:
221 checksum.update(contents)
223 xml = ElementTree.parse(fp).getroot()
226 root = ElementTree.Element(xml.tag)
227 #elif root.tag != xml.tag:
228 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
230 for child in xml.getchildren():
232 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
234 def concat_files(file_list, reader=None, intersperse=""):
235 """ Concatenates contents of all provided files
237 :param list(str) file_list: list of files to check
238 :param function reader: reading procedure for each file
239 :param str intersperse: string to intersperse between file contents
240 :returns: (concatenation_result, checksum)
243 checksum = hashlib.new('sha1')
245 return '', checksum.hexdigest()
249 with open(f, 'rb') as fp:
253 for fname in file_list:
254 contents = reader(fname)
255 checksum.update(contents)
256 files_content.append(contents)
258 files_concat = intersperse.join(files_content)
259 return files_concat, checksum.hexdigest()
263 def concat_js(file_list):
264 content, checksum = concat_files(file_list, intersperse=';')
265 if checksum in concat_js_cache:
266 content = concat_js_cache[checksum]
268 content = rjsmin(content)
269 concat_js_cache[checksum] = content
270 return content, checksum
273 """convert FS path into web path"""
274 return '/'.join(path.split(os.path.sep))
276 def manifest_glob(req, extension, addons=None, db=None):
278 addons = module_boot(req, db=db)
280 addons = addons.split(',')
283 manifest = openerpweb.addons_manifest.get(addon, None)
286 # ensure does not ends with /
287 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
288 globlist = manifest.get(extension, [])
289 for pattern in globlist:
290 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
291 r.append((path, fs2web(path[len(addons_path):])))
294 def manifest_list(req, extension, mods=None, db=None):
296 path = '/web/webclient/' + extension
298 path += '?' + urllib.urlencode({'mods': mods})
300 path += '?' + urllib.urlencode({'db': db})
302 files = manifest_glob(req, extension, addons=mods, db=db)
303 return [wp for _fp, wp in files]
305 def get_last_modified(files):
306 """ Returns the modification time of the most recently modified
309 :param list(str) files: names of files to check
310 :return: most recent modification time amongst the fileset
311 :rtype: datetime.datetime
315 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
317 return datetime.datetime(1970, 1, 1)
319 def make_conditional(req, response, last_modified=None, etag=None):
320 """ Makes the provided response conditional based upon the request,
321 and mandates revalidation from clients
323 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
324 setting ``last_modified`` and ``etag`` correctly on the response object
326 :param req: OpenERP request
327 :type req: web.common.http.WebRequest
328 :param response: Werkzeug response
329 :type response: werkzeug.wrappers.Response
330 :param datetime.datetime last_modified: last modification date of the response content
331 :param str etag: some sort of checksum of the content (deep etag)
332 :return: the response object provided
333 :rtype: werkzeug.wrappers.Response
335 response.cache_control.must_revalidate = True
336 response.cache_control.max_age = 0
338 response.last_modified = last_modified
340 response.set_etag(etag)
341 return response.make_conditional(req.httprequest)
343 def login_and_redirect(req, db, login, key, redirect_url='/'):
344 wsgienv = req.httprequest.environ
346 base_location=req.httprequest.url_root.rstrip('/'),
347 HTTP_HOST=wsgienv['HTTP_HOST'],
348 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
350 req.session.authenticate(db, login, key, env)
351 return set_cookie_and_redirect(req, redirect_url)
353 def set_cookie_and_redirect(req, redirect_url):
354 redirect = werkzeug.utils.redirect(redirect_url, 303)
355 redirect.autocorrect_location_header = False
356 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
357 redirect.set_cookie('instance0|session_id', cookie_val)
360 def load_actions_from_ir_values(req, key, key2, models, meta):
361 Values = req.session.model('ir.values')
362 actions = Values.get(key, key2, models, meta, req.context)
364 return [(id, name, clean_action(req, action))
365 for id, name, action in actions]
367 def clean_action(req, action):
368 action.setdefault('flags', {})
369 action_type = action.setdefault('type', 'ir.actions.act_window_close')
370 if action_type == 'ir.actions.act_window':
371 return fix_view_modes(action)
374 # I think generate_views,fix_view_modes should go into js ActionManager
375 def generate_views(action):
377 While the server generates a sequence called "views" computing dependencies
378 between a bunch of stuff for views coming directly from the database
379 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
380 to return custom view dictionaries generated on the fly.
382 In that case, there is no ``views`` key available on the action.
384 Since the web client relies on ``action['views']``, generate it here from
385 ``view_mode`` and ``view_id``.
387 Currently handles two different cases:
389 * no view_id, multiple view_mode
390 * single view_id, single view_mode
392 :param dict action: action descriptor dictionary to generate a views key for
394 view_id = action.get('view_id') or False
395 if isinstance(view_id, (list, tuple)):
398 # providing at least one view mode is a requirement, not an option
399 view_modes = action['view_mode'].split(',')
401 if len(view_modes) > 1:
403 raise ValueError('Non-db action dictionaries should provide '
404 'either multiple view modes or a single view '
405 'mode and an optional view id.\n\n Got view '
406 'modes %r and view id %r for action %r' % (
407 view_modes, view_id, action))
408 action['views'] = [(False, mode) for mode in view_modes]
410 action['views'] = [(view_id, view_modes[0])]
412 def fix_view_modes(action):
413 """ For historical reasons, OpenERP has weird dealings in relation to
414 view_mode and the view_type attribute (on window actions):
416 * one of the view modes is ``tree``, which stands for both list views
418 * the choice is made by checking ``view_type``, which is either
419 ``form`` for a list view or ``tree`` for an actual tree view
421 This methods simply folds the view_type into view_mode by adding a
422 new view mode ``list`` which is the result of the ``tree`` view_mode
423 in conjunction with the ``form`` view_type.
425 TODO: this should go into the doc, some kind of "peculiarities" section
427 :param dict action: an action descriptor
428 :returns: nothing, the action is modified in place
430 if not action.get('views'):
431 generate_views(action)
433 if action.pop('view_type', 'form') != 'form':
436 if 'view_mode' in action:
437 action['view_mode'] = ','.join(
438 mode if mode != 'tree' else 'list'
439 for mode in action['view_mode'].split(','))
441 [id, mode if mode != 'tree' else 'list']
442 for id, mode in action['views']
447 def _local_web_translations(trans_file):
450 with open(trans_file) as t_file:
451 po = babel.messages.pofile.read_po(t_file)
455 if x.id and x.string and "openerp-web" in x.auto_comments:
456 messages.append({'id': x.id, 'string': x.string})
459 def xml2json_from_elementtree(el, preserve_whitespaces=False):
461 Simple and straightforward XML-to-JSON converter in Python
463 http://code.google.com/p/xml2json-direct/
467 ns, name = el.tag.rsplit("}", 1)
469 res["namespace"] = ns[1:]
473 for k, v in el.items():
476 if el.text and (preserve_whitespaces or el.text.strip() != ''):
479 kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
480 if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
481 kids.append(kid.tail)
482 res["children"] = kids
485 def content_disposition(filename, req):
486 filename = filename.encode('utf8')
487 escaped = urllib2.quote(filename)
488 browser = req.httprequest.user_agent.browser
489 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
490 if browser == 'msie' and version < 9:
491 return "attachment; filename=%s" % escaped
492 elif browser == 'safari':
493 return "attachment; filename=%s" % filename
495 return "attachment; filename*=UTF-8''%s" % escaped
498 #----------------------------------------------------------
499 # OpenERP Web web Controllers
500 #----------------------------------------------------------
502 html_template = """<!DOCTYPE html>
503 <html style="height: 100%%">
505 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
506 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
507 <title>OpenERP</title>
508 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
509 <link rel="stylesheet" href="/web/static/src/css/full.css" />
512 <script type="text/javascript">
514 var s = new openerp.init(%(modules)s);
521 <script src="//ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
522 <script>CFInstall.check({mode: "overlay"});</script>
528 class Home(openerpweb.Controller):
531 @openerpweb.httprequest
532 def index(self, req, s_action=None, db=None, **kw):
533 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, 'js', db=db))
534 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, 'css', db=db))
536 r = html_template % {
539 'modules': simplejson.dumps(module_boot(req, db=db)),
540 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
544 @openerpweb.httprequest
545 def login(self, req, db, login, key):
546 return login_and_redirect(req, db, login, key)
548 class WebClient(openerpweb.Controller):
549 _cp_path = "/web/webclient"
551 @openerpweb.jsonrequest
552 def csslist(self, req, mods=None):
553 return manifest_list(req, 'css', mods=mods)
555 @openerpweb.jsonrequest
556 def jslist(self, req, mods=None):
557 return manifest_list(req, 'js', mods=mods)
559 @openerpweb.jsonrequest
560 def qweblist(self, req, mods=None):
561 return manifest_list(req, 'qweb', mods=mods)
563 @openerpweb.httprequest
564 def css(self, req, mods=None, db=None):
565 files = list(manifest_glob(req, 'css', addons=mods, db=db))
566 last_modified = get_last_modified(f[0] for f in files)
567 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
568 return werkzeug.wrappers.Response(status=304)
570 file_map = dict(files)
572 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
573 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
576 """read the a css file and absolutify all relative uris"""
577 with open(f, 'rb') as fp:
578 data = fp.read().decode('utf-8')
581 web_dir = os.path.dirname(path)
585 r"""@import \1%s/""" % (web_dir,),
591 r"""url(\1%s/""" % (web_dir,),
594 return data.encode('utf-8')
596 content, checksum = concat_files((f[0] for f in files), reader)
598 return make_conditional(
599 req, req.make_response(content, [('Content-Type', 'text/css')]),
600 last_modified, checksum)
602 @openerpweb.httprequest
603 def js(self, req, mods=None, db=None):
604 files = [f[0] for f in manifest_glob(req, 'js', addons=mods, db=db)]
605 last_modified = get_last_modified(files)
606 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
607 return werkzeug.wrappers.Response(status=304)
609 content, checksum = concat_js(files)
611 return make_conditional(
612 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
613 last_modified, checksum)
615 @openerpweb.httprequest
616 def qweb(self, req, mods=None, db=None):
617 files = [f[0] for f in manifest_glob(req, 'qweb', addons=mods, db=db)]
618 last_modified = get_last_modified(files)
619 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
620 return werkzeug.wrappers.Response(status=304)
622 content, checksum = concat_xml(files)
624 return make_conditional(
625 req, req.make_response(content, [('Content-Type', 'text/xml')]),
626 last_modified, checksum)
628 @openerpweb.jsonrequest
629 def bootstrap_translations(self, req, mods):
630 """ Load local translations from *.po files, as a temporary solution
631 until we have established a valid session. This is meant only
632 for translating the login page and db management chrome, using
633 the browser's language. """
634 # For performance reasons we only load a single translation, so for
635 # sub-languages (that should only be partially translated) we load the
636 # main language PO instead - that should be enough for the login screen.
637 lang = req.lang.split('_')[0]
639 translations_per_module = {}
640 for addon_name in mods:
641 if openerpweb.addons_manifest[addon_name].get('bootstrap'):
642 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
643 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
644 if not os.path.exists(f_name):
646 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
648 return {"modules": translations_per_module,
649 "lang_parameters": None}
651 @openerpweb.jsonrequest
652 def translations(self, req, mods, lang):
653 res_lang = req.session.model('res.lang')
654 ids = res_lang.search([("code", "=", lang)])
657 lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
658 "grouping", "decimal_point", "thousands_sep"])
660 # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
661 # done server-side when the language is loaded, so we only need to load the user's lang.
662 ir_translation = req.session.model('ir.translation')
663 translations_per_module = {}
664 messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
665 ('comments','like','openerp-web'),('value','!=',False),
667 ['module','src','value','lang'], order='module')
668 for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
669 translations_per_module.setdefault(mod,{'messages':[]})
670 translations_per_module[mod]['messages'].extend({'id': m['src'],
671 'string': m['value']} \
673 return {"modules": translations_per_module,
674 "lang_parameters": lang_params}
676 @openerpweb.jsonrequest
677 def version_info(self, req):
678 return openerp.service.web_services.RPC_VERSION_1
680 class Proxy(openerpweb.Controller):
681 _cp_path = '/web/proxy'
683 @openerpweb.jsonrequest
684 def load(self, req, path):
685 """ Proxies an HTTP request through a JSON request.
687 It is strongly recommended to not request binary files through this,
688 as the result will be a binary data blob as well.
690 :param req: OpenERP request
691 :param path: actual request path
692 :return: file content
694 from werkzeug.test import Client
695 from werkzeug.wrappers import BaseResponse
697 return Client(req.httprequest.app, BaseResponse).get(path).data
699 class Database(openerpweb.Controller):
700 _cp_path = "/web/database"
702 @openerpweb.jsonrequest
703 def get_list(self, req):
706 @openerpweb.jsonrequest
707 def create(self, req, fields):
708 params = dict(map(operator.itemgetter('name', 'value'), fields))
709 return req.session.proxy("db").create_database(
710 params['super_admin_pwd'],
712 bool(params.get('demo_data')),
714 params['create_admin_pwd'])
716 @openerpweb.jsonrequest
717 def duplicate(self, req, fields):
718 params = dict(map(operator.itemgetter('name', 'value'), fields))
719 return req.session.proxy("db").duplicate_database(
720 params['super_admin_pwd'],
721 params['db_original_name'],
724 @openerpweb.jsonrequest
725 def duplicate(self, req, fields):
726 params = dict(map(operator.itemgetter('name', 'value'), fields))
728 params['super_admin_pwd'],
729 params['db_original_name'],
733 return req.session.proxy("db").duplicate_database(*duplicate_attrs)
735 @openerpweb.jsonrequest
736 def drop(self, req, fields):
737 password, db = operator.itemgetter(
738 'drop_pwd', 'drop_db')(
739 dict(map(operator.itemgetter('name', 'value'), fields)))
742 return req.session.proxy("db").drop(password, db)
743 except xmlrpclib.Fault, e:
744 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
745 return {'error': e.faultCode, 'title': 'Drop Database'}
746 return {'error': _('Could not drop database !'), 'title': _('Drop Database')}
748 @openerpweb.httprequest
749 def backup(self, req, backup_db, backup_pwd, token):
751 db_dump = base64.b64decode(
752 req.session.proxy("db").dump(backup_pwd, backup_db))
753 filename = "%(db)s_%(timestamp)s.dump" % {
755 'timestamp': datetime.datetime.utcnow().strftime(
756 "%Y-%m-%d_%H-%M-%SZ")
758 return req.make_response(db_dump,
759 [('Content-Type', 'application/octet-stream; charset=binary'),
760 ('Content-Disposition', content_disposition(filename, req))],
761 {'fileToken': int(token)}
763 except xmlrpclib.Fault, e:
764 return simplejson.dumps([[],[{'error': e.faultCode, 'title': _('Backup Database')}]])
766 @openerpweb.httprequest
767 def restore(self, req, db_file, restore_pwd, new_db):
769 data = base64.b64encode(db_file.read())
770 req.session.proxy("db").restore(restore_pwd, new_db, data)
772 except xmlrpclib.Fault, e:
773 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
774 raise Exception("AccessDenied")
776 @openerpweb.jsonrequest
777 def change_password(self, req, fields):
778 old_password, new_password = operator.itemgetter(
779 'old_pwd', 'new_pwd')(
780 dict(map(operator.itemgetter('name', 'value'), fields)))
782 return req.session.proxy("db").change_admin_password(old_password, new_password)
783 except xmlrpclib.Fault, e:
784 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
785 return {'error': e.faultCode, 'title': _('Change Password')}
786 return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
788 class Session(openerpweb.Controller):
789 _cp_path = "/web/session"
791 def session_info(self, req):
792 req.session.ensure_valid()
794 "session_id": req.session_id,
795 "uid": req.session._uid,
796 "user_context": req.session.get_context() if req.session._uid else {},
797 "db": req.session._db,
798 "username": req.session._login,
801 @openerpweb.jsonrequest
802 def get_session_info(self, req):
803 return self.session_info(req)
805 @openerpweb.jsonrequest
806 def authenticate(self, req, db, login, password, base_location=None):
807 wsgienv = req.httprequest.environ
809 base_location=base_location,
810 HTTP_HOST=wsgienv['HTTP_HOST'],
811 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
813 req.session.authenticate(db, login, password, env)
815 return self.session_info(req)
817 @openerpweb.jsonrequest
818 def change_password (self,req,fields):
819 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
820 dict(map(operator.itemgetter('name', 'value'), fields)))
821 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
822 return {'error':_('You cannot leave any password empty.'),'title': _('Change Password')}
823 if new_password != confirm_password:
824 return {'error': _('The new password and its confirmation must be identical.'),'title': _('Change Password')}
826 if req.session.model('res.users').change_password(
827 old_password, new_password):
828 return {'new_password':new_password}
830 return {'error': _('The old password you provided is incorrect, your password was not changed.'), 'title': _('Change Password')}
831 return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
833 @openerpweb.jsonrequest
834 def sc_list(self, req):
835 return req.session.model('ir.ui.view_sc').get_sc(
836 req.session._uid, "ir.ui.menu", req.context)
838 @openerpweb.jsonrequest
839 def get_lang_list(self, req):
841 return req.session.proxy("db").list_lang() or []
843 return {"error": e, "title": _("Languages")}
845 @openerpweb.jsonrequest
846 def modules(self, req):
847 # return all installed modules. Web client is smart enough to not load a module twice
848 return module_installed(req)
850 @openerpweb.jsonrequest
851 def save_session_action(self, req, the_action):
853 This method store an action object in the session object and returns an integer
854 identifying that action. The method get_session_action() can be used to get
857 :param the_action: The action to save in the session.
858 :type the_action: anything
859 :return: A key identifying the saved action.
862 saved_actions = req.httpsession.get('saved_actions')
863 if not saved_actions:
864 saved_actions = {"next":1, "actions":{}}
865 req.httpsession['saved_actions'] = saved_actions
866 # we don't allow more than 10 stored actions
867 if len(saved_actions["actions"]) >= 10:
868 del saved_actions["actions"][min(saved_actions["actions"])]
869 key = saved_actions["next"]
870 saved_actions["actions"][key] = the_action
871 saved_actions["next"] = key + 1
874 @openerpweb.jsonrequest
875 def get_session_action(self, req, key):
877 Gets back a previously saved action. This method can return None if the action
878 was saved since too much time (this case should be handled in a smart way).
880 :param key: The key given by save_session_action()
882 :return: The saved action or None.
885 saved_actions = req.httpsession.get('saved_actions')
886 if not saved_actions:
888 return saved_actions["actions"].get(key)
890 @openerpweb.jsonrequest
891 def check(self, req):
892 req.session.assert_valid()
895 @openerpweb.jsonrequest
896 def destroy(self, req):
897 req.session._suicide = True
899 class Menu(openerpweb.Controller):
900 _cp_path = "/web/menu"
902 @openerpweb.jsonrequest
903 def get_user_roots(self, req):
904 """ Return all root menu ids visible for the session user.
906 :param req: A request object, with an OpenERP session attribute
907 :type req: < session -> OpenERPSession >
908 :return: the root menu ids
912 Menus = s.model('ir.ui.menu')
913 # If a menu action is defined use its domain to get the root menu items
914 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'],
915 req.context)[0]['menu_id']
917 menu_domain = [('parent_id', '=', False)]
919 domain_string = s.model('ir.actions.act_window').read(
920 [user_menu_id[0]], ['domain'],req.context)[0]['domain']
922 menu_domain = ast.literal_eval(domain_string)
924 return Menus.search(menu_domain, 0, False, False, req.context)
926 @openerpweb.jsonrequest
928 """ Loads all menu items (all applications and their sub-menus).
930 :param req: A request object, with an OpenERP session attribute
931 :type req: < session -> OpenERPSession >
932 :return: the menu root
933 :rtype: dict('children': menu_nodes)
935 Menus = req.session.model('ir.ui.menu')
937 fields = ['name', 'sequence', 'parent_id', 'action']
938 menu_root_ids = self.get_user_roots(req)
939 menu_roots = Menus.read(menu_root_ids, fields, req.context) if menu_root_ids else []
943 'parent_id': [-1, ''],
944 'children': menu_roots,
945 'all_menu_ids': menu_root_ids,
950 # menus are loaded fully unlike a regular tree view, cause there are a
951 # limited number of items (752 when all 6.1 addons are installed)
952 menu_ids = Menus.search([('id', 'child_of', menu_root_ids)], 0, False, False, req.context)
953 menu_items = Menus.read(menu_ids, fields, req.context)
954 # adds roots at the end of the sequence, so that they will overwrite
955 # equivalent menu items from full menu read when put into id:item
956 # mapping, resulting in children being correctly set on the roots.
957 menu_items.extend(menu_roots)
958 menu_root['all_menu_ids'] = menu_ids # includes menu_root_ids!
960 # make a tree using parent_id
961 menu_items_map = dict(
962 (menu_item["id"], menu_item) for menu_item in menu_items)
963 for menu_item in menu_items:
964 if menu_item['parent_id']:
965 parent = menu_item['parent_id'][0]
968 if parent in menu_items_map:
969 menu_items_map[parent].setdefault(
970 'children', []).append(menu_item)
972 # sort by sequence a tree using parent_id
973 for menu_item in menu_items:
974 menu_item.setdefault('children', []).sort(
975 key=operator.itemgetter('sequence'))
979 @openerpweb.jsonrequest
980 def load_needaction(self, req, menu_ids):
981 """ Loads needaction counters for specific menu ids.
983 :return: needaction data
984 :rtype: dict(menu_id: {'needaction_enabled': boolean, 'needaction_counter': int})
986 return req.session.model('ir.ui.menu').get_needaction_data(menu_ids, req.context)
988 @openerpweb.jsonrequest
989 def action(self, req, menu_id):
990 # still used by web_shortcut
991 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
992 [('ir.ui.menu', menu_id)], False)
993 return {"action": actions}
995 class DataSet(openerpweb.Controller):
996 _cp_path = "/web/dataset"
998 @openerpweb.jsonrequest
999 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1000 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1001 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1003 """ Performs a search() followed by a read() (if needed) using the
1004 provided search criteria
1006 :param req: a JSON-RPC request object
1007 :type req: openerpweb.JsonRequest
1008 :param str model: the name of the model to search on
1009 :param fields: a list of the fields to return in the result records
1011 :param int offset: from which index should the results start being returned
1012 :param int limit: the maximum number of records to return
1013 :param list domain: the search domain for the query
1014 :param list sort: sorting directives
1015 :returns: A structure (dict) with two keys: ids (all the ids matching
1016 the (domain, context) pair) and records (paginated records
1017 matching fields selection set)
1020 Model = req.session.model(model)
1022 ids = Model.search(domain, offset or 0, limit or False, sort or False,
1024 if limit and len(ids) == limit:
1025 length = Model.search_count(domain, req.context)
1027 length = len(ids) + (offset or 0)
1028 if fields and fields == ['id']:
1029 # shortcut read if we only want the ids
1032 'records': [{'id': id} for id in ids]
1035 records = Model.read(ids, fields or False, req.context)
1036 records.sort(key=lambda obj: ids.index(obj['id']))
1042 @openerpweb.jsonrequest
1043 def load(self, req, model, id, fields):
1044 m = req.session.model(model)
1046 r = m.read([id], False, req.context)
1049 return {'value': value}
1051 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1052 return self._call_kw(req, model, method, args, {})
1054 def _call_kw(self, req, model, method, args, kwargs):
1055 # Temporary implements future display_name special field for model#read()
1056 if method == 'read' and kwargs.get('context', {}).get('future_display_name'):
1057 if 'display_name' in args[1]:
1058 names = dict(req.session.model(model).name_get(args[0], **kwargs))
1059 args[1].remove('display_name')
1060 records = req.session.model(model).read(*args, **kwargs)
1061 for record in records:
1062 record['display_name'] = \
1063 names.get(record['id']) or "%s#%d" % (model, (record['id']))
1066 return getattr(req.session.model(model), method)(*args, **kwargs)
1068 @openerpweb.jsonrequest
1069 def call(self, req, model, method, args, domain_id=None, context_id=None):
1070 return self._call_kw(req, model, method, args, {})
1072 @openerpweb.jsonrequest
1073 def call_kw(self, req, model, method, args, kwargs):
1074 return self._call_kw(req, model, method, args, kwargs)
1076 @openerpweb.jsonrequest
1077 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1078 action = self._call_kw(req, model, method, args, {})
1079 if isinstance(action, dict) and action.get('type') != '':
1080 return clean_action(req, action)
1083 @openerpweb.jsonrequest
1084 def exec_workflow(self, req, model, id, signal):
1085 return req.session.exec_workflow(model, id, signal)
1087 @openerpweb.jsonrequest
1088 def resequence(self, req, model, ids, field='sequence', offset=0):
1089 """ Re-sequences a number of records in the model, by their ids
1091 The re-sequencing starts at the first model of ``ids``, the sequence
1092 number is incremented by one after each record and starts at ``offset``
1094 :param ids: identifiers of the records to resequence, in the new sequence order
1096 :param str field: field used for sequence specification, defaults to
1098 :param int offset: sequence number for first record in ``ids``, allows
1099 starting the resequencing from an arbitrary number,
1102 m = req.session.model(model)
1103 if not m.fields_get([field]):
1105 # python 2.6 has no start parameter
1106 for i, id in enumerate(ids):
1107 m.write(id, { field: i + offset })
1110 class View(openerpweb.Controller):
1111 _cp_path = "/web/view"
1113 @openerpweb.jsonrequest
1114 def add_custom(self, req, view_id, arch):
1115 CustomView = req.session.model('ir.ui.view.custom')
1117 'user_id': req.session._uid,
1121 return {'result': True}
1123 @openerpweb.jsonrequest
1124 def undo_custom(self, req, view_id, reset=False):
1125 CustomView = req.session.model('ir.ui.view.custom')
1126 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1127 0, False, False, req.context)
1130 CustomView.unlink(vcustom, req.context)
1132 CustomView.unlink([vcustom[0]], req.context)
1133 return {'result': True}
1134 return {'result': False}
1136 class TreeView(View):
1137 _cp_path = "/web/treeview"
1139 @openerpweb.jsonrequest
1140 def action(self, req, model, id):
1141 return load_actions_from_ir_values(
1142 req,'action', 'tree_but_open',[(model, id)],
1145 class Binary(openerpweb.Controller):
1146 _cp_path = "/web/binary"
1148 @openerpweb.httprequest
1149 def image(self, req, model, id, field, **kw):
1150 last_update = '__last_update'
1151 Model = req.session.model(model)
1152 headers = [('Content-Type', 'image/png')]
1153 etag = req.httprequest.headers.get('If-None-Match')
1154 hashed_session = hashlib.md5(req.session_id).hexdigest()
1155 id = None if not id else simplejson.loads(id)
1156 if type(id) is list:
1159 if not id and hashed_session == etag:
1160 return werkzeug.wrappers.Response(status=304)
1162 date = Model.read([id], [last_update], req.context)[0].get(last_update)
1163 if hashlib.md5(date).hexdigest() == etag:
1164 return werkzeug.wrappers.Response(status=304)
1166 retag = hashed_session
1169 res = Model.default_get([field], req.context).get(field)
1172 res = Model.read([id], [last_update, field], req.context)[0]
1173 retag = hashlib.md5(res.get(last_update)).hexdigest()
1174 image_base64 = res.get(field)
1176 if kw.get('resize'):
1177 resize = kw.get('resize').split(',')
1178 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1179 width = int(resize[0])
1180 height = int(resize[1])
1181 # resize maximum 500*500
1182 if width > 500: width = 500
1183 if height > 500: height = 500
1184 image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1186 image_data = base64.b64decode(image_base64)
1188 except (TypeError, xmlrpclib.Fault):
1189 image_data = self.placeholder(req)
1190 headers.append(('ETag', retag))
1191 headers.append(('Content-Length', len(image_data)))
1193 ncache = int(kw.get('cache'))
1194 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1197 return req.make_response(image_data, headers)
1199 def placeholder(self, req, image='placeholder.png'):
1200 addons_path = openerpweb.addons_manifest['web']['addons_path']
1201 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', image), 'rb').read()
1203 @openerpweb.httprequest
1204 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1205 """ Download link for files stored as binary fields.
1207 If the ``id`` parameter is omitted, fetches the default value for the
1208 binary field (via ``default_get``), otherwise fetches the field for
1209 that precise record.
1211 :param req: OpenERP request
1212 :type req: :class:`web.common.http.HttpRequest`
1213 :param str model: name of the model to fetch the binary from
1214 :param str field: binary field
1215 :param str id: id of the record from which to fetch the binary
1216 :param str filename_field: field holding the file's name, if any
1217 :returns: :class:`werkzeug.wrappers.Response`
1219 Model = req.session.model(model)
1222 fields.append(filename_field)
1224 res = Model.read([int(id)], fields, req.context)[0]
1226 res = Model.default_get(fields, req.context)
1227 filecontent = base64.b64decode(res.get(field, ''))
1229 return req.not_found()
1231 filename = '%s_%s' % (model.replace('.', '_'), id)
1233 filename = res.get(filename_field, '') or filename
1234 return req.make_response(filecontent,
1235 [('Content-Type', 'application/octet-stream'),
1236 ('Content-Disposition', content_disposition(filename, req))])
1238 @openerpweb.httprequest
1239 def saveas_ajax(self, req, data, token):
1240 jdata = simplejson.loads(data)
1241 model = jdata['model']
1242 field = jdata['field']
1243 data = jdata['data']
1244 id = jdata.get('id', None)
1245 filename_field = jdata.get('filename_field', None)
1246 context = jdata.get('context', {})
1248 Model = req.session.model(model)
1251 fields.append(filename_field)
1253 res = { field: data }
1255 res = Model.read([int(id)], fields, context)[0]
1257 res = Model.default_get(fields, context)
1258 filecontent = base64.b64decode(res.get(field, ''))
1260 raise ValueError(_("No content found for field '%s' on '%s:%s'") %
1263 filename = '%s_%s' % (model.replace('.', '_'), id)
1265 filename = res.get(filename_field, '') or filename
1266 return req.make_response(filecontent,
1267 headers=[('Content-Type', 'application/octet-stream'),
1268 ('Content-Disposition', content_disposition(filename, req))],
1269 cookies={'fileToken': int(token)})
1271 @openerpweb.httprequest
1272 def upload(self, req, callback, ufile):
1273 # TODO: might be useful to have a configuration flag for max-length file uploads
1274 out = """<script language="javascript" type="text/javascript">
1275 var win = window.top.window;
1276 win.jQuery(win).trigger(%s, %s);
1280 args = [len(data), ufile.filename,
1281 ufile.content_type, base64.b64encode(data)]
1282 except Exception, e:
1283 args = [False, e.message]
1284 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1286 @openerpweb.httprequest
1287 def upload_attachment(self, req, callback, model, id, ufile):
1288 Model = req.session.model('ir.attachment')
1289 out = """<script language="javascript" type="text/javascript">
1290 var win = window.top.window;
1291 win.jQuery(win).trigger(%s, %s);
1294 attachment_id = Model.create({
1295 'name': ufile.filename,
1296 'datas': base64.encodestring(ufile.read()),
1297 'datas_fname': ufile.filename,
1302 'filename': ufile.filename,
1305 except xmlrpclib.Fault, e:
1306 args = {'error':e.faultCode }
1307 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1309 @openerpweb.httprequest
1310 def company_logo(self, req, dbname=None):
1311 # TODO add etag, refactor to use /image code for etag
1314 dbname = req.session._db
1315 uid = req.session._uid
1316 elif dbname is None:
1317 dbname = db_monodb(req)
1320 uid = openerp.SUPERUSER_ID
1323 image_data = self.placeholder(req, 'logo.png')
1325 registry = openerp.modules.registry.RegistryManager.get(dbname)
1326 with registry.cursor() as cr:
1327 user = registry.get('res.users').browse(cr, uid, uid)
1328 if user.company_id.logo_web:
1329 image_data = user.company_id.logo_web.decode('base64')
1331 image_data = self.placeholder(req, 'nologo.png')
1333 ('Content-Type', 'image/png'),
1334 ('Content-Length', len(image_data)),
1336 return req.make_response(image_data, headers)
1338 class Action(openerpweb.Controller):
1339 _cp_path = "/web/action"
1341 @openerpweb.jsonrequest
1342 def load(self, req, action_id, do_not_eval=False):
1343 Actions = req.session.model('ir.actions.actions')
1346 action_id = int(action_id)
1349 module, xmlid = action_id.split('.', 1)
1350 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1351 assert model.startswith('ir.actions.')
1353 action_id = 0 # force failed read
1355 base_action = Actions.read([action_id], ['type'], req.context)
1358 action_type = base_action[0]['type']
1359 if action_type == 'ir.actions.report.xml':
1360 ctx.update({'bin_size': True})
1361 ctx.update(req.context)
1362 action = req.session.model(action_type).read([action_id], False, ctx)
1364 value = clean_action(req, action[0])
1367 @openerpweb.jsonrequest
1368 def run(self, req, action_id):
1369 return_action = req.session.model('ir.actions.server').run(
1370 [action_id], req.context)
1372 return clean_action(req, return_action)
1377 _cp_path = "/web/export"
1379 @openerpweb.jsonrequest
1380 def formats(self, req):
1381 """ Returns all valid export formats
1383 :returns: for each export format, a pair of identifier and printable name
1384 :rtype: [(str, str)]
1388 for path, controller in openerpweb.controllers_path.iteritems()
1389 if path.startswith(self._cp_path)
1390 if hasattr(controller, 'fmt')
1391 ], key=operator.itemgetter("label"))
1393 def fields_get(self, req, model):
1394 Model = req.session.model(model)
1395 fields = Model.fields_get(False, req.context)
1398 @openerpweb.jsonrequest
1399 def get_fields(self, req, model, prefix='', parent_name= '',
1400 import_compat=True, parent_field_type=None,
1403 if import_compat and parent_field_type == "many2one":
1406 fields = self.fields_get(req, model)
1409 fields.pop('id', None)
1411 fields['.id'] = fields.pop('id', {'string': 'ID'})
1413 fields_sequence = sorted(fields.iteritems(),
1414 key=lambda field: field[1].get('string', ''))
1417 for field_name, field in fields_sequence:
1419 if exclude and field_name in exclude:
1421 if field.get('readonly'):
1422 # If none of the field's states unsets readonly, skip the field
1423 if all(dict(attrs).get('readonly', True)
1424 for attrs in field.get('states', {}).values()):
1427 id = prefix + (prefix and '/'or '') + field_name
1428 name = parent_name + (parent_name and '/' or '') + field['string']
1429 record = {'id': id, 'string': name,
1430 'value': id, 'children': False,
1431 'field_type': field.get('type'),
1432 'required': field.get('required'),
1433 'relation_field': field.get('relation_field')}
1434 records.append(record)
1436 if len(name.split('/')) < 3 and 'relation' in field:
1437 ref = field.pop('relation')
1438 record['value'] += '/id'
1439 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1441 if not import_compat or field['type'] == 'one2many':
1442 # m2m field in import_compat is childless
1443 record['children'] = True
1447 @openerpweb.jsonrequest
1448 def namelist(self,req, model, export_id):
1449 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1450 export = req.session.model("ir.exports").read([export_id])[0]
1451 export_fields_list = req.session.model("ir.exports.line").read(
1452 export['export_fields'])
1454 fields_data = self.fields_info(
1455 req, model, map(operator.itemgetter('name'), export_fields_list))
1458 {'name': field['name'], 'label': fields_data[field['name']]}
1459 for field in export_fields_list
1462 def fields_info(self, req, model, export_fields):
1464 fields = self.fields_get(req, model)
1465 if ".id" in export_fields:
1466 fields['.id'] = fields.pop('id', {'string': 'ID'})
1468 # To make fields retrieval more efficient, fetch all sub-fields of a
1469 # given field at the same time. Because the order in the export list is
1470 # arbitrary, this requires ordering all sub-fields of a given field
1471 # together so they can be fetched at the same time
1473 # Works the following way:
1474 # * sort the list of fields to export, the default sorting order will
1475 # put the field itself (if present, for xmlid) and all of its
1476 # sub-fields right after it
1477 # * then, group on: the first field of the path (which is the same for
1478 # a field and for its subfields and the length of splitting on the
1479 # first '/', which basically means grouping the field on one side and
1480 # all of the subfields on the other. This way, we have the field (for
1481 # the xmlid) with length 1, and all of the subfields with the same
1482 # base but a length "flag" of 2
1483 # * if we have a normal field (length 1), just add it to the info
1484 # mapping (with its string) as-is
1485 # * otherwise, recursively call fields_info via graft_subfields.
1486 # all graft_subfields does is take the result of fields_info (on the
1487 # field's model) and prepend the current base (current field), which
1488 # rebuilds the whole sub-tree for the field
1490 # result: because we're not fetching the fields_get for half the
1491 # database models, fetching a namelist with a dozen fields (including
1492 # relational data) falls from ~6s to ~300ms (on the leads model).
1493 # export lists with no sub-fields (e.g. import_compatible lists with
1494 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1495 # there's a single fields_get to execute)
1496 for (base, length), subfields in itertools.groupby(
1497 sorted(export_fields),
1498 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1499 subfields = list(subfields)
1501 # subfields is a seq of $base/*rest, and not loaded yet
1502 info.update(self.graft_subfields(
1503 req, fields[base]['relation'], base, fields[base]['string'],
1507 info[base] = fields[base]['string']
1511 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1512 export_fields = [field.split('/', 1)[1] for field in fields]
1514 (prefix + '/' + k, prefix_string + '/' + v)
1515 for k, v in self.fields_info(req, model, export_fields).iteritems())
1517 #noinspection PyPropertyDefinition
1519 def content_type(self):
1520 """ Provides the format's content type """
1521 raise NotImplementedError()
1523 def filename(self, base):
1524 """ Creates a valid filename for the format (with extension) from the
1525 provided base name (exension-less)
1527 raise NotImplementedError()
1529 def from_data(self, fields, rows):
1530 """ Conversion method from OpenERP's export data to whatever the
1531 current export class outputs
1533 :params list fields: a list of fields to export
1534 :params list rows: a list of records to export
1538 raise NotImplementedError()
1540 @openerpweb.httprequest
1541 def index(self, req, data, token):
1542 model, fields, ids, domain, import_compat = \
1543 operator.itemgetter('model', 'fields', 'ids', 'domain',
1545 simplejson.loads(data))
1547 Model = req.session.model(model)
1548 ids = ids or Model.search(domain, 0, False, False, req.context)
1550 field_names = map(operator.itemgetter('name'), fields)
1551 import_data = Model.export_data(ids, field_names, req.context).get('datas',[])
1554 columns_headers = field_names
1556 columns_headers = [val['label'].strip() for val in fields]
1559 return req.make_response(self.from_data(columns_headers, import_data),
1560 headers=[('Content-Disposition',
1561 content_disposition(self.filename(model), req)),
1562 ('Content-Type', self.content_type)],
1563 cookies={'fileToken': int(token)})
1565 class CSVExport(Export):
1566 _cp_path = '/web/export/csv'
1567 fmt = {'tag': 'csv', 'label': 'CSV'}
1570 def content_type(self):
1571 return 'text/csv;charset=utf8'
1573 def filename(self, base):
1574 return base + '.csv'
1576 def from_data(self, fields, rows):
1578 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1580 writer.writerow([name.encode('utf-8') for name in fields])
1585 if isinstance(d, basestring):
1586 d = d.replace('\n',' ').replace('\t',' ')
1588 d = d.encode('utf-8')
1589 except UnicodeError:
1591 if d is False: d = None
1593 writer.writerow(row)
1600 class ExcelExport(Export):
1601 _cp_path = '/web/export/xls'
1605 'error': None if xlwt else "XLWT required"
1609 def content_type(self):
1610 return 'application/vnd.ms-excel'
1612 def filename(self, base):
1613 return base + '.xls'
1615 def from_data(self, fields, rows):
1616 workbook = xlwt.Workbook()
1617 worksheet = workbook.add_sheet('Sheet 1')
1619 for i, fieldname in enumerate(fields):
1620 worksheet.write(0, i, fieldname)
1621 worksheet.col(i).width = 8000 # around 220 pixels
1623 style = xlwt.easyxf('align: wrap yes')
1625 for row_index, row in enumerate(rows):
1626 for cell_index, cell_value in enumerate(row):
1627 if isinstance(cell_value, basestring):
1628 cell_value = re.sub("\r", " ", cell_value)
1629 if cell_value is False: cell_value = None
1630 worksheet.write(row_index + 1, cell_index, cell_value, style)
1639 class Reports(View):
1640 _cp_path = "/web/report"
1641 POLLING_DELAY = 0.25
1643 'doc': 'application/vnd.ms-word',
1644 'html': 'text/html',
1645 'odt': 'application/vnd.oasis.opendocument.text',
1646 'pdf': 'application/pdf',
1647 'sxw': 'application/vnd.sun.xml.writer',
1648 'xls': 'application/vnd.ms-excel',
1651 @openerpweb.httprequest
1652 def index(self, req, action, token):
1653 action = simplejson.loads(action)
1655 report_srv = req.session.proxy("report")
1656 context = dict(req.context)
1657 context.update(action["context"])
1660 report_ids = context["active_ids"]
1661 if 'report_type' in action:
1662 report_data['report_type'] = action['report_type']
1663 if 'datas' in action:
1664 if 'ids' in action['datas']:
1665 report_ids = action['datas'].pop('ids')
1666 report_data.update(action['datas'])
1668 report_id = report_srv.report(
1669 req.session._db, req.session._uid, req.session._password,
1670 action["report_name"], report_ids,
1671 report_data, context)
1673 report_struct = None
1675 report_struct = report_srv.report_get(
1676 req.session._db, req.session._uid, req.session._password, report_id)
1677 if report_struct["state"]:
1680 time.sleep(self.POLLING_DELAY)
1682 report = base64.b64decode(report_struct['result'])
1683 if report_struct.get('code') == 'zlib':
1684 report = zlib.decompress(report)
1685 report_mimetype = self.TYPES_MAPPING.get(
1686 report_struct['format'], 'octet-stream')
1687 file_name = action.get('name', 'report')
1688 if 'name' not in action:
1689 reports = req.session.model('ir.actions.report.xml')
1690 res_id = reports.search([('report_name', '=', action['report_name']),],
1691 0, False, False, context)
1693 file_name = reports.read(res_id[0], ['name'], context)['name']
1695 file_name = action['report_name']
1696 file_name = '%s.%s' % (file_name, report_struct['format'])
1698 return req.make_response(report,
1700 ('Content-Disposition', content_disposition(file_name, req)),
1701 ('Content-Type', report_mimetype),
1702 ('Content-Length', len(report))],
1703 cookies={'fileToken': int(token)})
1705 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: