1 # -*- coding: utf-8 -*-
19 from xml.etree import ElementTree
20 from cStringIO import StringIO
22 import babel.messages.pofile
24 import werkzeug.wrappers
33 from .. import nonliterals
36 #----------------------------------------------------------
38 #----------------------------------------------------------
41 """ Minify js with a clever regex.
42 Taken from http://opensource.perlig.de/rjsmin
43 Apache License, Version 2.0 """
45 """ Substitution callback """
46 groups = match.groups()
52 (groups[4] and '\n') or
53 (groups[5] and ' ') or
54 (groups[6] and ' ') or
55 (groups[7] and ' ') or
60 r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
61 r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
62 r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
63 r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
64 r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
65 r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
66 r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
67 r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
68 r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
69 r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
70 r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
71 r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
72 r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
73 r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
74 r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
75 r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
76 r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
77 r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
78 r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
79 r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
80 r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
85 # Validated by diff -u of sass2scss against:
86 # sass-convert -F sass -T scss openerp.sass openerp.scss
89 reComment = re.compile(r'//.*$')
90 reIndent = re.compile(r'^\s+')
91 reIgnore = re.compile(r'^\s*(//.*)?$')
92 reFixes = { re.compile(r'\(\((.*)\)\)') : r'(\1)', }
95 for l in src.split('\n'):
97 if reIgnore.search(l): continue
98 l = reComment.sub('', l)
100 indent = reIndent.match(l)
101 level = indent.end() if indent else 0
104 prevBlocks[lastLevel] = block
106 block[-1] = (block[-1], newBlock)
108 elif level<lastLevel:
109 block = prevBlocks[level]
113 for ereg, repl in reFixes.items():
114 l = ereg.sub(repl if type(repl)==str else repl(), l)
117 def write(sass, level=-1):
120 if type(sass)==tuple:
122 out += indent+sass[0]+" {\n"
124 out += write(e, level+1)
126 out = out.rstrip(" \n")
131 out += indent+sass+";\n"
137 proxy = req.session.proxy("db")
139 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
141 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
142 dbs = [i for i in dbs if re.match(r, i)]
145 def module_topological_sort(modules):
146 """ Return a list of module names sorted so that their dependencies of the
147 modules are listed before the module itself
149 modules is a dict of {module_name: dependencies}
151 :param modules: modules to sort
156 dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
157 # incoming edge: dependency on other module (if a depends on b, a has an
158 # incoming edge from b, aka there's an edge from b to a)
159 # outgoing edge: other module depending on this one
161 # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
162 #L ← Empty list that will contain the sorted nodes
164 #S ← Set of all nodes with no outgoing edges (modules on which no other
166 S = set(module for module in modules if module not in dependencies)
169 #function visit(node n)
171 #if n has not been visited yet then
175 #change: n not web module, can not be resolved, ignore
176 if n not in modules: return
177 #for each node m with an edge from m to n do (dependencies of n)
183 #for each node n in S do
189 def module_installed(req):
190 # Candidates module the current heuristic is the /static dir
191 loadable = openerpweb.addons_manifest.keys()
194 # Retrieve database installed modules
195 # TODO The following code should move to ir.module.module.list_installed_modules()
196 Modules = req.session.model('ir.module.module')
197 domain = [('state','=','installed'), ('name','in', loadable)]
198 for module in Modules.search_read(domain, ['name', 'dependencies_id']):
199 modules[module['name']] = []
200 deps = module.get('dependencies_id')
202 deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
203 dependencies = [i['name'] for i in deps_read]
204 modules[module['name']] = dependencies
206 sorted_modules = module_topological_sort(modules)
207 return sorted_modules
209 def module_installed_bypass_session(dbname):
210 loadable = openerpweb.addons_manifest.keys()
213 import openerp.modules.registry
214 registry = openerp.modules.registry.RegistryManager.get(dbname)
215 with registry.cursor() as cr:
216 m = registry.get('ir.module.module')
217 # TODO The following code should move to ir.module.module.list_installed_modules()
218 domain = [('state','=','installed'), ('name','in', loadable)]
219 ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
220 for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
221 modules[module['name']] = []
222 deps = module.get('dependencies_id')
224 deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
225 dependencies = [i['name'] for i in deps_read]
226 modules[module['name']] = dependencies
229 sorted_modules = module_topological_sort(modules)
230 return sorted_modules
232 def module_boot(req):
233 server_wide_modules = openerp.conf.server_wide_modules or ['web']
234 return [m for m in server_wide_modules if m in openerpweb.addons_manifest]
235 # TODO the following will be enabled once we separate the module code and translation loading
238 for i in server_wide_modules:
239 if i in openerpweb.addons_manifest:
241 # if only one db load every module at boot
245 except xmlrpclib.Fault:
246 # ignore access denied
249 dbside = module_installed_bypass_session(dbs[0])
250 dbside = [i for i in dbside if i not in serverside]
251 addons = serverside + dbside
254 def concat_xml(file_list):
255 """Concatenate xml files
257 :param list(str) file_list: list of files to check
258 :returns: (concatenation_result, checksum)
261 checksum = hashlib.new('sha1')
263 return '', checksum.hexdigest()
266 for fname in file_list:
267 with open(fname, 'rb') as fp:
269 checksum.update(contents)
271 xml = ElementTree.parse(fp).getroot()
274 root = ElementTree.Element(xml.tag)
275 #elif root.tag != xml.tag:
276 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
278 for child in xml.getchildren():
280 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
282 def concat_files(file_list, reader=None, intersperse=""):
283 """ Concatenates contents of all provided files
285 :param list(str) file_list: list of files to check
286 :param function reader: reading procedure for each file
287 :param str intersperse: string to intersperse between file contents
288 :returns: (concatenation_result, checksum)
291 checksum = hashlib.new('sha1')
293 return '', checksum.hexdigest()
297 with open(f, 'rb') as fp:
301 for fname in file_list:
302 contents = reader(fname)
303 checksum.update(contents)
304 files_content.append(contents)
306 files_concat = intersperse.join(files_content)
307 return files_concat, checksum.hexdigest()
309 def concat_js(file_list):
310 content, checksum = concat_files(file_list, intersperse=';')
311 content = rjsmin(content)
312 return content, checksum
314 def manifest_glob(req, addons, key):
316 addons = module_boot(req)
318 addons = addons.split(',')
321 manifest = openerpweb.addons_manifest.get(addon, None)
324 # ensure does not ends with /
325 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
326 globlist = manifest.get(key, [])
327 for pattern in globlist:
328 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
329 r.append((path, path[len(addons_path):]))
332 def manifest_list(req, mods, extension):
334 path = '/web/webclient/' + extension
336 path += '?mods=' + mods
338 files = manifest_glob(req, mods, extension)
339 i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
340 req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
342 return [wp for _fp, wp in files]
344 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
346 def get_last_modified(files):
347 """ Returns the modification time of the most recently modified
350 :param list(str) files: names of files to check
351 :return: most recent modification time amongst the fileset
352 :rtype: datetime.datetime
356 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
358 return datetime.datetime(1970, 1, 1)
360 def make_conditional(req, response, last_modified=None, etag=None):
361 """ Makes the provided response conditional based upon the request,
362 and mandates revalidation from clients
364 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
365 setting ``last_modified`` and ``etag`` correctly on the response object
367 :param req: OpenERP request
368 :type req: web.common.http.WebRequest
369 :param response: Werkzeug response
370 :type response: werkzeug.wrappers.Response
371 :param datetime.datetime last_modified: last modification date of the response content
372 :param str etag: some sort of checksum of the content (deep etag)
373 :return: the response object provided
374 :rtype: werkzeug.wrappers.Response
376 response.cache_control.must_revalidate = True
377 response.cache_control.max_age = 0
379 response.last_modified = last_modified
381 response.set_etag(etag)
382 return response.make_conditional(req.httprequest)
384 def login_and_redirect(req, db, login, key, redirect_url='/'):
385 req.session.authenticate(db, login, key, {})
386 return set_cookie_and_redirect(req, redirect_url)
388 def set_cookie_and_redirect(req, redirect_url):
389 redirect = werkzeug.utils.redirect(redirect_url, 303)
390 redirect.autocorrect_location_header = False
391 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
392 redirect.set_cookie('instance0|session_id', cookie_val)
395 def eval_context_and_domain(session, context, domain=None):
396 e_context = session.eval_context(context)
397 # should we give the evaluated context as an evaluation context to the domain?
398 e_domain = session.eval_domain(domain or [])
400 return e_context, e_domain
402 def load_actions_from_ir_values(req, key, key2, models, meta):
403 context = req.session.eval_context(req.context)
404 Values = req.session.model('ir.values')
405 actions = Values.get(key, key2, models, meta, context)
407 return [(id, name, clean_action(req, action))
408 for id, name, action in actions]
410 def clean_action(req, action, do_not_eval=False):
411 action.setdefault('flags', {})
413 context = req.session.eval_context(req.context)
414 eval_ctx = req.session.evaluation_context(context)
417 # values come from the server, we can just eval them
418 if action.get('context') and isinstance(action.get('context'), basestring):
419 action['context'] = eval( action['context'], eval_ctx ) or {}
421 if action.get('domain') and isinstance(action.get('domain'), basestring):
422 action['domain'] = eval( action['domain'], eval_ctx ) or []
424 if 'context' in action:
425 action['context'] = parse_context(action['context'], req.session)
426 if 'domain' in action:
427 action['domain'] = parse_domain(action['domain'], req.session)
429 action_type = action.setdefault('type', 'ir.actions.act_window_close')
430 if action_type == 'ir.actions.act_window':
431 return fix_view_modes(action)
434 # I think generate_views,fix_view_modes should go into js ActionManager
435 def generate_views(action):
437 While the server generates a sequence called "views" computing dependencies
438 between a bunch of stuff for views coming directly from the database
439 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
440 to return custom view dictionaries generated on the fly.
442 In that case, there is no ``views`` key available on the action.
444 Since the web client relies on ``action['views']``, generate it here from
445 ``view_mode`` and ``view_id``.
447 Currently handles two different cases:
449 * no view_id, multiple view_mode
450 * single view_id, single view_mode
452 :param dict action: action descriptor dictionary to generate a views key for
454 view_id = action.get('view_id') or False
455 if isinstance(view_id, (list, tuple)):
458 # providing at least one view mode is a requirement, not an option
459 view_modes = action['view_mode'].split(',')
461 if len(view_modes) > 1:
463 raise ValueError('Non-db action dictionaries should provide '
464 'either multiple view modes or a single view '
465 'mode and an optional view id.\n\n Got view '
466 'modes %r and view id %r for action %r' % (
467 view_modes, view_id, action))
468 action['views'] = [(False, mode) for mode in view_modes]
470 action['views'] = [(view_id, view_modes[0])]
472 def fix_view_modes(action):
473 """ For historical reasons, OpenERP has weird dealings in relation to
474 view_mode and the view_type attribute (on window actions):
476 * one of the view modes is ``tree``, which stands for both list views
478 * the choice is made by checking ``view_type``, which is either
479 ``form`` for a list view or ``tree`` for an actual tree view
481 This methods simply folds the view_type into view_mode by adding a
482 new view mode ``list`` which is the result of the ``tree`` view_mode
483 in conjunction with the ``form`` view_type.
485 TODO: this should go into the doc, some kind of "peculiarities" section
487 :param dict action: an action descriptor
488 :returns: nothing, the action is modified in place
490 if not action.get('views'):
491 generate_views(action)
493 if action.pop('view_type', 'form') != 'form':
496 if 'view_mode' in action:
497 action['view_mode'] = ','.join(
498 mode if mode != 'tree' else 'list'
499 for mode in action['view_mode'].split(','))
501 [id, mode if mode != 'tree' else 'list']
502 for id, mode in action['views']
507 def parse_domain(domain, session):
508 """ Parses an arbitrary string containing a domain, transforms it
509 to either a literal domain or a :class:`nonliterals.Domain`
511 :param domain: the domain to parse, if the domain is not a string it
512 is assumed to be a literal domain and is returned as-is
513 :param session: Current OpenERP session
514 :type session: openerpweb.OpenERPSession
516 if not isinstance(domain, basestring):
519 return ast.literal_eval(domain)
522 return nonliterals.Domain(session, domain)
524 def parse_context(context, session):
525 """ Parses an arbitrary string containing a context, transforms it
526 to either a literal context or a :class:`nonliterals.Context`
528 :param context: the context to parse, if the context is not a string it
529 is assumed to be a literal domain and is returned as-is
530 :param session: Current OpenERP session
531 :type session: openerpweb.OpenERPSession
533 if not isinstance(context, basestring):
536 return ast.literal_eval(context)
538 return nonliterals.Context(session, context)
540 def _local_web_translations(trans_file):
543 with open(trans_file) as t_file:
544 po = babel.messages.pofile.read_po(t_file)
548 if x.id and x.string and "openerp-web" in x.auto_comments:
549 messages.append({'id': x.id, 'string': x.string})
552 def from_elementtree(el, preserve_whitespaces=False):
554 Simple and straightforward XML-to-JSON converter in Python
556 http://code.google.com/p/xml2json-direct/
560 ns, name = el.tag.rsplit("}", 1)
562 res["namespace"] = ns[1:]
566 for k, v in el.items():
569 if el.text and (preserve_whitespaces or el.text.strip() != ''):
572 kids.append(from_elementtree(kid, preserve_whitespaces))
573 if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
574 kids.append(kid.tail)
575 res["children"] = kids
579 def content_disposition(filename, req):
580 filename = filename.encode('utf8')
581 escaped = urllib2.quote(filename)
582 browser = req.httprequest.user_agent.browser
583 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
584 if browser == 'msie' and version < 9:
585 return "attachment; filename=%s" % escaped
586 elif browser == 'safari':
587 return "attachment; filename=%s" % filename
589 return "attachment; filename*=UTF-8''%s" % escaped
592 #----------------------------------------------------------
593 # OpenERP Web web Controllers
594 #----------------------------------------------------------
596 html_template = """<!DOCTYPE html>
597 <html style="height: 100%%">
599 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
600 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
601 <title>OpenERP</title>
602 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
603 <link rel="stylesheet" href="/web/static/src/css/full.css" />
606 <script type="text/javascript">
608 var s = new openerp.init(%(modules)s);
615 <script type="text/javascript"
616 src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
618 var test = function() {
623 if (window.localStorage && false) {
624 if (! localStorage.getItem("hasShownGFramePopup")) {
626 localStorage.setItem("hasShownGFramePopup", true);
637 class Home(openerpweb.Controller):
640 @openerpweb.httprequest
641 def index(self, req, s_action=None, **kw):
642 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, None, 'js'))
643 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, None, 'css'))
645 r = html_template % {
648 'modules': simplejson.dumps(module_boot(req)),
649 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
653 @openerpweb.httprequest
654 def login(self, req, db, login, key):
655 return login_and_redirect(req, db, login, key)
657 class WebClient(openerpweb.Controller):
658 _cp_path = "/web/webclient"
660 @openerpweb.jsonrequest
661 def csslist(self, req, mods=None):
662 return manifest_list(req, mods, 'css')
664 @openerpweb.jsonrequest
665 def jslist(self, req, mods=None):
666 return manifest_list(req, mods, 'js')
668 @openerpweb.jsonrequest
669 def qweblist(self, req, mods=None):
670 return manifest_list(req, mods, 'qweb')
672 @openerpweb.httprequest
673 def css(self, req, mods=None):
674 files = list(manifest_glob(req, mods, 'css'))
675 last_modified = get_last_modified(f[0] for f in files)
676 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
677 return werkzeug.wrappers.Response(status=304)
679 file_map = dict(files)
681 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
682 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
685 """read the a css file and absolutify all relative uris"""
686 with open(f, 'rb') as fp:
687 data = fp.read().decode('utf-8')
690 # convert FS path into web path
691 web_dir = '/'.join(os.path.dirname(path).split(os.path.sep))
695 r"""@import \1%s/""" % (web_dir,),
701 r"""url(\1%s/""" % (web_dir,),
704 return data.encode('utf-8')
706 content, checksum = concat_files((f[0] for f in files), reader)
708 return make_conditional(
709 req, req.make_response(content, [('Content-Type', 'text/css')]),
710 last_modified, checksum)
712 @openerpweb.httprequest
713 def js(self, req, mods=None):
714 files = [f[0] for f in manifest_glob(req, mods, 'js')]
715 last_modified = get_last_modified(files)
716 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
717 return werkzeug.wrappers.Response(status=304)
719 content, checksum = concat_js(files)
721 return make_conditional(
722 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
723 last_modified, checksum)
725 @openerpweb.httprequest
726 def qweb(self, req, mods=None):
727 files = [f[0] for f in manifest_glob(req, mods, 'qweb')]
728 last_modified = get_last_modified(files)
729 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
730 return werkzeug.wrappers.Response(status=304)
732 content, checksum = concat_xml(files)
734 return make_conditional(
735 req, req.make_response(content, [('Content-Type', 'text/xml')]),
736 last_modified, checksum)
738 @openerpweb.jsonrequest
739 def bootstrap_translations(self, req, mods):
740 """ Load local translations from *.po files, as a temporary solution
741 until we have established a valid session. This is meant only
742 for translating the login page and db management chrome, using
743 the browser's language. """
744 lang = req.httprequest.accept_languages.best or 'en'
745 # For performance reasons we only load a single translation, so for
746 # sub-languages (that should only be partially translated) we load the
747 # main language PO instead - that should be enough for the login screen.
748 if '-' in lang: # RFC2616 uses '-' separators for sublanguages
749 lang = lang.split('-')[0]
751 translations_per_module = {}
752 for addon_name in mods:
753 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
754 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
755 if not os.path.exists(f_name):
757 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
759 return {"modules": translations_per_module,
760 "lang_parameters": None}
762 @openerpweb.jsonrequest
763 def translations(self, req, mods, lang):
764 res_lang = req.session.model('res.lang')
765 ids = res_lang.search([("code", "=", lang)])
768 lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
769 "grouping", "decimal_point", "thousands_sep"])
771 # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
772 # done server-side when the language is loaded, so we only need to load the user's lang.
773 ir_translation = req.session.model('ir.translation')
774 translations_per_module = {}
775 messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
776 ('comments','like','openerp-web'),('value','!=',False),
778 ['module','src','value','lang'], order='module')
779 for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
780 translations_per_module.setdefault(mod,{'messages':[]})
781 translations_per_module[mod]['messages'].extend({'id': m['src'],
782 'string': m['value']} \
784 return {"modules": translations_per_module,
785 "lang_parameters": lang_params}
787 @openerpweb.jsonrequest
788 def version_info(self, req):
790 "version": openerp.release.version
793 class Proxy(openerpweb.Controller):
794 _cp_path = '/web/proxy'
796 @openerpweb.jsonrequest
797 def load(self, req, path):
798 """ Proxies an HTTP request through a JSON request.
800 It is strongly recommended to not request binary files through this,
801 as the result will be a binary data blob as well.
803 :param req: OpenERP request
804 :param path: actual request path
805 :return: file content
807 from werkzeug.test import Client
808 from werkzeug.wrappers import BaseResponse
810 return Client(req.httprequest.app, BaseResponse).get(path).data
812 class Database(openerpweb.Controller):
813 _cp_path = "/web/database"
815 @openerpweb.jsonrequest
816 def get_list(self, req):
818 return {"db_list": dbs}
820 @openerpweb.jsonrequest
821 def create(self, req, fields):
822 params = dict(map(operator.itemgetter('name', 'value'), fields))
824 params['super_admin_pwd'],
826 bool(params.get('demo_data')),
828 params['create_admin_pwd']
831 return req.session.proxy("db").create_database(*create_attrs)
833 @openerpweb.jsonrequest
834 def drop(self, req, fields):
835 password, db = operator.itemgetter(
836 'drop_pwd', 'drop_db')(
837 dict(map(operator.itemgetter('name', 'value'), fields)))
840 return req.session.proxy("db").drop(password, db)
841 except xmlrpclib.Fault, e:
842 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
843 return {'error': e.faultCode, 'title': 'Drop Database'}
844 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
846 @openerpweb.httprequest
847 def backup(self, req, backup_db, backup_pwd, token):
849 db_dump = base64.b64decode(
850 req.session.proxy("db").dump(backup_pwd, backup_db))
851 filename = "%(db)s_%(timestamp)s.dump" % {
853 'timestamp': datetime.datetime.utcnow().strftime(
854 "%Y-%m-%d_%H-%M-%SZ")
856 return req.make_response(db_dump,
857 [('Content-Type', 'application/octet-stream; charset=binary'),
858 ('Content-Disposition', content_disposition(filename, req))],
859 {'fileToken': int(token)}
861 except xmlrpclib.Fault, e:
862 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
864 @openerpweb.httprequest
865 def restore(self, req, db_file, restore_pwd, new_db):
867 data = base64.b64encode(db_file.read())
868 req.session.proxy("db").restore(restore_pwd, new_db, data)
870 except xmlrpclib.Fault, e:
871 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
872 raise Exception("AccessDenied")
874 @openerpweb.jsonrequest
875 def change_password(self, req, fields):
876 old_password, new_password = operator.itemgetter(
877 'old_pwd', 'new_pwd')(
878 dict(map(operator.itemgetter('name', 'value'), fields)))
880 return req.session.proxy("db").change_admin_password(old_password, new_password)
881 except xmlrpclib.Fault, e:
882 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
883 return {'error': e.faultCode, 'title': 'Change Password'}
884 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
886 class Session(openerpweb.Controller):
887 _cp_path = "/web/session"
889 def session_info(self, req):
890 req.session.ensure_valid()
892 "session_id": req.session_id,
893 "uid": req.session._uid,
894 "context": req.session.get_context() if req.session._uid else {},
895 "db": req.session._db,
896 "login": req.session._login,
899 @openerpweb.jsonrequest
900 def get_session_info(self, req):
901 return self.session_info(req)
903 @openerpweb.jsonrequest
904 def authenticate(self, req, db, login, password, base_location=None):
905 wsgienv = req.httprequest.environ
907 base_location=base_location,
908 HTTP_HOST=wsgienv['HTTP_HOST'],
909 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
911 req.session.authenticate(db, login, password, env)
913 return self.session_info(req)
915 @openerpweb.jsonrequest
916 def change_password (self,req,fields):
917 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
918 dict(map(operator.itemgetter('name', 'value'), fields)))
919 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
920 return {'error':'All passwords have to be filled.','title': 'Change Password'}
921 if new_password != confirm_password:
922 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
924 if req.session.model('res.users').change_password(
925 old_password, new_password):
926 return {'new_password':new_password}
928 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
929 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
931 @openerpweb.jsonrequest
932 def sc_list(self, req):
933 return req.session.model('ir.ui.view_sc').get_sc(
934 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
936 @openerpweb.jsonrequest
937 def get_lang_list(self, req):
940 'lang_list': (req.session.proxy("db").list_lang() or []),
944 return {"error": e, "title": "Languages"}
946 @openerpweb.jsonrequest
947 def modules(self, req):
948 # return all installed modules. Web client is smart enough to not load a module twice
949 return module_installed(req)
951 @openerpweb.jsonrequest
952 def eval_domain_and_context(self, req, contexts, domains,
954 """ Evaluates sequences of domains and contexts, composing them into
955 a single context, domain or group_by sequence.
957 :param list contexts: list of contexts to merge together. Contexts are
958 evaluated in sequence, all previous contexts
959 are part of their own evaluation context
960 (starting at the session context).
961 :param list domains: list of domains to merge together. Domains are
962 evaluated in sequence and appended to one another
963 (implicit AND), their evaluation domain is the
964 result of merging all contexts.
965 :param list group_by_seq: list of domains (which may be in a different
966 order than the ``contexts`` parameter),
967 evaluated in sequence, their ``'group_by'``
968 key is extracted if they have one.
973 the global context created by merging all of
977 the concatenation of all domains
980 a list of fields to group by, potentially empty (in which case
981 no group by should be performed)
983 context, domain = eval_context_and_domain(req.session,
984 nonliterals.CompoundContext(*(contexts or [])),
985 nonliterals.CompoundDomain(*(domains or [])))
987 group_by_sequence = []
988 for candidate in (group_by_seq or []):
989 ctx = req.session.eval_context(candidate, context)
990 group_by = ctx.get('group_by')
993 elif isinstance(group_by, basestring):
994 group_by_sequence.append(group_by)
996 group_by_sequence.extend(group_by)
1001 'group_by': group_by_sequence
1004 @openerpweb.jsonrequest
1005 def save_session_action(self, req, the_action):
1007 This method store an action object in the session object and returns an integer
1008 identifying that action. The method get_session_action() can be used to get
1011 :param the_action: The action to save in the session.
1012 :type the_action: anything
1013 :return: A key identifying the saved action.
1016 saved_actions = req.httpsession.get('saved_actions')
1017 if not saved_actions:
1018 saved_actions = {"next":0, "actions":{}}
1019 req.httpsession['saved_actions'] = saved_actions
1020 # we don't allow more than 10 stored actions
1021 if len(saved_actions["actions"]) >= 10:
1022 del saved_actions["actions"][min(saved_actions["actions"])]
1023 key = saved_actions["next"]
1024 saved_actions["actions"][key] = the_action
1025 saved_actions["next"] = key + 1
1028 @openerpweb.jsonrequest
1029 def get_session_action(self, req, key):
1031 Gets back a previously saved action. This method can return None if the action
1032 was saved since too much time (this case should be handled in a smart way).
1034 :param key: The key given by save_session_action()
1036 :return: The saved action or None.
1039 saved_actions = req.httpsession.get('saved_actions')
1040 if not saved_actions:
1042 return saved_actions["actions"].get(key)
1044 @openerpweb.jsonrequest
1045 def check(self, req):
1046 req.session.assert_valid()
1049 @openerpweb.jsonrequest
1050 def destroy(self, req):
1051 req.session._suicide = True
1053 class Menu(openerpweb.Controller):
1054 _cp_path = "/web/menu"
1056 @openerpweb.jsonrequest
1057 def load(self, req):
1058 return {'data': self.do_load(req)}
1060 @openerpweb.jsonrequest
1061 def get_user_roots(self, req):
1062 return self.do_get_user_roots(req)
1064 def do_get_user_roots(self, req):
1065 """ Return all root menu ids visible for the session user.
1067 :param req: A request object, with an OpenERP session attribute
1068 :type req: < session -> OpenERPSession >
1069 :return: the root menu ids
1073 context = s.eval_context(req.context)
1074 Menus = s.model('ir.ui.menu')
1075 # If a menu action is defined use its domain to get the root menu items
1076 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1078 menu_domain = [('parent_id', '=', False)]
1080 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1082 menu_domain = ast.literal_eval(domain_string)
1084 return Menus.search(menu_domain, 0, False, False, context)
1086 def do_load(self, req):
1087 """ Loads all menu items (all applications and their sub-menus).
1089 :param req: A request object, with an OpenERP session attribute
1090 :type req: < session -> OpenERPSession >
1091 :return: the menu root
1092 :rtype: dict('children': menu_nodes)
1094 context = req.session.eval_context(req.context)
1095 Menus = req.session.model('ir.ui.menu')
1097 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1098 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1100 # menus are loaded fully unlike a regular tree view, cause there are a
1101 # limited number of items (752 when all 6.1 addons are installed)
1102 menu_ids = Menus.search([], 0, False, False, context)
1103 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1104 # adds roots at the end of the sequence, so that they will overwrite
1105 # equivalent menu items from full menu read when put into id:item
1106 # mapping, resulting in children being correctly set on the roots.
1107 menu_items.extend(menu_roots)
1109 # make a tree using parent_id
1110 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1111 for menu_item in menu_items:
1112 if menu_item['parent_id']:
1113 parent = menu_item['parent_id'][0]
1116 if parent in menu_items_map:
1117 menu_items_map[parent].setdefault(
1118 'children', []).append(menu_item)
1120 # sort by sequence a tree using parent_id
1121 for menu_item in menu_items:
1122 menu_item.setdefault('children', []).sort(
1123 key=operator.itemgetter('sequence'))
1127 @openerpweb.jsonrequest
1128 def action(self, req, menu_id):
1129 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1130 [('ir.ui.menu', menu_id)], False)
1131 return {"action": actions}
1133 class DataSet(openerpweb.Controller):
1134 _cp_path = "/web/dataset"
1136 @openerpweb.jsonrequest
1137 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1138 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1139 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1141 """ Performs a search() followed by a read() (if needed) using the
1142 provided search criteria
1144 :param req: a JSON-RPC request object
1145 :type req: openerpweb.JsonRequest
1146 :param str model: the name of the model to search on
1147 :param fields: a list of the fields to return in the result records
1149 :param int offset: from which index should the results start being returned
1150 :param int limit: the maximum number of records to return
1151 :param list domain: the search domain for the query
1152 :param list sort: sorting directives
1153 :returns: A structure (dict) with two keys: ids (all the ids matching
1154 the (domain, context) pair) and records (paginated records
1155 matching fields selection set)
1158 Model = req.session.model(model)
1160 context, domain = eval_context_and_domain(
1161 req.session, req.context, domain)
1163 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1164 if limit and len(ids) == limit:
1165 length = Model.search_count(domain, context)
1167 length = len(ids) + (offset or 0)
1168 if fields and fields == ['id']:
1169 # shortcut read if we only want the ids
1172 'records': [{'id': id} for id in ids]
1175 records = Model.read(ids, fields or False, context)
1176 records.sort(key=lambda obj: ids.index(obj['id']))
1182 @openerpweb.jsonrequest
1183 def load(self, req, model, id, fields):
1184 m = req.session.model(model)
1186 r = m.read([id], False, req.session.eval_context(req.context))
1189 return {'value': value}
1191 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1192 has_domain = domain_id is not None and domain_id < len(args)
1193 has_context = context_id is not None and context_id < len(args)
1195 domain = args[domain_id] if has_domain else []
1196 context = args[context_id] if has_context else {}
1197 c, d = eval_context_and_domain(req.session, context, domain)
1201 args[context_id] = c
1203 return self._call_kw(req, model, method, args, {})
1205 def _call_kw(self, req, model, method, args, kwargs):
1206 for i in xrange(len(args)):
1207 if isinstance(args[i], nonliterals.BaseContext):
1208 args[i] = req.session.eval_context(args[i])
1209 elif isinstance(args[i], nonliterals.BaseDomain):
1210 args[i] = req.session.eval_domain(args[i])
1211 for k in kwargs.keys():
1212 if isinstance(kwargs[k], nonliterals.BaseContext):
1213 kwargs[k] = req.session.eval_context(kwargs[k])
1214 elif isinstance(kwargs[k], nonliterals.BaseDomain):
1215 kwargs[k] = req.session.eval_domain(kwargs[k])
1217 # Temporary implements future display_name special field for model#read()
1218 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1219 if 'display_name' in args[1]:
1220 names = req.session.model(model).name_get(args[0], **kwargs)
1221 args[1].remove('display_name')
1222 r = getattr(req.session.model(model), method)(*args, **kwargs)
1223 for i in range(len(r)):
1224 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1227 return getattr(req.session.model(model), method)(*args, **kwargs)
1229 @openerpweb.jsonrequest
1230 def onchange(self, req, model, method, args, context_id=None):
1231 """ Support method for handling onchange calls: behaves much like call
1232 with the following differences:
1234 * Does not take a domain_id
1235 * Is aware of the return value's structure, and will parse the domains
1236 if needed in order to return either parsed literal domains (in JSON)
1237 or non-literal domain instances, allowing those domains to be used
1241 :type req: web.common.http.JsonRequest
1242 :param str model: object type on which to call the method
1243 :param str method: name of the onchange handler method
1244 :param list args: arguments to call the onchange handler with
1245 :param int context_id: index of the context object in the list of
1247 :return: result of the onchange call with all domains parsed
1249 result = self.call_common(req, model, method, args, context_id=context_id)
1250 if not result or 'domain' not in result:
1253 result['domain'] = dict(
1254 (k, parse_domain(v, req.session))
1255 for k, v in result['domain'].iteritems())
1259 @openerpweb.jsonrequest
1260 def call(self, req, model, method, args, domain_id=None, context_id=None):
1261 return self.call_common(req, model, method, args, domain_id, context_id)
1263 @openerpweb.jsonrequest
1264 def call_kw(self, req, model, method, args, kwargs):
1265 return self._call_kw(req, model, method, args, kwargs)
1267 @openerpweb.jsonrequest
1268 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1269 action = self.call_common(req, model, method, args, domain_id, context_id)
1270 if isinstance(action, dict) and action.get('type') != '':
1271 return clean_action(req, action)
1274 @openerpweb.jsonrequest
1275 def exec_workflow(self, req, model, id, signal):
1276 return req.session.exec_workflow(model, id, signal)
1278 @openerpweb.jsonrequest
1279 def resequence(self, req, model, ids, field='sequence', offset=0):
1280 """ Re-sequences a number of records in the model, by their ids
1282 The re-sequencing starts at the first model of ``ids``, the sequence
1283 number is incremented by one after each record and starts at ``offset``
1285 :param ids: identifiers of the records to resequence, in the new sequence order
1287 :param str field: field used for sequence specification, defaults to
1289 :param int offset: sequence number for first record in ``ids``, allows
1290 starting the resequencing from an arbitrary number,
1293 m = req.session.model(model)
1294 if not m.fields_get([field]):
1296 # python 2.6 has no start parameter
1297 for i, id in enumerate(ids):
1298 m.write(id, { field: i + offset })
1301 class DataGroup(openerpweb.Controller):
1302 _cp_path = "/web/group"
1303 @openerpweb.jsonrequest
1304 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1305 Model = req.session.model(model)
1306 context, domain = eval_context_and_domain(req.session, req.context, domain)
1308 return Model.read_group(
1309 domain or [], fields, group_by_fields, 0, False,
1310 dict(context, group_by=group_by_fields), sort or False)
1312 class View(openerpweb.Controller):
1313 _cp_path = "/web/view"
1315 def fields_view_get(self, req, model, view_id, view_type,
1316 transform=True, toolbar=False, submenu=False):
1317 Model = req.session.model(model)
1318 context = req.session.eval_context(req.context)
1319 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1320 # todo fme?: check that we should pass the evaluated context here
1321 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1322 if toolbar and transform:
1323 self.process_toolbar(req, fvg['toolbar'])
1326 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1327 # depending on how it feels, xmlrpclib.ServerProxy can translate
1328 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1329 # enjoy unicode strings which can not be trivially converted to
1330 # strings, and it blows up during parsing.
1332 # So ensure we fix this retardation by converting view xml back to
1334 if isinstance(fvg['arch'], unicode):
1335 arch = fvg['arch'].encode('utf-8')
1338 fvg['arch_string'] = arch
1341 evaluation_context = session.evaluation_context(context or {})
1342 xml = self.transform_view(arch, session, evaluation_context)
1344 xml = ElementTree.fromstring(arch)
1345 fvg['arch'] = from_elementtree(xml, preserve_whitespaces)
1347 if 'id' in fvg['fields']:
1348 # Special case for id's
1349 id_field = fvg['fields']['id']
1350 id_field['original_type'] = id_field['type']
1351 id_field['type'] = 'id'
1353 for field in fvg['fields'].itervalues():
1354 if field.get('views'):
1355 for view in field["views"].itervalues():
1356 self.process_view(session, view, None, transform)
1357 if field.get('domain'):
1358 field["domain"] = parse_domain(field["domain"], session)
1359 if field.get('context'):
1360 field["context"] = parse_context(field["context"], session)
1362 def process_toolbar(self, req, toolbar):
1364 The toolbar is a mapping of section_key: [action_descriptor]
1366 We need to clean all those actions in order to ensure correct
1369 for actions in toolbar.itervalues():
1370 for action in actions:
1371 if 'context' in action:
1372 action['context'] = parse_context(
1373 action['context'], req.session)
1374 if 'domain' in action:
1375 action['domain'] = parse_domain(
1376 action['domain'], req.session)
1378 @openerpweb.jsonrequest
1379 def add_custom(self, req, view_id, arch):
1380 CustomView = req.session.model('ir.ui.view.custom')
1382 'user_id': req.session._uid,
1385 }, req.session.eval_context(req.context))
1386 return {'result': True}
1388 @openerpweb.jsonrequest
1389 def undo_custom(self, req, view_id, reset=False):
1390 CustomView = req.session.model('ir.ui.view.custom')
1391 context = req.session.eval_context(req.context)
1392 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1393 0, False, False, context)
1396 CustomView.unlink(vcustom, context)
1398 CustomView.unlink([vcustom[0]], context)
1399 return {'result': True}
1400 return {'result': False}
1402 def transform_view(self, view_string, session, context=None):
1403 # transform nodes on the fly via iterparse, instead of
1404 # doing it statically on the parsing result
1405 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1407 for event, elem in parser:
1408 if event == "start":
1411 self.parse_domains_and_contexts(elem, session)
1414 def parse_domains_and_contexts(self, elem, session):
1415 """ Converts domains and contexts from the view into Python objects,
1416 either literals if they can be parsed by literal_eval or a special
1417 placeholder object if the domain or context refers to free variables.
1419 :param elem: the current node being parsed
1420 :type param: xml.etree.ElementTree.Element
1421 :param session: OpenERP session object, used to store and retrieve
1423 :type session: openerpweb.openerpweb.OpenERPSession
1425 for el in ['domain', 'filter_domain']:
1426 domain = elem.get(el, '').strip()
1428 elem.set(el, parse_domain(domain, session))
1429 elem.set(el + '_string', domain)
1430 for el in ['context', 'default_get']:
1431 context_string = elem.get(el, '').strip()
1433 elem.set(el, parse_context(context_string, session))
1434 elem.set(el + '_string', context_string)
1436 @openerpweb.jsonrequest
1437 def load(self, req, model, view_id, view_type, toolbar=False):
1438 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1440 class TreeView(View):
1441 _cp_path = "/web/treeview"
1443 @openerpweb.jsonrequest
1444 def action(self, req, model, id):
1445 return load_actions_from_ir_values(
1446 req,'action', 'tree_but_open',[(model, id)],
1449 class SearchView(View):
1450 _cp_path = "/web/searchview"
1452 @openerpweb.jsonrequest
1453 def load(self, req, model, view_id):
1454 fields_view = self.fields_view_get(req, model, view_id, 'search')
1455 return {'fields_view': fields_view}
1457 @openerpweb.jsonrequest
1458 def fields_get(self, req, model):
1459 Model = req.session.model(model)
1460 fields = Model.fields_get(False, req.session.eval_context(req.context))
1461 for field in fields.values():
1462 # shouldn't convert the views too?
1463 if field.get('domain'):
1464 field["domain"] = parse_domain(field["domain"], req.session)
1465 if field.get('context'):
1466 field["context"] = parse_context(field["context"], req.session)
1467 return {'fields': fields}
1469 @openerpweb.jsonrequest
1470 def get_filters(self, req, model):
1471 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1472 Model = req.session.model("ir.filters")
1473 filters = Model.get_filters(model)
1474 for filter in filters:
1476 parsed_context = parse_context(filter["context"], req.session)
1477 filter["context"] = (parsed_context
1478 if not isinstance(parsed_context, nonliterals.BaseContext)
1479 else req.session.eval_context(parsed_context))
1481 parsed_domain = parse_domain(filter["domain"], req.session)
1482 filter["domain"] = (parsed_domain
1483 if not isinstance(parsed_domain, nonliterals.BaseDomain)
1484 else req.session.eval_domain(parsed_domain))
1486 logger.exception("Failed to parse custom filter %s in %s",
1487 filter['name'], model)
1488 filter['disabled'] = True
1489 del filter['context']
1490 del filter['domain']
1493 class Binary(openerpweb.Controller):
1494 _cp_path = "/web/binary"
1496 @openerpweb.httprequest
1497 def image(self, req, model, id, field, **kw):
1498 last_update = '__last_update'
1499 Model = req.session.model(model)
1500 context = req.session.eval_context(req.context)
1501 headers = [('Content-Type', 'image/png')]
1502 etag = req.httprequest.headers.get('If-None-Match')
1503 hashed_session = hashlib.md5(req.session_id).hexdigest()
1504 id = None if not id else simplejson.loads(id)
1505 if type(id) is list:
1508 if not id and hashed_session == etag:
1509 return werkzeug.wrappers.Response(status=304)
1511 date = Model.read([id], [last_update], context)[0].get(last_update)
1512 if hashlib.md5(date).hexdigest() == etag:
1513 return werkzeug.wrappers.Response(status=304)
1515 retag = hashed_session
1518 res = Model.default_get([field], context).get(field)
1519 image_data = base64.b64decode(res)
1521 res = Model.read([id], [last_update, field], context)[0]
1522 retag = hashlib.md5(res.get(last_update)).hexdigest()
1523 image_data = base64.b64decode(res.get(field))
1524 except (TypeError, xmlrpclib.Fault):
1525 image_data = self.placeholder(req)
1526 headers.append(('ETag', retag))
1527 headers.append(('Content-Length', len(image_data)))
1529 ncache = int(kw.get('cache'))
1530 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1533 return req.make_response(image_data, headers)
1534 def placeholder(self, req):
1535 addons_path = openerpweb.addons_manifest['web']['addons_path']
1536 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1538 @openerpweb.httprequest
1539 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1540 """ Download link for files stored as binary fields.
1542 If the ``id`` parameter is omitted, fetches the default value for the
1543 binary field (via ``default_get``), otherwise fetches the field for
1544 that precise record.
1546 :param req: OpenERP request
1547 :type req: :class:`web.common.http.HttpRequest`
1548 :param str model: name of the model to fetch the binary from
1549 :param str field: binary field
1550 :param str id: id of the record from which to fetch the binary
1551 :param str filename_field: field holding the file's name, if any
1552 :returns: :class:`werkzeug.wrappers.Response`
1554 Model = req.session.model(model)
1555 context = req.session.eval_context(req.context)
1558 fields.append(filename_field)
1560 res = Model.read([int(id)], fields, context)[0]
1562 res = Model.default_get(fields, context)
1563 filecontent = base64.b64decode(res.get(field, ''))
1565 return req.not_found()
1567 filename = '%s_%s' % (model.replace('.', '_'), id)
1569 filename = res.get(filename_field, '') or filename
1570 return req.make_response(filecontent,
1571 [('Content-Type', 'application/octet-stream'),
1572 ('Content-Disposition', content_disposition(filename, req))])
1574 @openerpweb.httprequest
1575 def saveas_ajax(self, req, data, token):
1576 jdata = simplejson.loads(data)
1577 model = jdata['model']
1578 field = jdata['field']
1579 id = jdata.get('id', None)
1580 filename_field = jdata.get('filename_field', None)
1581 context = jdata.get('context', dict())
1583 context = req.session.eval_context(context)
1584 Model = req.session.model(model)
1587 fields.append(filename_field)
1589 res = Model.read([int(id)], fields, context)[0]
1591 res = Model.default_get(fields, context)
1592 filecontent = base64.b64decode(res.get(field, ''))
1594 raise ValueError("No content found for field '%s' on '%s:%s'" %
1597 filename = '%s_%s' % (model.replace('.', '_'), id)
1599 filename = res.get(filename_field, '') or filename
1600 return req.make_response(filecontent,
1601 headers=[('Content-Type', 'application/octet-stream'),
1602 ('Content-Disposition', content_disposition(filename, req))],
1603 cookies={'fileToken': int(token)})
1605 @openerpweb.httprequest
1606 def upload(self, req, callback, ufile):
1607 # TODO: might be useful to have a configuration flag for max-length file uploads
1609 out = """<script language="javascript" type="text/javascript">
1610 var win = window.top.window;
1611 win.jQuery(win).trigger(%s, %s);
1614 args = [len(data), ufile.filename,
1615 ufile.content_type, base64.b64encode(data)]
1616 except Exception, e:
1617 args = [False, e.message]
1618 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1620 @openerpweb.httprequest
1621 def upload_attachment(self, req, callback, model, id, ufile):
1622 context = req.session.eval_context(req.context)
1623 Model = req.session.model('ir.attachment')
1625 out = """<script language="javascript" type="text/javascript">
1626 var win = window.top.window;
1627 win.jQuery(win).trigger(%s, %s);
1629 attachment_id = Model.create({
1630 'name': ufile.filename,
1631 'datas': base64.encodestring(ufile.read()),
1632 'datas_fname': ufile.filename,
1637 'filename': ufile.filename,
1640 except Exception, e:
1641 args = { 'error': e.message }
1642 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1644 class Action(openerpweb.Controller):
1645 _cp_path = "/web/action"
1647 @openerpweb.jsonrequest
1648 def load(self, req, action_id, do_not_eval=False):
1649 Actions = req.session.model('ir.actions.actions')
1651 context = req.session.eval_context(req.context)
1654 action_id = int(action_id)
1657 module, xmlid = action_id.split('.', 1)
1658 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1659 assert model.startswith('ir.actions.')
1661 action_id = 0 # force failed read
1663 base_action = Actions.read([action_id], ['type'], context)
1666 action_type = base_action[0]['type']
1667 if action_type == 'ir.actions.report.xml':
1668 ctx.update({'bin_size': True})
1670 action = req.session.model(action_type).read([action_id], False, ctx)
1672 value = clean_action(req, action[0], do_not_eval)
1675 @openerpweb.jsonrequest
1676 def run(self, req, action_id):
1677 return_action = req.session.model('ir.actions.server').run(
1678 [action_id], req.session.eval_context(req.context))
1680 return clean_action(req, return_action)
1685 _cp_path = "/web/export"
1687 @openerpweb.jsonrequest
1688 def formats(self, req):
1689 """ Returns all valid export formats
1691 :returns: for each export format, a pair of identifier and printable name
1692 :rtype: [(str, str)]
1696 for path, controller in openerpweb.controllers_path.iteritems()
1697 if path.startswith(self._cp_path)
1698 if hasattr(controller, 'fmt')
1699 ], key=operator.itemgetter("label"))
1701 def fields_get(self, req, model):
1702 Model = req.session.model(model)
1703 fields = Model.fields_get(False, req.session.eval_context(req.context))
1706 @openerpweb.jsonrequest
1707 def get_fields(self, req, model, prefix='', parent_name= '',
1708 import_compat=True, parent_field_type=None,
1711 if import_compat and parent_field_type == "many2one":
1714 fields = self.fields_get(req, model)
1717 fields.pop('id', None)
1719 fields['.id'] = fields.pop('id', {'string': 'ID'})
1721 fields_sequence = sorted(fields.iteritems(),
1722 key=lambda field: field[1].get('string', ''))
1725 for field_name, field in fields_sequence:
1727 if exclude and field_name in exclude:
1729 if field.get('readonly'):
1730 # If none of the field's states unsets readonly, skip the field
1731 if all(dict(attrs).get('readonly', True)
1732 for attrs in field.get('states', {}).values()):
1735 id = prefix + (prefix and '/'or '') + field_name
1736 name = parent_name + (parent_name and '/' or '') + field['string']
1737 record = {'id': id, 'string': name,
1738 'value': id, 'children': False,
1739 'field_type': field.get('type'),
1740 'required': field.get('required'),
1741 'relation_field': field.get('relation_field')}
1742 records.append(record)
1744 if len(name.split('/')) < 3 and 'relation' in field:
1745 ref = field.pop('relation')
1746 record['value'] += '/id'
1747 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1749 if not import_compat or field['type'] == 'one2many':
1750 # m2m field in import_compat is childless
1751 record['children'] = True
1755 @openerpweb.jsonrequest
1756 def namelist(self,req, model, export_id):
1757 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1758 export = req.session.model("ir.exports").read([export_id])[0]
1759 export_fields_list = req.session.model("ir.exports.line").read(
1760 export['export_fields'])
1762 fields_data = self.fields_info(
1763 req, model, map(operator.itemgetter('name'), export_fields_list))
1766 {'name': field['name'], 'label': fields_data[field['name']]}
1767 for field in export_fields_list
1770 def fields_info(self, req, model, export_fields):
1772 fields = self.fields_get(req, model)
1774 # To make fields retrieval more efficient, fetch all sub-fields of a
1775 # given field at the same time. Because the order in the export list is
1776 # arbitrary, this requires ordering all sub-fields of a given field
1777 # together so they can be fetched at the same time
1779 # Works the following way:
1780 # * sort the list of fields to export, the default sorting order will
1781 # put the field itself (if present, for xmlid) and all of its
1782 # sub-fields right after it
1783 # * then, group on: the first field of the path (which is the same for
1784 # a field and for its subfields and the length of splitting on the
1785 # first '/', which basically means grouping the field on one side and
1786 # all of the subfields on the other. This way, we have the field (for
1787 # the xmlid) with length 1, and all of the subfields with the same
1788 # base but a length "flag" of 2
1789 # * if we have a normal field (length 1), just add it to the info
1790 # mapping (with its string) as-is
1791 # * otherwise, recursively call fields_info via graft_subfields.
1792 # all graft_subfields does is take the result of fields_info (on the
1793 # field's model) and prepend the current base (current field), which
1794 # rebuilds the whole sub-tree for the field
1796 # result: because we're not fetching the fields_get for half the
1797 # database models, fetching a namelist with a dozen fields (including
1798 # relational data) falls from ~6s to ~300ms (on the leads model).
1799 # export lists with no sub-fields (e.g. import_compatible lists with
1800 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1801 # there's a single fields_get to execute)
1802 for (base, length), subfields in itertools.groupby(
1803 sorted(export_fields),
1804 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1805 subfields = list(subfields)
1807 # subfields is a seq of $base/*rest, and not loaded yet
1808 info.update(self.graft_subfields(
1809 req, fields[base]['relation'], base, fields[base]['string'],
1813 info[base] = fields[base]['string']
1817 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1818 export_fields = [field.split('/', 1)[1] for field in fields]
1820 (prefix + '/' + k, prefix_string + '/' + v)
1821 for k, v in self.fields_info(req, model, export_fields).iteritems())
1823 #noinspection PyPropertyDefinition
1825 def content_type(self):
1826 """ Provides the format's content type """
1827 raise NotImplementedError()
1829 def filename(self, base):
1830 """ Creates a valid filename for the format (with extension) from the
1831 provided base name (exension-less)
1833 raise NotImplementedError()
1835 def from_data(self, fields, rows):
1836 """ Conversion method from OpenERP's export data to whatever the
1837 current export class outputs
1839 :params list fields: a list of fields to export
1840 :params list rows: a list of records to export
1844 raise NotImplementedError()
1846 @openerpweb.httprequest
1847 def index(self, req, data, token):
1848 model, fields, ids, domain, import_compat = \
1849 operator.itemgetter('model', 'fields', 'ids', 'domain',
1851 simplejson.loads(data))
1853 context = req.session.eval_context(req.context)
1854 Model = req.session.model(model)
1855 ids = ids or Model.search(domain, 0, False, False, context)
1857 field_names = map(operator.itemgetter('name'), fields)
1858 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1861 columns_headers = field_names
1863 columns_headers = [val['label'].strip() for val in fields]
1866 return req.make_response(self.from_data(columns_headers, import_data),
1867 headers=[('Content-Disposition',
1868 content_disposition(self.filename(model), req)),
1869 ('Content-Type', self.content_type)],
1870 cookies={'fileToken': int(token)})
1872 class CSVExport(Export):
1873 _cp_path = '/web/export/csv'
1874 fmt = {'tag': 'csv', 'label': 'CSV'}
1877 def content_type(self):
1878 return 'text/csv;charset=utf8'
1880 def filename(self, base):
1881 return base + '.csv'
1883 def from_data(self, fields, rows):
1885 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1887 writer.writerow([name.encode('utf-8') for name in fields])
1892 if isinstance(d, basestring):
1893 d = d.replace('\n',' ').replace('\t',' ')
1895 d = d.encode('utf-8')
1896 except UnicodeError:
1898 if d is False: d = None
1900 writer.writerow(row)
1907 class ExcelExport(Export):
1908 _cp_path = '/web/export/xls'
1912 'error': None if xlwt else "XLWT required"
1916 def content_type(self):
1917 return 'application/vnd.ms-excel'
1919 def filename(self, base):
1920 return base + '.xls'
1922 def from_data(self, fields, rows):
1923 workbook = xlwt.Workbook()
1924 worksheet = workbook.add_sheet('Sheet 1')
1926 for i, fieldname in enumerate(fields):
1927 worksheet.write(0, i, fieldname)
1928 worksheet.col(i).width = 8000 # around 220 pixels
1930 style = xlwt.easyxf('align: wrap yes')
1932 for row_index, row in enumerate(rows):
1933 for cell_index, cell_value in enumerate(row):
1934 if isinstance(cell_value, basestring):
1935 cell_value = re.sub("\r", " ", cell_value)
1936 if cell_value is False: cell_value = None
1937 worksheet.write(row_index + 1, cell_index, cell_value, style)
1946 class Reports(View):
1947 _cp_path = "/web/report"
1948 POLLING_DELAY = 0.25
1950 'doc': 'application/vnd.ms-word',
1951 'html': 'text/html',
1952 'odt': 'application/vnd.oasis.opendocument.text',
1953 'pdf': 'application/pdf',
1954 'sxw': 'application/vnd.sun.xml.writer',
1955 'xls': 'application/vnd.ms-excel',
1958 @openerpweb.httprequest
1959 def index(self, req, action, token):
1960 action = simplejson.loads(action)
1962 report_srv = req.session.proxy("report")
1963 context = req.session.eval_context(
1964 nonliterals.CompoundContext(
1965 req.context or {}, action[ "context"]))
1968 report_ids = context["active_ids"]
1969 if 'report_type' in action:
1970 report_data['report_type'] = action['report_type']
1971 if 'datas' in action:
1972 if 'ids' in action['datas']:
1973 report_ids = action['datas'].pop('ids')
1974 report_data.update(action['datas'])
1976 report_id = report_srv.report(
1977 req.session._db, req.session._uid, req.session._password,
1978 action["report_name"], report_ids,
1979 report_data, context)
1981 report_struct = None
1983 report_struct = report_srv.report_get(
1984 req.session._db, req.session._uid, req.session._password, report_id)
1985 if report_struct["state"]:
1988 time.sleep(self.POLLING_DELAY)
1990 report = base64.b64decode(report_struct['result'])
1991 if report_struct.get('code') == 'zlib':
1992 report = zlib.decompress(report)
1993 report_mimetype = self.TYPES_MAPPING.get(
1994 report_struct['format'], 'octet-stream')
1995 file_name = action.get('name', 'report')
1996 if 'name' not in action:
1997 reports = req.session.model('ir.actions.report.xml')
1998 res_id = reports.search([('report_name', '=', action['report_name']),],
1999 0, False, False, context)
2001 file_name = reports.read(res_id[0], ['name'], context)['name']
2003 file_name = action['report_name']
2004 file_name = '%s.%s' % (file_name, report_struct['format'])
2006 return req.make_response(report,
2008 ('Content-Disposition', content_disposition(file_name, req)),
2009 ('Content-Type', report_mimetype),
2010 ('Content-Length', len(report))],
2011 cookies={'fileToken': int(token)})
2014 _cp_path = "/web/import"
2016 def fields_get(self, req, model):
2017 Model = req.session.model(model)
2018 fields = Model.fields_get(False, req.session.eval_context(req.context))
2021 @openerpweb.httprequest
2022 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
2024 data = list(csv.reader(
2025 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
2026 except csv.Error, e:
2028 return '<script>window.top.%s(%s);</script>' % (
2029 jsonp, simplejson.dumps({'error': {
2030 'message': 'Error parsing CSV file: %s' % e,
2031 # decodes each byte to a unicode character, which may or
2032 # may not be printable, but decoding will succeed.
2033 # Otherwise simplejson will try to decode the `str` using
2034 # utf-8, which is very likely to blow up on characters out
2035 # of the ascii range (in range [128, 256))
2036 'preview': csvfile.read(200).decode('iso-8859-1')}}))
2039 return '<script>window.top.%s(%s);</script>' % (
2040 jsonp, simplejson.dumps(
2041 {'records': data[:10]}, encoding=csvcode))
2042 except UnicodeDecodeError:
2043 return '<script>window.top.%s(%s);</script>' % (
2044 jsonp, simplejson.dumps({
2045 'message': u"Failed to decode CSV file using encoding %s, "
2046 u"try switching to a different encoding" % csvcode
2049 @openerpweb.httprequest
2050 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
2052 modle_obj = req.session.model(model)
2053 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
2054 simplejson.loads(meta))
2057 if not (csvdel and len(csvdel) == 1):
2058 error = u"The CSV delimiter must be a single character"
2060 if not indices and fields:
2061 error = u"You must select at least one field to import"
2064 return '<script>window.top.%s(%s);</script>' % (
2065 jsonp, simplejson.dumps({'error': {'message': error}}))
2067 # skip ignored records (@skip parameter)
2068 # then skip empty lines (not valid csv)
2069 # nb: should these operations be reverted?
2070 rows_to_import = itertools.ifilter(
2073 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
2076 # if only one index, itemgetter will return an atom rather than a tuple
2077 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
2078 else: mapper = operator.itemgetter(*indices)
2083 # decode each data row
2085 [record.decode(csvcode) for record in row]
2086 for row in itertools.imap(mapper, rows_to_import)
2087 # don't insert completely empty rows (can happen due to fields
2088 # filtering in case of e.g. o2m content rows)
2091 except UnicodeDecodeError:
2092 error = u"Failed to decode CSV file using encoding %s" % csvcode
2093 except csv.Error, e:
2094 error = u"Could not process CSV file: %s" % e
2096 # If the file contains nothing,
2098 error = u"File to import is empty"
2100 return '<script>window.top.%s(%s);</script>' % (
2101 jsonp, simplejson.dumps({'error': {'message': error}}))
2104 (code, record, message, _nope) = modle_obj.import_data(
2105 fields, data, 'init', '', False,
2106 req.session.eval_context(req.context))
2107 except xmlrpclib.Fault, e:
2108 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
2109 return '<script>window.top.%s(%s);</script>' % (
2110 jsonp, simplejson.dumps({'error':error}))
2113 return '<script>window.top.%s(%s);</script>' % (
2114 jsonp, simplejson.dumps({'success':True}))
2116 msg = u"Error during import: %s\n\nTrying to import record %r" % (
2118 return '<script>window.top.%s(%s);</script>' % (
2119 jsonp, simplejson.dumps({'error': {'message':msg}}))
2121 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: