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 #----------------------------------------------------------
35 #----------------------------------------------------------
38 """ Minify js with a clever regex.
39 Taken from http://opensource.perlig.de/rjsmin
40 Apache License, Version 2.0 """
42 """ Substitution callback """
43 groups = match.groups()
49 (groups[4] and '\n') or
50 (groups[5] and ' ') or
51 (groups[6] and ' ') or
52 (groups[7] and ' ') or
57 r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
58 r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
59 r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
60 r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
61 r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
62 r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
63 r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
64 r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
65 r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
66 r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
67 r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
68 r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
69 r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
70 r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
71 r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
72 r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
73 r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
74 r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
75 r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
76 r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
77 r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
82 # Validated by diff -u of sass2scss against:
83 # sass-convert -F sass -T scss openerp.sass openerp.scss
86 reComment = re.compile(r'//.*$')
87 reIndent = re.compile(r'^\s+')
88 reIgnore = re.compile(r'^\s*(//.*)?$')
89 reFixes = { re.compile(r'\(\((.*)\)\)') : r'(\1)', }
92 for l in src.split('\n'):
94 if reIgnore.search(l): continue
95 l = reComment.sub('', l)
97 indent = reIndent.match(l)
98 level = indent.end() if indent else 0
101 prevBlocks[lastLevel] = block
103 block[-1] = (block[-1], newBlock)
105 elif level<lastLevel:
106 block = prevBlocks[level]
110 for ereg, repl in reFixes.items():
111 l = ereg.sub(repl if type(repl)==str else repl(), l)
114 def write(sass, level=-1):
117 if type(sass)==tuple:
119 out += indent+sass[0]+" {\n"
121 out += write(e, level+1)
123 out = out.rstrip(" \n")
128 out += indent+sass+";\n"
134 proxy = req.session.proxy("db")
136 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
138 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
139 dbs = [i for i in dbs if re.match(r, i)]
142 def module_topological_sort(modules):
143 """ Return a list of module names sorted so that their dependencies of the
144 modules are listed before the module itself
146 modules is a dict of {module_name: dependencies}
148 :param modules: modules to sort
153 dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
154 # incoming edge: dependency on other module (if a depends on b, a has an
155 # incoming edge from b, aka there's an edge from b to a)
156 # outgoing edge: other module depending on this one
158 # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
159 #L ← Empty list that will contain the sorted nodes
161 #S ← Set of all nodes with no outgoing edges (modules on which no other
163 S = set(module for module in modules if module not in dependencies)
166 #function visit(node n)
168 #if n has not been visited yet then
172 #change: n not web module, can not be resolved, ignore
173 if n not in modules: return
174 #for each node m with an edge from m to n do (dependencies of n)
180 #for each node n in S do
186 def module_installed(req):
187 # Candidates module the current heuristic is the /static dir
188 loadable = openerpweb.addons_manifest.keys()
191 # Retrieve database installed modules
192 # TODO The following code should move to ir.module.module.list_installed_modules()
193 Modules = req.session.model('ir.module.module')
194 domain = [('state','=','installed'), ('name','in', loadable)]
195 for module in Modules.search_read(domain, ['name', 'dependencies_id']):
196 modules[module['name']] = []
197 deps = module.get('dependencies_id')
199 deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
200 dependencies = [i['name'] for i in deps_read]
201 modules[module['name']] = dependencies
203 sorted_modules = module_topological_sort(modules)
204 return sorted_modules
206 def module_installed_bypass_session(dbname):
207 loadable = openerpweb.addons_manifest.keys()
210 import openerp.modules.registry
211 registry = openerp.modules.registry.RegistryManager.get(dbname)
212 with registry.cursor() as cr:
213 m = registry.get('ir.module.module')
214 # TODO The following code should move to ir.module.module.list_installed_modules()
215 domain = [('state','=','installed'), ('name','in', loadable)]
216 ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
217 for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
218 modules[module['name']] = []
219 deps = module.get('dependencies_id')
221 deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
222 dependencies = [i['name'] for i in deps_read]
223 modules[module['name']] = dependencies
226 sorted_modules = module_topological_sort(modules)
227 return sorted_modules
229 def module_boot(req):
230 return [m for m in req.config.server_wide_modules if m in openerpweb.addons_manifest]
231 # TODO the following will be enabled once we separate the module code and translation loading
234 for i in req.config.server_wide_modules:
235 if i in openerpweb.addons_manifest:
237 # if only one db load every module at boot
241 except xmlrpclib.Fault:
242 # ignore access denied
245 dbside = module_installed_bypass_session(dbs[0])
246 dbside = [i for i in dbside if i not in serverside]
247 addons = serverside + dbside
250 def concat_xml(file_list):
251 """Concatenate xml files
253 :param list(str) file_list: list of files to check
254 :returns: (concatenation_result, checksum)
257 checksum = hashlib.new('sha1')
259 return '', checksum.hexdigest()
262 for fname in file_list:
263 with open(fname, 'rb') as fp:
265 checksum.update(contents)
267 xml = ElementTree.parse(fp).getroot()
270 root = ElementTree.Element(xml.tag)
271 #elif root.tag != xml.tag:
272 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
274 for child in xml.getchildren():
276 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
278 def concat_files(file_list, reader=None, intersperse=""):
279 """ Concatenates contents of all provided files
281 :param list(str) file_list: list of files to check
282 :param function reader: reading procedure for each file
283 :param str intersperse: string to intersperse between file contents
284 :returns: (concatenation_result, checksum)
287 checksum = hashlib.new('sha1')
289 return '', checksum.hexdigest()
293 with open(f, 'rb') as fp:
297 for fname in file_list:
298 contents = reader(fname)
299 checksum.update(contents)
300 files_content.append(contents)
302 files_concat = intersperse.join(files_content)
303 return files_concat, checksum.hexdigest()
305 def concat_js(file_list):
306 content, checksum = concat_files(file_list, intersperse=';')
307 content = rjsmin(content)
308 return content, checksum
310 def manifest_glob(req, addons, key):
312 addons = module_boot(req)
314 addons = addons.split(',')
317 manifest = openerpweb.addons_manifest.get(addon, None)
320 # ensure does not ends with /
321 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
322 globlist = manifest.get(key, [])
323 for pattern in globlist:
324 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
325 r.append((path, path[len(addons_path):]))
328 def manifest_list(req, mods, extension):
330 path = '/web/webclient/' + extension
332 path += '?mods=' + mods
334 files = manifest_glob(req, mods, extension)
335 i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
336 req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
338 return [wp for _fp, wp in files]
340 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
342 def get_last_modified(files):
343 """ Returns the modification time of the most recently modified
346 :param list(str) files: names of files to check
347 :return: most recent modification time amongst the fileset
348 :rtype: datetime.datetime
352 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
354 return datetime.datetime(1970, 1, 1)
356 def make_conditional(req, response, last_modified=None, etag=None):
357 """ Makes the provided response conditional based upon the request,
358 and mandates revalidation from clients
360 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
361 setting ``last_modified`` and ``etag`` correctly on the response object
363 :param req: OpenERP request
364 :type req: web.common.http.WebRequest
365 :param response: Werkzeug response
366 :type response: werkzeug.wrappers.Response
367 :param datetime.datetime last_modified: last modification date of the response content
368 :param str etag: some sort of checksum of the content (deep etag)
369 :return: the response object provided
370 :rtype: werkzeug.wrappers.Response
372 response.cache_control.must_revalidate = True
373 response.cache_control.max_age = 0
375 response.last_modified = last_modified
377 response.set_etag(etag)
378 return response.make_conditional(req.httprequest)
380 def login_and_redirect(req, db, login, key, redirect_url='/'):
381 req.session.authenticate(db, login, key, {})
382 return set_cookie_and_redirect(req, redirect_url)
384 def set_cookie_and_redirect(req, redirect_url):
385 redirect = werkzeug.utils.redirect(redirect_url, 303)
386 redirect.autocorrect_location_header = False
387 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
388 redirect.set_cookie('instance0|session_id', cookie_val)
391 def eval_context_and_domain(session, context, domain=None):
392 e_context = session.eval_context(context)
393 # should we give the evaluated context as an evaluation context to the domain?
394 e_domain = session.eval_domain(domain or [])
396 return e_context, e_domain
398 def load_actions_from_ir_values(req, key, key2, models, meta):
399 context = req.session.eval_context(req.context)
400 Values = req.session.model('ir.values')
401 actions = Values.get(key, key2, models, meta, context)
403 return [(id, name, clean_action(req, action))
404 for id, name, action in actions]
406 def clean_action(req, action, do_not_eval=False):
407 action.setdefault('flags', {})
409 context = req.session.eval_context(req.context)
410 eval_ctx = req.session.evaluation_context(context)
413 # values come from the server, we can just eval them
414 if action.get('context') and isinstance(action.get('context'), basestring):
415 action['context'] = eval( action['context'], eval_ctx ) or {}
417 if action.get('domain') and isinstance(action.get('domain'), basestring):
418 action['domain'] = eval( action['domain'], eval_ctx ) or []
420 if 'context' in action:
421 action['context'] = parse_context(action['context'], req.session)
422 if 'domain' in action:
423 action['domain'] = parse_domain(action['domain'], req.session)
425 action_type = action.setdefault('type', 'ir.actions.act_window_close')
426 if action_type == 'ir.actions.act_window':
427 return fix_view_modes(action)
430 # I think generate_views,fix_view_modes should go into js ActionManager
431 def generate_views(action):
433 While the server generates a sequence called "views" computing dependencies
434 between a bunch of stuff for views coming directly from the database
435 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
436 to return custom view dictionaries generated on the fly.
438 In that case, there is no ``views`` key available on the action.
440 Since the web client relies on ``action['views']``, generate it here from
441 ``view_mode`` and ``view_id``.
443 Currently handles two different cases:
445 * no view_id, multiple view_mode
446 * single view_id, single view_mode
448 :param dict action: action descriptor dictionary to generate a views key for
450 view_id = action.get('view_id') or False
451 if isinstance(view_id, (list, tuple)):
454 # providing at least one view mode is a requirement, not an option
455 view_modes = action['view_mode'].split(',')
457 if len(view_modes) > 1:
459 raise ValueError('Non-db action dictionaries should provide '
460 'either multiple view modes or a single view '
461 'mode and an optional view id.\n\n Got view '
462 'modes %r and view id %r for action %r' % (
463 view_modes, view_id, action))
464 action['views'] = [(False, mode) for mode in view_modes]
466 action['views'] = [(view_id, view_modes[0])]
468 def fix_view_modes(action):
469 """ For historical reasons, OpenERP has weird dealings in relation to
470 view_mode and the view_type attribute (on window actions):
472 * one of the view modes is ``tree``, which stands for both list views
474 * the choice is made by checking ``view_type``, which is either
475 ``form`` for a list view or ``tree`` for an actual tree view
477 This methods simply folds the view_type into view_mode by adding a
478 new view mode ``list`` which is the result of the ``tree`` view_mode
479 in conjunction with the ``form`` view_type.
481 TODO: this should go into the doc, some kind of "peculiarities" section
483 :param dict action: an action descriptor
484 :returns: nothing, the action is modified in place
486 if not action.get('views'):
487 generate_views(action)
489 if action.pop('view_type', 'form') != 'form':
492 if 'view_mode' in action:
493 action['view_mode'] = ','.join(
494 mode if mode != 'tree' else 'list'
495 for mode in action['view_mode'].split(','))
497 [id, mode if mode != 'tree' else 'list']
498 for id, mode in action['views']
503 def parse_domain(domain, session):
504 """ Parses an arbitrary string containing a domain, transforms it
505 to either a literal domain or a :class:`common.nonliterals.Domain`
507 :param domain: the domain to parse, if the domain is not a string it
508 is assumed to be a literal domain and is returned as-is
509 :param session: Current OpenERP session
510 :type session: openerpweb.openerpweb.OpenERPSession
512 if not isinstance(domain, basestring):
515 return ast.literal_eval(domain)
518 return common.nonliterals.Domain(session, domain)
520 def parse_context(context, session):
521 """ Parses an arbitrary string containing a context, transforms it
522 to either a literal context or a :class:`common.nonliterals.Context`
524 :param context: the context to parse, if the context is not a string it
525 is assumed to be a literal domain and is returned as-is
526 :param session: Current OpenERP session
527 :type session: openerpweb.openerpweb.OpenERPSession
529 if not isinstance(context, basestring):
532 return ast.literal_eval(context)
534 return common.nonliterals.Context(session, context)
537 def _local_web_translations(trans_file):
540 with open(trans_file) as t_file:
541 po = babel.messages.pofile.read_po(t_file)
545 if x.id and x.string and "openerp-web" in x.auto_comments:
546 messages.append({'id': x.id, 'string': x.string})
550 #----------------------------------------------------------
551 # OpenERP Web web Controllers
552 #----------------------------------------------------------
554 html_template = """<!DOCTYPE html>
555 <html style="height: 100%%">
557 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
558 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
559 <title>OpenERP</title>
560 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
561 <link rel="stylesheet" href="/web/static/src/css/full.css" />
564 <script type="text/javascript">
566 var s = new openerp.init(%(modules)s);
573 <script type="text/javascript"
574 src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
576 var test = function() {
581 if (window.localStorage && false) {
582 if (! localStorage.getItem("hasShownGFramePopup")) {
584 localStorage.setItem("hasShownGFramePopup", true);
595 class Home(openerpweb.Controller):
598 @openerpweb.httprequest
599 def index(self, req, s_action=None, **kw):
600 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, None, 'js'))
601 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, None, 'css'))
603 r = html_template % {
606 'modules': simplejson.dumps(module_boot(req)),
607 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
611 @openerpweb.httprequest
612 def login(self, req, db, login, key):
613 return login_and_redirect(req, db, login, key)
616 class WebClient(openerpweb.Controller):
617 _cp_path = "/web/webclient"
619 @openerpweb.jsonrequest
620 def csslist(self, req, mods=None):
621 return manifest_list(req, mods, 'css')
623 @openerpweb.jsonrequest
624 def jslist(self, req, mods=None):
625 return manifest_list(req, mods, 'js')
627 @openerpweb.jsonrequest
628 def qweblist(self, req, mods=None):
629 return manifest_list(req, mods, 'qweb')
631 @openerpweb.httprequest
632 def css(self, req, mods=None):
633 files = list(manifest_glob(req, mods, 'css'))
634 last_modified = get_last_modified(f[0] for f in files)
635 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
636 return werkzeug.wrappers.Response(status=304)
638 file_map = dict(files)
640 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
641 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
644 """read the a css file and absolutify all relative uris"""
645 with open(f, 'rb') as fp:
646 data = fp.read().decode('utf-8')
649 # convert FS path into web path
650 web_dir = '/'.join(os.path.dirname(path).split(os.path.sep))
654 r"""@import \1%s/""" % (web_dir,),
660 r"""url(\1%s/""" % (web_dir,),
663 return data.encode('utf-8')
665 content, checksum = concat_files((f[0] for f in files), reader)
667 return make_conditional(
668 req, req.make_response(content, [('Content-Type', 'text/css')]),
669 last_modified, checksum)
671 @openerpweb.httprequest
672 def js(self, req, mods=None):
673 files = [f[0] for f in manifest_glob(req, mods, 'js')]
674 last_modified = get_last_modified(files)
675 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
676 return werkzeug.wrappers.Response(status=304)
678 content, checksum = concat_js(files)
680 return make_conditional(
681 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
682 last_modified, checksum)
684 @openerpweb.httprequest
685 def qweb(self, req, mods=None):
686 files = [f[0] for f in manifest_glob(req, mods, 'qweb')]
687 last_modified = get_last_modified(files)
688 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
689 return werkzeug.wrappers.Response(status=304)
691 content, checksum = concat_xml(files)
693 return make_conditional(
694 req, req.make_response(content, [('Content-Type', 'text/xml')]),
695 last_modified, checksum)
697 @openerpweb.jsonrequest
698 def bootstrap_translations(self, req, mods):
699 """ Load local translations from *.po files, as a temporary solution
700 until we have established a valid session. This is meant only
701 for translating the login page and db management chrome, using
702 the browser's language. """
703 lang = req.httprequest.accept_languages.best or 'en'
704 # For performance reasons we only load a single translation, so for
705 # sub-languages (that should only be partially translated) we load the
706 # main language PO instead - that should be enough for the login screen.
707 if '-' in lang: # RFC2616 uses '-' separators for sublanguages
708 lang = lang.split('-')[0]
710 translations_per_module = {}
711 for addon_name in mods:
712 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
713 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
714 if not os.path.exists(f_name):
716 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
718 return {"modules": translations_per_module,
719 "lang_parameters": None}
721 @openerpweb.jsonrequest
722 def translations(self, req, mods, lang):
723 res_lang = req.session.model('res.lang')
724 ids = res_lang.search([("code", "=", lang)])
727 lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
728 "grouping", "decimal_point", "thousands_sep"])
730 # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
731 # done server-side when the language is loaded, so we only need to load the user's lang.
732 ir_translation = req.session.model('ir.translation')
733 translations_per_module = {}
734 messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
735 ('comments','like','openerp-web'),('value','!=',False),
737 ['module','src','value','lang'], order='module')
738 for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
739 translations_per_module.setdefault(mod,{'messages':[]})
740 translations_per_module[mod]['messages'].extend({'id': m['src'],
741 'string': m['value']} \
743 return {"modules": translations_per_module,
744 "lang_parameters": lang_params}
746 @openerpweb.jsonrequest
747 def version_info(self, req):
748 return req.session.proxy('common').version()['openerp']
750 class Proxy(openerpweb.Controller):
751 _cp_path = '/web/proxy'
753 @openerpweb.jsonrequest
754 def load(self, req, path):
755 """ Proxies an HTTP request through a JSON request.
757 It is strongly recommended to not request binary files through this,
758 as the result will be a binary data blob as well.
760 :param req: OpenERP request
761 :param path: actual request path
762 :return: file content
764 from werkzeug.test import Client
765 from werkzeug.wrappers import BaseResponse
767 return Client(req.httprequest.app, BaseResponse).get(path).data
769 class Database(openerpweb.Controller):
770 _cp_path = "/web/database"
772 @openerpweb.jsonrequest
773 def get_list(self, req):
775 return {"db_list": dbs}
777 @openerpweb.jsonrequest
778 def create(self, req, fields):
779 params = dict(map(operator.itemgetter('name', 'value'), fields))
781 params['super_admin_pwd'],
783 bool(params.get('demo_data')),
785 params['create_admin_pwd']
788 return req.session.proxy("db").create_database(*create_attrs)
790 @openerpweb.jsonrequest
791 def drop(self, req, fields):
792 password, db = operator.itemgetter(
793 'drop_pwd', 'drop_db')(
794 dict(map(operator.itemgetter('name', 'value'), fields)))
797 return req.session.proxy("db").drop(password, db)
798 except xmlrpclib.Fault, e:
799 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
800 return {'error': e.faultCode, 'title': 'Drop Database'}
801 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
803 @openerpweb.httprequest
804 def backup(self, req, backup_db, backup_pwd, token):
806 db_dump = base64.b64decode(
807 req.session.proxy("db").dump(backup_pwd, backup_db))
808 filename = "%(db)s_%(timestamp)s.dump" % {
810 'timestamp': datetime.datetime.utcnow().strftime(
811 "%Y-%m-%d_%H-%M-%SZ")
813 return req.make_response(db_dump,
814 [('Content-Type', 'application/octet-stream; charset=binary'),
815 ('Content-Disposition', 'attachment; filename="' + filename + '"')],
816 {'fileToken': int(token)}
818 except xmlrpclib.Fault, e:
819 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
821 @openerpweb.httprequest
822 def restore(self, req, db_file, restore_pwd, new_db):
824 data = base64.b64encode(db_file.read())
825 req.session.proxy("db").restore(restore_pwd, new_db, data)
827 except xmlrpclib.Fault, e:
828 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
829 raise Exception("AccessDenied")
831 @openerpweb.jsonrequest
832 def change_password(self, req, fields):
833 old_password, new_password = operator.itemgetter(
834 'old_pwd', 'new_pwd')(
835 dict(map(operator.itemgetter('name', 'value'), fields)))
837 return req.session.proxy("db").change_admin_password(old_password, new_password)
838 except xmlrpclib.Fault, e:
839 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
840 return {'error': e.faultCode, 'title': 'Change Password'}
841 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
843 class Session(openerpweb.Controller):
844 _cp_path = "/web/session"
846 def session_info(self, req):
847 req.session.ensure_valid()
849 "session_id": req.session_id,
850 "uid": req.session._uid,
851 "context": req.session.get_context() if req.session._uid else {},
852 "db": req.session._db,
853 "login": req.session._login,
856 @openerpweb.jsonrequest
857 def get_session_info(self, req):
858 return self.session_info(req)
860 @openerpweb.jsonrequest
861 def authenticate(self, req, db, login, password, base_location=None):
862 wsgienv = req.httprequest.environ
863 release = common.release
865 base_location=base_location,
866 HTTP_HOST=wsgienv['HTTP_HOST'],
867 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
868 user_agent="%s / %s" % (release.name, release.version),
870 req.session.authenticate(db, login, password, env)
872 return self.session_info(req)
874 @openerpweb.jsonrequest
875 def change_password (self,req,fields):
876 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
877 dict(map(operator.itemgetter('name', 'value'), fields)))
878 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
879 return {'error':'All passwords have to be filled.','title': 'Change Password'}
880 if new_password != confirm_password:
881 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
883 if req.session.model('res.users').change_password(
884 old_password, new_password):
885 return {'new_password':new_password}
887 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
888 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
890 @openerpweb.jsonrequest
891 def sc_list(self, req):
892 return req.session.model('ir.ui.view_sc').get_sc(
893 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
895 @openerpweb.jsonrequest
896 def get_lang_list(self, req):
899 'lang_list': (req.session.proxy("db").list_lang() or []),
903 return {"error": e, "title": "Languages"}
905 @openerpweb.jsonrequest
906 def modules(self, req):
907 # return all installed modules. Web client is smart enough to not load a module twice
908 return module_installed(req)
910 @openerpweb.jsonrequest
911 def eval_domain_and_context(self, req, contexts, domains,
913 """ Evaluates sequences of domains and contexts, composing them into
914 a single context, domain or group_by sequence.
916 :param list contexts: list of contexts to merge together. Contexts are
917 evaluated in sequence, all previous contexts
918 are part of their own evaluation context
919 (starting at the session context).
920 :param list domains: list of domains to merge together. Domains are
921 evaluated in sequence and appended to one another
922 (implicit AND), their evaluation domain is the
923 result of merging all contexts.
924 :param list group_by_seq: list of domains (which may be in a different
925 order than the ``contexts`` parameter),
926 evaluated in sequence, their ``'group_by'``
927 key is extracted if they have one.
932 the global context created by merging all of
936 the concatenation of all domains
939 a list of fields to group by, potentially empty (in which case
940 no group by should be performed)
942 context, domain = eval_context_and_domain(req.session,
943 common.nonliterals.CompoundContext(*(contexts or [])),
944 common.nonliterals.CompoundDomain(*(domains or [])))
946 group_by_sequence = []
947 for candidate in (group_by_seq or []):
948 ctx = req.session.eval_context(candidate, context)
949 group_by = ctx.get('group_by')
952 elif isinstance(group_by, basestring):
953 group_by_sequence.append(group_by)
955 group_by_sequence.extend(group_by)
960 'group_by': group_by_sequence
963 @openerpweb.jsonrequest
964 def save_session_action(self, req, the_action):
966 This method store an action object in the session object and returns an integer
967 identifying that action. The method get_session_action() can be used to get
970 :param the_action: The action to save in the session.
971 :type the_action: anything
972 :return: A key identifying the saved action.
975 saved_actions = req.httpsession.get('saved_actions')
976 if not saved_actions:
977 saved_actions = {"next":0, "actions":{}}
978 req.httpsession['saved_actions'] = saved_actions
979 # we don't allow more than 10 stored actions
980 if len(saved_actions["actions"]) >= 10:
981 del saved_actions["actions"][min(saved_actions["actions"])]
982 key = saved_actions["next"]
983 saved_actions["actions"][key] = the_action
984 saved_actions["next"] = key + 1
987 @openerpweb.jsonrequest
988 def get_session_action(self, req, key):
990 Gets back a previously saved action. This method can return None if the action
991 was saved since too much time (this case should be handled in a smart way).
993 :param key: The key given by save_session_action()
995 :return: The saved action or None.
998 saved_actions = req.httpsession.get('saved_actions')
999 if not saved_actions:
1001 return saved_actions["actions"].get(key)
1003 @openerpweb.jsonrequest
1004 def check(self, req):
1005 req.session.assert_valid()
1008 @openerpweb.jsonrequest
1009 def destroy(self, req):
1010 req.session._suicide = True
1012 class Menu(openerpweb.Controller):
1013 _cp_path = "/web/menu"
1015 @openerpweb.jsonrequest
1016 def load(self, req):
1017 return {'data': self.do_load(req)}
1019 @openerpweb.jsonrequest
1020 def get_user_roots(self, req):
1021 return self.do_get_user_roots(req)
1023 def do_get_user_roots(self, req):
1024 """ Return all root menu ids visible for the session user.
1026 :param req: A request object, with an OpenERP session attribute
1027 :type req: < session -> OpenERPSession >
1028 :return: the root menu ids
1032 context = s.eval_context(req.context)
1033 Menus = s.model('ir.ui.menu')
1034 # If a menu action is defined use its domain to get the root menu items
1035 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1037 menu_domain = [('parent_id', '=', False)]
1039 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1041 menu_domain = ast.literal_eval(domain_string)
1043 return Menus.search(menu_domain, 0, False, False, context)
1045 def do_load(self, req):
1046 """ Loads all menu items (all applications and their sub-menus).
1048 :param req: A request object, with an OpenERP session attribute
1049 :type req: < session -> OpenERPSession >
1050 :return: the menu root
1051 :rtype: dict('children': menu_nodes)
1053 context = req.session.eval_context(req.context)
1054 Menus = req.session.model('ir.ui.menu')
1056 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1057 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1059 # menus are loaded fully unlike a regular tree view, cause there are a
1060 # limited number of items (752 when all 6.1 addons are installed)
1061 menu_ids = Menus.search([], 0, False, False, context)
1062 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1063 # adds roots at the end of the sequence, so that they will overwrite
1064 # equivalent menu items from full menu read when put into id:item
1065 # mapping, resulting in children being correctly set on the roots.
1066 menu_items.extend(menu_roots)
1068 # make a tree using parent_id
1069 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1070 for menu_item in menu_items:
1071 if menu_item['parent_id']:
1072 parent = menu_item['parent_id'][0]
1075 if parent in menu_items_map:
1076 menu_items_map[parent].setdefault(
1077 'children', []).append(menu_item)
1079 # sort by sequence a tree using parent_id
1080 for menu_item in menu_items:
1081 menu_item.setdefault('children', []).sort(
1082 key=operator.itemgetter('sequence'))
1086 @openerpweb.jsonrequest
1087 def action(self, req, menu_id):
1088 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1089 [('ir.ui.menu', menu_id)], False)
1090 return {"action": actions}
1092 class DataSet(openerpweb.Controller):
1093 _cp_path = "/web/dataset"
1095 @openerpweb.jsonrequest
1096 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1097 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1098 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1100 """ Performs a search() followed by a read() (if needed) using the
1101 provided search criteria
1103 :param req: a JSON-RPC request object
1104 :type req: openerpweb.JsonRequest
1105 :param str model: the name of the model to search on
1106 :param fields: a list of the fields to return in the result records
1108 :param int offset: from which index should the results start being returned
1109 :param int limit: the maximum number of records to return
1110 :param list domain: the search domain for the query
1111 :param list sort: sorting directives
1112 :returns: A structure (dict) with two keys: ids (all the ids matching
1113 the (domain, context) pair) and records (paginated records
1114 matching fields selection set)
1117 Model = req.session.model(model)
1119 context, domain = eval_context_and_domain(
1120 req.session, req.context, domain)
1122 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1123 if limit and len(ids) == limit:
1124 length = Model.search_count(domain, context)
1126 length = len(ids) + (offset or 0)
1127 if fields and fields == ['id']:
1128 # shortcut read if we only want the ids
1131 'records': [{'id': id} for id in ids]
1134 records = Model.read(ids, fields or False, context)
1135 records.sort(key=lambda obj: ids.index(obj['id']))
1141 @openerpweb.jsonrequest
1142 def load(self, req, model, id, fields):
1143 m = req.session.model(model)
1145 r = m.read([id], False, req.session.eval_context(req.context))
1148 return {'value': value}
1150 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1151 has_domain = domain_id is not None and domain_id < len(args)
1152 has_context = context_id is not None and context_id < len(args)
1154 domain = args[domain_id] if has_domain else []
1155 context = args[context_id] if has_context else {}
1156 c, d = eval_context_and_domain(req.session, context, domain)
1160 args[context_id] = c
1162 return self._call_kw(req, model, method, args, {})
1164 def _call_kw(self, req, model, method, args, kwargs):
1165 for i in xrange(len(args)):
1166 if isinstance(args[i], common.nonliterals.BaseContext):
1167 args[i] = req.session.eval_context(args[i])
1168 elif isinstance(args[i], common.nonliterals.BaseDomain):
1169 args[i] = req.session.eval_domain(args[i])
1170 for k in kwargs.keys():
1171 if isinstance(kwargs[k], common.nonliterals.BaseContext):
1172 kwargs[k] = req.session.eval_context(kwargs[k])
1173 elif isinstance(kwargs[k], common.nonliterals.BaseDomain):
1174 kwargs[k] = req.session.eval_domain(kwargs[k])
1176 # Temporary implements future display_name special field for model#read()
1177 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1178 if 'display_name' in args[1]:
1179 names = req.session.model(model).name_get(args[0], **kwargs)
1180 args[1].remove('display_name')
1181 r = getattr(req.session.model(model), method)(*args, **kwargs)
1182 for i in range(len(r)):
1183 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1186 return getattr(req.session.model(model), method)(*args, **kwargs)
1188 @openerpweb.jsonrequest
1189 def onchange(self, req, model, method, args, context_id=None):
1190 """ Support method for handling onchange calls: behaves much like call
1191 with the following differences:
1193 * Does not take a domain_id
1194 * Is aware of the return value's structure, and will parse the domains
1195 if needed in order to return either parsed literal domains (in JSON)
1196 or non-literal domain instances, allowing those domains to be used
1200 :type req: web.common.http.JsonRequest
1201 :param str model: object type on which to call the method
1202 :param str method: name of the onchange handler method
1203 :param list args: arguments to call the onchange handler with
1204 :param int context_id: index of the context object in the list of
1206 :return: result of the onchange call with all domains parsed
1208 result = self.call_common(req, model, method, args, context_id=context_id)
1209 if not result or 'domain' not in result:
1212 result['domain'] = dict(
1213 (k, parse_domain(v, req.session))
1214 for k, v in result['domain'].iteritems())
1218 @openerpweb.jsonrequest
1219 def call(self, req, model, method, args, domain_id=None, context_id=None):
1220 return self.call_common(req, model, method, args, domain_id, context_id)
1222 @openerpweb.jsonrequest
1223 def call_kw(self, req, model, method, args, kwargs):
1224 return self._call_kw(req, model, method, args, kwargs)
1226 @openerpweb.jsonrequest
1227 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1228 action = self.call_common(req, model, method, args, domain_id, context_id)
1229 if isinstance(action, dict) and action.get('type') != '':
1230 return clean_action(req, action)
1233 @openerpweb.jsonrequest
1234 def exec_workflow(self, req, model, id, signal):
1235 return req.session.exec_workflow(model, id, signal)
1237 @openerpweb.jsonrequest
1238 def resequence(self, req, model, ids, field='sequence', offset=0):
1239 """ Re-sequences a number of records in the model, by their ids
1241 The re-sequencing starts at the first model of ``ids``, the sequence
1242 number is incremented by one after each record and starts at ``offset``
1244 :param ids: identifiers of the records to resequence, in the new sequence order
1246 :param str field: field used for sequence specification, defaults to
1248 :param int offset: sequence number for first record in ``ids``, allows
1249 starting the resequencing from an arbitrary number,
1252 m = req.session.model(model)
1253 if not m.fields_get([field]):
1255 # python 2.6 has no start parameter
1256 for i, id in enumerate(ids):
1257 m.write(id, { field: i + offset })
1260 class DataGroup(openerpweb.Controller):
1261 _cp_path = "/web/group"
1262 @openerpweb.jsonrequest
1263 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1264 Model = req.session.model(model)
1265 context, domain = eval_context_and_domain(req.session, req.context, domain)
1267 return Model.read_group(
1268 domain or [], fields, group_by_fields, 0, False,
1269 dict(context, group_by=group_by_fields), sort or False)
1271 class View(openerpweb.Controller):
1272 _cp_path = "/web/view"
1274 def fields_view_get(self, req, model, view_id, view_type,
1275 transform=True, toolbar=False, submenu=False):
1276 Model = req.session.model(model)
1277 context = req.session.eval_context(req.context)
1278 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1279 # todo fme?: check that we should pass the evaluated context here
1280 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1281 if toolbar and transform:
1282 self.process_toolbar(req, fvg['toolbar'])
1285 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1286 # depending on how it feels, xmlrpclib.ServerProxy can translate
1287 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1288 # enjoy unicode strings which can not be trivially converted to
1289 # strings, and it blows up during parsing.
1291 # So ensure we fix this retardation by converting view xml back to
1293 if isinstance(fvg['arch'], unicode):
1294 arch = fvg['arch'].encode('utf-8')
1297 fvg['arch_string'] = arch
1300 evaluation_context = session.evaluation_context(context or {})
1301 xml = self.transform_view(arch, session, evaluation_context)
1303 xml = ElementTree.fromstring(arch)
1304 fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
1306 if 'id' in fvg['fields']:
1307 # Special case for id's
1308 id_field = fvg['fields']['id']
1309 id_field['original_type'] = id_field['type']
1310 id_field['type'] = 'id'
1312 for field in fvg['fields'].itervalues():
1313 if field.get('views'):
1314 for view in field["views"].itervalues():
1315 self.process_view(session, view, None, transform)
1316 if field.get('domain'):
1317 field["domain"] = parse_domain(field["domain"], session)
1318 if field.get('context'):
1319 field["context"] = parse_context(field["context"], session)
1321 def process_toolbar(self, req, toolbar):
1323 The toolbar is a mapping of section_key: [action_descriptor]
1325 We need to clean all those actions in order to ensure correct
1328 for actions in toolbar.itervalues():
1329 for action in actions:
1330 if 'context' in action:
1331 action['context'] = parse_context(
1332 action['context'], req.session)
1333 if 'domain' in action:
1334 action['domain'] = parse_domain(
1335 action['domain'], req.session)
1337 @openerpweb.jsonrequest
1338 def add_custom(self, req, view_id, arch):
1339 CustomView = req.session.model('ir.ui.view.custom')
1341 'user_id': req.session._uid,
1344 }, req.session.eval_context(req.context))
1345 return {'result': True}
1347 @openerpweb.jsonrequest
1348 def undo_custom(self, req, view_id, reset=False):
1349 CustomView = req.session.model('ir.ui.view.custom')
1350 context = req.session.eval_context(req.context)
1351 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1352 0, False, False, context)
1355 CustomView.unlink(vcustom, context)
1357 CustomView.unlink([vcustom[0]], context)
1358 return {'result': True}
1359 return {'result': False}
1361 def transform_view(self, view_string, session, context=None):
1362 # transform nodes on the fly via iterparse, instead of
1363 # doing it statically on the parsing result
1364 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1366 for event, elem in parser:
1367 if event == "start":
1370 self.parse_domains_and_contexts(elem, session)
1373 def parse_domains_and_contexts(self, elem, session):
1374 """ Converts domains and contexts from the view into Python objects,
1375 either literals if they can be parsed by literal_eval or a special
1376 placeholder object if the domain or context refers to free variables.
1378 :param elem: the current node being parsed
1379 :type param: xml.etree.ElementTree.Element
1380 :param session: OpenERP session object, used to store and retrieve
1382 :type session: openerpweb.openerpweb.OpenERPSession
1384 for el in ['domain', 'filter_domain']:
1385 domain = elem.get(el, '').strip()
1387 elem.set(el, parse_domain(domain, session))
1388 elem.set(el + '_string', domain)
1389 for el in ['context', 'default_get']:
1390 context_string = elem.get(el, '').strip()
1392 elem.set(el, parse_context(context_string, session))
1393 elem.set(el + '_string', context_string)
1395 @openerpweb.jsonrequest
1396 def load(self, req, model, view_id, view_type, toolbar=False):
1397 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1399 class TreeView(View):
1400 _cp_path = "/web/treeview"
1402 @openerpweb.jsonrequest
1403 def action(self, req, model, id):
1404 return load_actions_from_ir_values(
1405 req,'action', 'tree_but_open',[(model, id)],
1408 class SearchView(View):
1409 _cp_path = "/web/searchview"
1411 @openerpweb.jsonrequest
1412 def load(self, req, model, view_id):
1413 fields_view = self.fields_view_get(req, model, view_id, 'search')
1414 return {'fields_view': fields_view}
1416 @openerpweb.jsonrequest
1417 def fields_get(self, req, model):
1418 Model = req.session.model(model)
1419 fields = Model.fields_get(False, req.session.eval_context(req.context))
1420 for field in fields.values():
1421 # shouldn't convert the views too?
1422 if field.get('domain'):
1423 field["domain"] = parse_domain(field["domain"], req.session)
1424 if field.get('context'):
1425 field["context"] = parse_context(field["context"], req.session)
1426 return {'fields': fields}
1428 @openerpweb.jsonrequest
1429 def get_filters(self, req, model):
1430 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1431 Model = req.session.model("ir.filters")
1432 filters = Model.get_filters(model)
1433 for filter in filters:
1435 parsed_context = parse_context(filter["context"], req.session)
1436 filter["context"] = (parsed_context
1437 if not isinstance(parsed_context, common.nonliterals.BaseContext)
1438 else req.session.eval_context(parsed_context))
1440 parsed_domain = parse_domain(filter["domain"], req.session)
1441 filter["domain"] = (parsed_domain
1442 if not isinstance(parsed_domain, common.nonliterals.BaseDomain)
1443 else req.session.eval_domain(parsed_domain))
1445 logger.exception("Failed to parse custom filter %s in %s",
1446 filter['name'], model)
1447 filter['disabled'] = True
1448 del filter['context']
1449 del filter['domain']
1452 class Binary(openerpweb.Controller):
1453 _cp_path = "/web/binary"
1455 @openerpweb.httprequest
1456 def image(self, req, model, id, field, **kw):
1457 last_update = '__last_update'
1458 Model = req.session.model(model)
1459 context = req.session.eval_context(req.context)
1460 headers = [('Content-Type', 'image/png')]
1461 etag = req.httprequest.headers.get('If-None-Match')
1462 hashed_session = hashlib.md5(req.session_id).hexdigest()
1463 id = None if not id else simplejson.loads(id)
1464 if type(id) is list:
1467 if not id and hashed_session == etag:
1468 return werkzeug.wrappers.Response(status=304)
1470 date = Model.read([id], [last_update], context)[0].get(last_update)
1471 if hashlib.md5(date).hexdigest() == etag:
1472 return werkzeug.wrappers.Response(status=304)
1474 retag = hashed_session
1477 res = Model.default_get([field], context).get(field)
1478 image_data = base64.b64decode(res)
1480 res = Model.read([id], [last_update, field], context)[0]
1481 retag = hashlib.md5(res.get(last_update)).hexdigest()
1482 image_data = base64.b64decode(res.get(field))
1483 except (TypeError, xmlrpclib.Fault):
1484 image_data = self.placeholder(req)
1485 headers.append(('ETag', retag))
1486 headers.append(('Content-Length', len(image_data)))
1488 ncache = int(kw.get('cache'))
1489 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1492 return req.make_response(image_data, headers)
1493 def placeholder(self, req):
1494 addons_path = openerpweb.addons_manifest['web']['addons_path']
1495 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1496 def content_disposition(self, filename, req):
1497 filename = filename.encode('utf8')
1498 escaped = urllib2.quote(filename)
1499 browser = req.httprequest.user_agent.browser
1500 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
1501 if browser == 'msie' and version < 9:
1502 return "attachment; filename=%s" % escaped
1503 elif browser == 'safari':
1504 return "attachment; filename=%s" % filename
1506 return "attachment; filename*=UTF-8''%s" % escaped
1508 @openerpweb.httprequest
1509 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1510 """ Download link for files stored as binary fields.
1512 If the ``id`` parameter is omitted, fetches the default value for the
1513 binary field (via ``default_get``), otherwise fetches the field for
1514 that precise record.
1516 :param req: OpenERP request
1517 :type req: :class:`web.common.http.HttpRequest`
1518 :param str model: name of the model to fetch the binary from
1519 :param str field: binary field
1520 :param str id: id of the record from which to fetch the binary
1521 :param str filename_field: field holding the file's name, if any
1522 :returns: :class:`werkzeug.wrappers.Response`
1524 Model = req.session.model(model)
1525 context = req.session.eval_context(req.context)
1528 fields.append(filename_field)
1530 res = Model.read([int(id)], fields, context)[0]
1532 res = Model.default_get(fields, context)
1533 filecontent = base64.b64decode(res.get(field, ''))
1535 return req.not_found()
1537 filename = '%s_%s' % (model.replace('.', '_'), id)
1539 filename = res.get(filename_field, '') or filename
1540 return req.make_response(filecontent,
1541 [('Content-Type', 'application/octet-stream'),
1542 ('Content-Disposition', self.content_disposition(filename, req))])
1544 @openerpweb.httprequest
1545 def saveas_ajax(self, req, data, token):
1546 jdata = simplejson.loads(data)
1547 model = jdata['model']
1548 field = jdata['field']
1549 id = jdata.get('id', None)
1550 filename_field = jdata.get('filename_field', None)
1551 context = jdata.get('context', dict())
1553 context = req.session.eval_context(context)
1554 Model = req.session.model(model)
1557 fields.append(filename_field)
1559 res = Model.read([int(id)], fields, context)[0]
1561 res = Model.default_get(fields, context)
1562 filecontent = base64.b64decode(res.get(field, ''))
1564 raise ValueError("No content found for field '%s' on '%s:%s'" %
1567 filename = '%s_%s' % (model.replace('.', '_'), id)
1569 filename = res.get(filename_field, '') or filename
1570 return req.make_response(filecontent,
1571 headers=[('Content-Type', 'application/octet-stream'),
1572 ('Content-Disposition', self.content_disposition(filename, req))],
1573 cookies={'fileToken': int(token)})
1575 @openerpweb.httprequest
1576 def upload(self, req, callback, ufile):
1577 # TODO: might be useful to have a configuration flag for max-length file uploads
1579 out = """<script language="javascript" type="text/javascript">
1580 var win = window.top.window;
1581 win.jQuery(win).trigger(%s, %s);
1584 args = [len(data), ufile.filename,
1585 ufile.content_type, base64.b64encode(data)]
1586 except Exception, e:
1587 args = [False, e.message]
1588 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1590 @openerpweb.httprequest
1591 def upload_attachment(self, req, callback, model, id, ufile):
1592 context = req.session.eval_context(req.context)
1593 Model = req.session.model('ir.attachment')
1595 out = """<script language="javascript" type="text/javascript">
1596 var win = window.top.window;
1597 win.jQuery(win).trigger(%s, %s);
1599 attachment_id = Model.create({
1600 'name': ufile.filename,
1601 'datas': base64.encodestring(ufile.read()),
1602 'datas_fname': ufile.filename,
1607 'filename': ufile.filename,
1610 except Exception, e:
1611 args = { 'error': e.message }
1612 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1614 class Action(openerpweb.Controller):
1615 _cp_path = "/web/action"
1617 @openerpweb.jsonrequest
1618 def load(self, req, action_id, do_not_eval=False):
1619 Actions = req.session.model('ir.actions.actions')
1621 context = req.session.eval_context(req.context)
1624 action_id = int(action_id)
1627 module, xmlid = action_id.split('.', 1)
1628 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1629 assert model.startswith('ir.actions.')
1631 action_id = 0 # force failed read
1633 base_action = Actions.read([action_id], ['type'], context)
1636 action_type = base_action[0]['type']
1637 if action_type == 'ir.actions.report.xml':
1638 ctx.update({'bin_size': True})
1640 action = req.session.model(action_type).read([action_id], False, ctx)
1642 value = clean_action(req, action[0], do_not_eval)
1645 @openerpweb.jsonrequest
1646 def run(self, req, action_id):
1647 return_action = req.session.model('ir.actions.server').run(
1648 [action_id], req.session.eval_context(req.context))
1650 return clean_action(req, return_action)
1655 _cp_path = "/web/export"
1657 @openerpweb.jsonrequest
1658 def formats(self, req):
1659 """ Returns all valid export formats
1661 :returns: for each export format, a pair of identifier and printable name
1662 :rtype: [(str, str)]
1666 for path, controller in openerpweb.controllers_path.iteritems()
1667 if path.startswith(self._cp_path)
1668 if hasattr(controller, 'fmt')
1669 ], key=operator.itemgetter("label"))
1671 def fields_get(self, req, model):
1672 Model = req.session.model(model)
1673 fields = Model.fields_get(False, req.session.eval_context(req.context))
1676 @openerpweb.jsonrequest
1677 def get_fields(self, req, model, prefix='', parent_name= '',
1678 import_compat=True, parent_field_type=None,
1681 if import_compat and parent_field_type == "many2one":
1684 fields = self.fields_get(req, model)
1687 fields.pop('id', None)
1689 fields['.id'] = fields.pop('id', {'string': 'ID'})
1691 fields_sequence = sorted(fields.iteritems(),
1692 key=lambda field: field[1].get('string', ''))
1695 for field_name, field in fields_sequence:
1697 if exclude and field_name in exclude:
1699 if field.get('readonly'):
1700 # If none of the field's states unsets readonly, skip the field
1701 if all(dict(attrs).get('readonly', True)
1702 for attrs in field.get('states', {}).values()):
1705 id = prefix + (prefix and '/'or '') + field_name
1706 name = parent_name + (parent_name and '/' or '') + field['string']
1707 record = {'id': id, 'string': name,
1708 'value': id, 'children': False,
1709 'field_type': field.get('type'),
1710 'required': field.get('required'),
1711 'relation_field': field.get('relation_field')}
1712 records.append(record)
1714 if len(name.split('/')) < 3 and 'relation' in field:
1715 ref = field.pop('relation')
1716 record['value'] += '/id'
1717 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1719 if not import_compat or field['type'] == 'one2many':
1720 # m2m field in import_compat is childless
1721 record['children'] = True
1725 @openerpweb.jsonrequest
1726 def namelist(self,req, model, export_id):
1727 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1728 export = req.session.model("ir.exports").read([export_id])[0]
1729 export_fields_list = req.session.model("ir.exports.line").read(
1730 export['export_fields'])
1732 fields_data = self.fields_info(
1733 req, model, map(operator.itemgetter('name'), export_fields_list))
1736 {'name': field['name'], 'label': fields_data[field['name']]}
1737 for field in export_fields_list
1740 def fields_info(self, req, model, export_fields):
1742 fields = self.fields_get(req, model)
1744 # To make fields retrieval more efficient, fetch all sub-fields of a
1745 # given field at the same time. Because the order in the export list is
1746 # arbitrary, this requires ordering all sub-fields of a given field
1747 # together so they can be fetched at the same time
1749 # Works the following way:
1750 # * sort the list of fields to export, the default sorting order will
1751 # put the field itself (if present, for xmlid) and all of its
1752 # sub-fields right after it
1753 # * then, group on: the first field of the path (which is the same for
1754 # a field and for its subfields and the length of splitting on the
1755 # first '/', which basically means grouping the field on one side and
1756 # all of the subfields on the other. This way, we have the field (for
1757 # the xmlid) with length 1, and all of the subfields with the same
1758 # base but a length "flag" of 2
1759 # * if we have a normal field (length 1), just add it to the info
1760 # mapping (with its string) as-is
1761 # * otherwise, recursively call fields_info via graft_subfields.
1762 # all graft_subfields does is take the result of fields_info (on the
1763 # field's model) and prepend the current base (current field), which
1764 # rebuilds the whole sub-tree for the field
1766 # result: because we're not fetching the fields_get for half the
1767 # database models, fetching a namelist with a dozen fields (including
1768 # relational data) falls from ~6s to ~300ms (on the leads model).
1769 # export lists with no sub-fields (e.g. import_compatible lists with
1770 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1771 # there's a single fields_get to execute)
1772 for (base, length), subfields in itertools.groupby(
1773 sorted(export_fields),
1774 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1775 subfields = list(subfields)
1777 # subfields is a seq of $base/*rest, and not loaded yet
1778 info.update(self.graft_subfields(
1779 req, fields[base]['relation'], base, fields[base]['string'],
1783 info[base] = fields[base]['string']
1787 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1788 export_fields = [field.split('/', 1)[1] for field in fields]
1790 (prefix + '/' + k, prefix_string + '/' + v)
1791 for k, v in self.fields_info(req, model, export_fields).iteritems())
1793 #noinspection PyPropertyDefinition
1795 def content_type(self):
1796 """ Provides the format's content type """
1797 raise NotImplementedError()
1799 def filename(self, base):
1800 """ Creates a valid filename for the format (with extension) from the
1801 provided base name (exension-less)
1803 raise NotImplementedError()
1805 def from_data(self, fields, rows):
1806 """ Conversion method from OpenERP's export data to whatever the
1807 current export class outputs
1809 :params list fields: a list of fields to export
1810 :params list rows: a list of records to export
1814 raise NotImplementedError()
1816 @openerpweb.httprequest
1817 def index(self, req, data, token):
1818 model, fields, ids, domain, import_compat = \
1819 operator.itemgetter('model', 'fields', 'ids', 'domain',
1821 simplejson.loads(data))
1823 context = req.session.eval_context(req.context)
1824 Model = req.session.model(model)
1825 ids = ids or Model.search(domain, 0, False, False, context)
1827 field_names = map(operator.itemgetter('name'), fields)
1828 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1831 columns_headers = field_names
1833 columns_headers = [val['label'].strip() for val in fields]
1836 return req.make_response(self.from_data(columns_headers, import_data),
1837 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1838 ('Content-Type', self.content_type)],
1839 cookies={'fileToken': int(token)})
1841 class CSVExport(Export):
1842 _cp_path = '/web/export/csv'
1843 fmt = {'tag': 'csv', 'label': 'CSV'}
1846 def content_type(self):
1847 return 'text/csv;charset=utf8'
1849 def filename(self, base):
1850 return base + '.csv'
1852 def from_data(self, fields, rows):
1854 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1856 writer.writerow([name.encode('utf-8') for name in fields])
1861 if isinstance(d, basestring):
1862 d = d.replace('\n',' ').replace('\t',' ')
1864 d = d.encode('utf-8')
1865 except UnicodeError:
1867 if d is False: d = None
1869 writer.writerow(row)
1876 class ExcelExport(Export):
1877 _cp_path = '/web/export/xls'
1881 'error': None if xlwt else "XLWT required"
1885 def content_type(self):
1886 return 'application/vnd.ms-excel'
1888 def filename(self, base):
1889 return base + '.xls'
1891 def from_data(self, fields, rows):
1892 workbook = xlwt.Workbook()
1893 worksheet = workbook.add_sheet('Sheet 1')
1895 for i, fieldname in enumerate(fields):
1896 worksheet.write(0, i, fieldname)
1897 worksheet.col(i).width = 8000 # around 220 pixels
1899 style = xlwt.easyxf('align: wrap yes')
1901 for row_index, row in enumerate(rows):
1902 for cell_index, cell_value in enumerate(row):
1903 if isinstance(cell_value, basestring):
1904 cell_value = re.sub("\r", " ", cell_value)
1905 if cell_value is False: cell_value = None
1906 worksheet.write(row_index + 1, cell_index, cell_value, style)
1915 class Reports(View):
1916 _cp_path = "/web/report"
1917 POLLING_DELAY = 0.25
1919 'doc': 'application/vnd.ms-word',
1920 'html': 'text/html',
1921 'odt': 'application/vnd.oasis.opendocument.text',
1922 'pdf': 'application/pdf',
1923 'sxw': 'application/vnd.sun.xml.writer',
1924 'xls': 'application/vnd.ms-excel',
1927 @openerpweb.httprequest
1928 def index(self, req, action, token):
1929 action = simplejson.loads(action)
1931 report_srv = req.session.proxy("report")
1932 context = req.session.eval_context(
1933 common.nonliterals.CompoundContext(
1934 req.context or {}, action[ "context"]))
1937 report_ids = context["active_ids"]
1938 if 'report_type' in action:
1939 report_data['report_type'] = action['report_type']
1940 if 'datas' in action:
1941 if 'ids' in action['datas']:
1942 report_ids = action['datas'].pop('ids')
1943 report_data.update(action['datas'])
1945 report_id = report_srv.report(
1946 req.session._db, req.session._uid, req.session._password,
1947 action["report_name"], report_ids,
1948 report_data, context)
1950 report_struct = None
1952 report_struct = report_srv.report_get(
1953 req.session._db, req.session._uid, req.session._password, report_id)
1954 if report_struct["state"]:
1957 time.sleep(self.POLLING_DELAY)
1959 report = base64.b64decode(report_struct['result'])
1960 if report_struct.get('code') == 'zlib':
1961 report = zlib.decompress(report)
1962 report_mimetype = self.TYPES_MAPPING.get(
1963 report_struct['format'], 'octet-stream')
1964 file_name = action.get('name', 'report')
1965 if 'name' not in action:
1966 reports = req.session.model('ir.actions.report.xml')
1967 res_id = reports.search([('report_name', '=', action['report_name']),],
1968 0, False, False, context)
1970 file_name = reports.read(res_id[0], ['name'], context)['name']
1972 file_name = action['report_name']
1974 return req.make_response(report,
1976 # maybe we should take of what characters can appear in a file name?
1977 ('Content-Disposition', 'attachment; filename="%s.%s"' % (file_name, report_struct['format'])),
1978 ('Content-Type', report_mimetype),
1979 ('Content-Length', len(report))],
1980 cookies={'fileToken': int(token)})
1983 _cp_path = "/web/import"
1985 def fields_get(self, req, model):
1986 Model = req.session.model(model)
1987 fields = Model.fields_get(False, req.session.eval_context(req.context))
1990 @openerpweb.httprequest
1991 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1993 data = list(csv.reader(
1994 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1995 except csv.Error, e:
1997 return '<script>window.top.%s(%s);</script>' % (
1998 jsonp, simplejson.dumps({'error': {
1999 'message': 'Error parsing CSV file: %s' % e,
2000 # decodes each byte to a unicode character, which may or
2001 # may not be printable, but decoding will succeed.
2002 # Otherwise simplejson will try to decode the `str` using
2003 # utf-8, which is very likely to blow up on characters out
2004 # of the ascii range (in range [128, 256))
2005 'preview': csvfile.read(200).decode('iso-8859-1')}}))
2008 return '<script>window.top.%s(%s);</script>' % (
2009 jsonp, simplejson.dumps(
2010 {'records': data[:10]}, encoding=csvcode))
2011 except UnicodeDecodeError:
2012 return '<script>window.top.%s(%s);</script>' % (
2013 jsonp, simplejson.dumps({
2014 'message': u"Failed to decode CSV file using encoding %s, "
2015 u"try switching to a different encoding" % csvcode
2018 @openerpweb.httprequest
2019 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
2021 modle_obj = req.session.model(model)
2022 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
2023 simplejson.loads(meta))
2026 if not (csvdel and len(csvdel) == 1):
2027 error = u"The CSV delimiter must be a single character"
2029 if not indices and fields:
2030 error = u"You must select at least one field to import"
2033 return '<script>window.top.%s(%s);</script>' % (
2034 jsonp, simplejson.dumps({'error': {'message': error}}))
2036 # skip ignored records (@skip parameter)
2037 # then skip empty lines (not valid csv)
2038 # nb: should these operations be reverted?
2039 rows_to_import = itertools.ifilter(
2042 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
2045 # if only one index, itemgetter will return an atom rather than a tuple
2046 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
2047 else: mapper = operator.itemgetter(*indices)
2052 # decode each data row
2054 [record.decode(csvcode) for record in row]
2055 for row in itertools.imap(mapper, rows_to_import)
2056 # don't insert completely empty rows (can happen due to fields
2057 # filtering in case of e.g. o2m content rows)
2060 except UnicodeDecodeError:
2061 error = u"Failed to decode CSV file using encoding %s" % csvcode
2062 except csv.Error, e:
2063 error = u"Could not process CSV file: %s" % e
2065 # If the file contains nothing,
2067 error = u"File to import is empty"
2069 return '<script>window.top.%s(%s);</script>' % (
2070 jsonp, simplejson.dumps({'error': {'message': error}}))
2073 (code, record, message, _nope) = modle_obj.import_data(
2074 fields, data, 'init', '', False,
2075 req.session.eval_context(req.context))
2076 except xmlrpclib.Fault, e:
2077 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
2078 return '<script>window.top.%s(%s);</script>' % (
2079 jsonp, simplejson.dumps({'error':error}))
2082 return '<script>window.top.%s(%s);</script>' % (
2083 jsonp, simplejson.dumps({'success':True}))
2085 msg = u"Error during import: %s\n\nTrying to import record %r" % (
2087 return '<script>window.top.%s(%s);</script>' % (
2088 jsonp, simplejson.dumps({'error': {'message':msg}}))
2090 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: