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
86 proxy = req.session.proxy("db")
88 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
90 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
91 dbs = [i for i in dbs if re.match(r, i)]
95 # if only one db is listed returns it else return False
100 except xmlrpclib.Fault:
101 # ignore access denied
105 def module_topological_sort(modules):
106 """ Return a list of module names sorted so that their dependencies of the
107 modules are listed before the module itself
109 modules is a dict of {module_name: dependencies}
111 :param modules: modules to sort
116 dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
117 # incoming edge: dependency on other module (if a depends on b, a has an
118 # incoming edge from b, aka there's an edge from b to a)
119 # outgoing edge: other module depending on this one
121 # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
122 #L ← Empty list that will contain the sorted nodes
124 #S ← Set of all nodes with no outgoing edges (modules on which no other
126 S = set(module for module in modules if module not in dependencies)
129 #function visit(node n)
131 #if n has not been visited yet then
135 #change: n not web module, can not be resolved, ignore
136 if n not in modules: return
137 #for each node m with an edge from m to n do (dependencies of n)
143 #for each node n in S do
149 def module_installed(req):
150 # Candidates module the current heuristic is the /static dir
151 loadable = openerpweb.addons_manifest.keys()
154 # Retrieve database installed modules
155 # TODO The following code should move to ir.module.module.list_installed_modules()
156 Modules = req.session.model('ir.module.module')
157 domain = [('state','=','installed'), ('name','in', loadable)]
158 for module in Modules.search_read(domain, ['name', 'dependencies_id']):
159 modules[module['name']] = []
160 deps = module.get('dependencies_id')
162 deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
163 dependencies = [i['name'] for i in deps_read]
164 modules[module['name']] = dependencies
166 sorted_modules = module_topological_sort(modules)
167 return sorted_modules
169 def module_installed_bypass_session(dbname):
170 loadable = openerpweb.addons_manifest.keys()
173 import openerp.modules.registry
174 registry = openerp.modules.registry.RegistryManager.get(dbname)
175 with registry.cursor() as cr:
176 m = registry.get('ir.module.module')
177 # TODO The following code should move to ir.module.module.list_installed_modules()
178 domain = [('state','=','installed'), ('name','in', loadable)]
179 ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
180 for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
181 modules[module['name']] = []
182 deps = module.get('dependencies_id')
184 deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
185 dependencies = [i['name'] for i in deps_read]
186 modules[module['name']] = dependencies
189 sorted_modules = module_topological_sort(modules)
190 return sorted_modules
192 def module_boot(req):
193 server_wide_modules = openerp.conf.server_wide_modules or ['web']
196 for i in server_wide_modules:
197 if i in openerpweb.addons_manifest:
199 monodb = db_monodb(req)
201 dbside = module_installed_bypass_session(monodb)
202 dbside = [i for i in dbside if i not in serverside]
203 addons = serverside + dbside
206 def concat_xml(file_list):
207 """Concatenate xml files
209 :param list(str) file_list: list of files to check
210 :returns: (concatenation_result, checksum)
213 checksum = hashlib.new('sha1')
215 return '', checksum.hexdigest()
218 for fname in file_list:
219 with open(fname, 'rb') as fp:
221 checksum.update(contents)
223 xml = ElementTree.parse(fp).getroot()
226 root = ElementTree.Element(xml.tag)
227 #elif root.tag != xml.tag:
228 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
230 for child in xml.getchildren():
232 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
234 def concat_files(file_list, reader=None, intersperse=""):
235 """ Concatenates contents of all provided files
237 :param list(str) file_list: list of files to check
238 :param function reader: reading procedure for each file
239 :param str intersperse: string to intersperse between file contents
240 :returns: (concatenation_result, checksum)
243 checksum = hashlib.new('sha1')
245 return '', checksum.hexdigest()
249 with open(f, 'rb') as fp:
253 for fname in file_list:
254 contents = reader(fname)
255 checksum.update(contents)
256 files_content.append(contents)
258 files_concat = intersperse.join(files_content)
259 return files_concat, checksum.hexdigest()
263 def concat_js(file_list):
264 content, checksum = concat_files(file_list, intersperse=';')
265 if checksum in concat_js_cache:
266 content = concat_js_cache[checksum]
268 content = rjsmin(content)
269 concat_js_cache[checksum] = content
270 return content, checksum
273 """convert FS path into web path"""
274 return '/'.join(path.split(os.path.sep))
276 def manifest_glob(req, addons, key):
278 addons = module_boot(req)
280 addons = addons.split(',')
283 manifest = openerpweb.addons_manifest.get(addon, None)
286 # ensure does not ends with /
287 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
288 globlist = manifest.get(key, [])
289 for pattern in globlist:
290 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
291 r.append((path, fs2web(path[len(addons_path):])))
294 def manifest_list(req, mods, extension):
296 path = '/web/webclient/' + extension
298 path += '?mods=' + mods
300 files = manifest_glob(req, mods, extension)
301 i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
302 req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
304 return [wp for _fp, wp in files]
306 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
308 def get_last_modified(files):
309 """ Returns the modification time of the most recently modified
312 :param list(str) files: names of files to check
313 :return: most recent modification time amongst the fileset
314 :rtype: datetime.datetime
318 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
320 return datetime.datetime(1970, 1, 1)
322 def make_conditional(req, response, last_modified=None, etag=None):
323 """ Makes the provided response conditional based upon the request,
324 and mandates revalidation from clients
326 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
327 setting ``last_modified`` and ``etag`` correctly on the response object
329 :param req: OpenERP request
330 :type req: web.common.http.WebRequest
331 :param response: Werkzeug response
332 :type response: werkzeug.wrappers.Response
333 :param datetime.datetime last_modified: last modification date of the response content
334 :param str etag: some sort of checksum of the content (deep etag)
335 :return: the response object provided
336 :rtype: werkzeug.wrappers.Response
338 response.cache_control.must_revalidate = True
339 response.cache_control.max_age = 0
341 response.last_modified = last_modified
343 response.set_etag(etag)
344 return response.make_conditional(req.httprequest)
346 def login_and_redirect(req, db, login, key, redirect_url='/'):
347 req.session.authenticate(db, login, key, {})
348 return set_cookie_and_redirect(req, redirect_url)
350 def set_cookie_and_redirect(req, redirect_url):
351 redirect = werkzeug.utils.redirect(redirect_url, 303)
352 redirect.autocorrect_location_header = False
353 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
354 redirect.set_cookie('instance0|session_id', cookie_val)
357 def eval_context_and_domain(session, context, domain=None):
358 e_context = session.eval_context(context)
359 # should we give the evaluated context as an evaluation context to the domain?
360 e_domain = session.eval_domain(domain or [])
362 return e_context, e_domain
364 def load_actions_from_ir_values(req, key, key2, models, meta):
365 context = req.session.eval_context(req.context)
366 Values = req.session.model('ir.values')
367 actions = Values.get(key, key2, models, meta, context)
369 return [(id, name, clean_action(req, action))
370 for id, name, action in actions]
372 def clean_action(req, action, do_not_eval=False):
373 action.setdefault('flags', {})
375 context = req.session.eval_context(req.context)
376 eval_ctx = req.session.evaluation_context(context)
379 # values come from the server, we can just eval them
380 if action.get('context') and isinstance(action.get('context'), basestring):
381 action['context'] = eval( action['context'], eval_ctx ) or {}
383 if action.get('domain') and isinstance(action.get('domain'), basestring):
384 action['domain'] = eval( action['domain'], eval_ctx ) or []
386 if 'context' in action:
387 action['context'] = parse_context(action['context'], req.session)
388 if 'domain' in action:
389 action['domain'] = parse_domain(action['domain'], req.session)
391 action_type = action.setdefault('type', 'ir.actions.act_window_close')
392 if action_type == 'ir.actions.act_window':
393 return fix_view_modes(action)
396 # I think generate_views,fix_view_modes should go into js ActionManager
397 def generate_views(action):
399 While the server generates a sequence called "views" computing dependencies
400 between a bunch of stuff for views coming directly from the database
401 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
402 to return custom view dictionaries generated on the fly.
404 In that case, there is no ``views`` key available on the action.
406 Since the web client relies on ``action['views']``, generate it here from
407 ``view_mode`` and ``view_id``.
409 Currently handles two different cases:
411 * no view_id, multiple view_mode
412 * single view_id, single view_mode
414 :param dict action: action descriptor dictionary to generate a views key for
416 view_id = action.get('view_id') or False
417 if isinstance(view_id, (list, tuple)):
420 # providing at least one view mode is a requirement, not an option
421 view_modes = action['view_mode'].split(',')
423 if len(view_modes) > 1:
425 raise ValueError('Non-db action dictionaries should provide '
426 'either multiple view modes or a single view '
427 'mode and an optional view id.\n\n Got view '
428 'modes %r and view id %r for action %r' % (
429 view_modes, view_id, action))
430 action['views'] = [(False, mode) for mode in view_modes]
432 action['views'] = [(view_id, view_modes[0])]
434 def fix_view_modes(action):
435 """ For historical reasons, OpenERP has weird dealings in relation to
436 view_mode and the view_type attribute (on window actions):
438 * one of the view modes is ``tree``, which stands for both list views
440 * the choice is made by checking ``view_type``, which is either
441 ``form`` for a list view or ``tree`` for an actual tree view
443 This methods simply folds the view_type into view_mode by adding a
444 new view mode ``list`` which is the result of the ``tree`` view_mode
445 in conjunction with the ``form`` view_type.
447 TODO: this should go into the doc, some kind of "peculiarities" section
449 :param dict action: an action descriptor
450 :returns: nothing, the action is modified in place
452 if not action.get('views'):
453 generate_views(action)
455 if action.pop('view_type', 'form') != 'form':
458 if 'view_mode' in action:
459 action['view_mode'] = ','.join(
460 mode if mode != 'tree' else 'list'
461 for mode in action['view_mode'].split(','))
463 [id, mode if mode != 'tree' else 'list']
464 for id, mode in action['views']
469 def parse_domain(domain, session):
470 """ Parses an arbitrary string containing a domain, transforms it
471 to either a literal domain or a :class:`nonliterals.Domain`
473 :param domain: the domain to parse, if the domain is not a string it
474 is assumed to be a literal domain and is returned as-is
475 :param session: Current OpenERP session
476 :type session: openerpweb.OpenERPSession
478 if not isinstance(domain, basestring):
481 return ast.literal_eval(domain)
484 return nonliterals.Domain(session, domain)
486 def parse_context(context, session):
487 """ Parses an arbitrary string containing a context, transforms it
488 to either a literal context or a :class:`nonliterals.Context`
490 :param context: the context to parse, if the context is not a string it
491 is assumed to be a literal domain and is returned as-is
492 :param session: Current OpenERP session
493 :type session: openerpweb.OpenERPSession
495 if not isinstance(context, basestring):
498 return ast.literal_eval(context)
500 return nonliterals.Context(session, context)
502 def _local_web_translations(trans_file):
505 with open(trans_file) as t_file:
506 po = babel.messages.pofile.read_po(t_file)
510 if x.id and x.string and "openerp-web" in x.auto_comments:
511 messages.append({'id': x.id, 'string': x.string})
514 def xml2json_from_elementtree(el, preserve_whitespaces=False):
516 Simple and straightforward XML-to-JSON converter in Python
518 http://code.google.com/p/xml2json-direct/
522 ns, name = el.tag.rsplit("}", 1)
524 res["namespace"] = ns[1:]
528 for k, v in el.items():
531 if el.text and (preserve_whitespaces or el.text.strip() != ''):
534 kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
535 if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
536 kids.append(kid.tail)
537 res["children"] = kids
540 def content_disposition(filename, req):
541 filename = filename.encode('utf8')
542 escaped = urllib2.quote(filename)
543 browser = req.httprequest.user_agent.browser
544 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
545 if browser == 'msie' and version < 9:
546 return "attachment; filename=%s" % escaped
547 elif browser == 'safari':
548 return "attachment; filename=%s" % filename
550 return "attachment; filename*=UTF-8''%s" % escaped
553 #----------------------------------------------------------
554 # OpenERP Web web Controllers
555 #----------------------------------------------------------
557 html_template = """<!DOCTYPE html>
558 <html style="height: 100%%">
560 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
561 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
562 <title>OpenERP</title>
563 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
564 <link rel="stylesheet" href="/web/static/src/css/full.css" />
567 <script type="text/javascript">
569 var s = new openerp.init(%(modules)s);
576 <script src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
578 var test = function() {
583 if (window.localStorage && false) {
584 if (! localStorage.getItem("hasShownGFramePopup")) {
586 localStorage.setItem("hasShownGFramePopup", true);
597 class Home(openerpweb.Controller):
600 @openerpweb.httprequest
601 def index(self, req, s_action=None, **kw):
602 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, None, 'js'))
603 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, None, 'css'))
605 r = html_template % {
608 'modules': simplejson.dumps(module_boot(req)),
609 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
613 @openerpweb.httprequest
614 def login(self, req, db, login, key):
615 return login_and_redirect(req, db, login, key)
617 class WebClient(openerpweb.Controller):
618 _cp_path = "/web/webclient"
620 @openerpweb.jsonrequest
621 def csslist(self, req, mods=None):
622 return manifest_list(req, mods, 'css')
624 @openerpweb.jsonrequest
625 def jslist(self, req, mods=None):
626 return manifest_list(req, mods, 'js')
628 @openerpweb.jsonrequest
629 def qweblist(self, req, mods=None):
630 return manifest_list(req, mods, 'qweb')
632 @openerpweb.httprequest
633 def css(self, req, mods=None):
634 files = list(manifest_glob(req, mods, 'css'))
635 last_modified = get_last_modified(f[0] for f in files)
636 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
637 return werkzeug.wrappers.Response(status=304)
639 file_map = dict(files)
641 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
642 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
645 """read the a css file and absolutify all relative uris"""
646 with open(f, 'rb') as fp:
647 data = fp.read().decode('utf-8')
650 web_dir = os.path.dirname(path)
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 # For performance reasons we only load a single translation, so for
704 # sub-languages (that should only be partially translated) we load the
705 # main language PO instead - that should be enough for the login screen.
706 lang = req.lang.split('_')[0]
708 translations_per_module = {}
709 for addon_name in mods:
710 if openerpweb.addons_manifest[addon_name].get('bootstrap'):
711 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
712 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
713 if not os.path.exists(f_name):
715 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
717 return {"modules": translations_per_module,
718 "lang_parameters": None}
720 @openerpweb.jsonrequest
721 def translations(self, req, mods, lang):
722 res_lang = req.session.model('res.lang')
723 ids = res_lang.search([("code", "=", lang)])
726 lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
727 "grouping", "decimal_point", "thousands_sep"])
729 # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
730 # done server-side when the language is loaded, so we only need to load the user's lang.
731 ir_translation = req.session.model('ir.translation')
732 translations_per_module = {}
733 messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
734 ('comments','like','openerp-web'),('value','!=',False),
736 ['module','src','value','lang'], order='module')
737 for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
738 translations_per_module.setdefault(mod,{'messages':[]})
739 translations_per_module[mod]['messages'].extend({'id': m['src'],
740 'string': m['value']} \
742 return {"modules": translations_per_module,
743 "lang_parameters": lang_params}
745 @openerpweb.jsonrequest
746 def version_info(self, req):
748 "version": openerp.release.version
751 class Proxy(openerpweb.Controller):
752 _cp_path = '/web/proxy'
754 @openerpweb.jsonrequest
755 def load(self, req, path):
756 """ Proxies an HTTP request through a JSON request.
758 It is strongly recommended to not request binary files through this,
759 as the result will be a binary data blob as well.
761 :param req: OpenERP request
762 :param path: actual request path
763 :return: file content
765 from werkzeug.test import Client
766 from werkzeug.wrappers import BaseResponse
768 return Client(req.httprequest.app, BaseResponse).get(path).data
770 class Database(openerpweb.Controller):
771 _cp_path = "/web/database"
773 @openerpweb.jsonrequest
774 def get_list(self, req):
777 @openerpweb.jsonrequest
778 def create(self, req, fields):
779 params = dict(map(operator.itemgetter('name', 'value'), fields))
780 return req.session.proxy("db").create_database(
781 params['super_admin_pwd'],
783 bool(params.get('demo_data')),
785 params['create_admin_pwd'])
787 @openerpweb.jsonrequest
788 def duplicate(self, req, fields):
789 params = dict(map(operator.itemgetter('name', 'value'), fields))
790 return req.session.proxy("db").duplicate_database(
791 params['super_admin_pwd'],
792 params['db_original_name'],
795 @openerpweb.jsonrequest
796 def duplicate(self, req, fields):
797 params = dict(map(operator.itemgetter('name', 'value'), fields))
799 params['super_admin_pwd'],
800 params['db_original_name'],
804 return req.session.proxy("db").duplicate_database(*duplicate_attrs)
806 @openerpweb.jsonrequest
807 def drop(self, req, fields):
808 password, db = operator.itemgetter(
809 'drop_pwd', 'drop_db')(
810 dict(map(operator.itemgetter('name', 'value'), fields)))
813 return req.session.proxy("db").drop(password, db)
814 except xmlrpclib.Fault, e:
815 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
816 return {'error': e.faultCode, 'title': 'Drop Database'}
817 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
819 @openerpweb.httprequest
820 def backup(self, req, backup_db, backup_pwd, token):
822 db_dump = base64.b64decode(
823 req.session.proxy("db").dump(backup_pwd, backup_db))
824 filename = "%(db)s_%(timestamp)s.dump" % {
826 'timestamp': datetime.datetime.utcnow().strftime(
827 "%Y-%m-%d_%H-%M-%SZ")
829 return req.make_response(db_dump,
830 [('Content-Type', 'application/octet-stream; charset=binary'),
831 ('Content-Disposition', content_disposition(filename, req))],
832 {'fileToken': int(token)}
834 except xmlrpclib.Fault, e:
835 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
837 @openerpweb.httprequest
838 def restore(self, req, db_file, restore_pwd, new_db):
840 data = base64.b64encode(db_file.read())
841 req.session.proxy("db").restore(restore_pwd, new_db, data)
843 except xmlrpclib.Fault, e:
844 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
845 raise Exception("AccessDenied")
847 @openerpweb.jsonrequest
848 def change_password(self, req, fields):
849 old_password, new_password = operator.itemgetter(
850 'old_pwd', 'new_pwd')(
851 dict(map(operator.itemgetter('name', 'value'), fields)))
853 return req.session.proxy("db").change_admin_password(old_password, new_password)
854 except xmlrpclib.Fault, e:
855 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
856 return {'error': e.faultCode, 'title': 'Change Password'}
857 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
859 class Session(openerpweb.Controller):
860 _cp_path = "/web/session"
862 def session_info(self, req):
863 req.session.ensure_valid()
865 "session_id": req.session_id,
866 "uid": req.session._uid,
867 "context": req.session.get_context() if req.session._uid else {},
868 "db": req.session._db,
869 "login": req.session._login,
872 @openerpweb.jsonrequest
873 def get_session_info(self, req):
874 return self.session_info(req)
876 @openerpweb.jsonrequest
877 def authenticate(self, req, db, login, password, base_location=None):
878 wsgienv = req.httprequest.environ
880 base_location=base_location,
881 HTTP_HOST=wsgienv['HTTP_HOST'],
882 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
884 req.session.authenticate(db, login, password, env)
886 return self.session_info(req)
888 @openerpweb.jsonrequest
889 def change_password (self,req,fields):
890 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
891 dict(map(operator.itemgetter('name', 'value'), fields)))
892 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
893 return {'error':'You cannot leave any password empty.','title': 'Change Password'}
894 user_pwd = req.session.model('res.users').read([req.session._uid], ['password'], None)[0]['password']
896 if old_password == user_pwd:
897 if old_password == new_password:
898 return {'error':'New password must be different from the old password. Please try again.','title': 'Change Password'}
899 elif new_password != confirm_password:
900 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
901 elif req.session.model('res.users').change_password(old_password, new_password):
902 return {'new_password':new_password}
904 return {'error': 'The Old Password you provided is incorrect. Please try again.', 'title': 'Change Password'}
906 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
908 @openerpweb.jsonrequest
909 def sc_list(self, req):
910 return req.session.model('ir.ui.view_sc').get_sc(
911 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
913 @openerpweb.jsonrequest
914 def get_lang_list(self, req):
916 return req.session.proxy("db").list_lang() or []
918 return {"error": e, "title": "Languages"}
920 @openerpweb.jsonrequest
921 def modules(self, req):
922 # return all installed modules. Web client is smart enough to not load a module twice
923 return module_installed(req)
925 @openerpweb.jsonrequest
926 def eval_domain_and_context(self, req, contexts, domains,
928 """ Evaluates sequences of domains and contexts, composing them into
929 a single context, domain or group_by sequence.
931 :param list contexts: list of contexts to merge together. Contexts are
932 evaluated in sequence, all previous contexts
933 are part of their own evaluation context
934 (starting at the session context).
935 :param list domains: list of domains to merge together. Domains are
936 evaluated in sequence and appended to one another
937 (implicit AND), their evaluation domain is the
938 result of merging all contexts.
939 :param list group_by_seq: list of domains (which may be in a different
940 order than the ``contexts`` parameter),
941 evaluated in sequence, their ``'group_by'``
942 key is extracted if they have one.
947 the global context created by merging all of
951 the concatenation of all domains
954 a list of fields to group by, potentially empty (in which case
955 no group by should be performed)
957 context, domain = eval_context_and_domain(req.session,
958 nonliterals.CompoundContext(*(contexts or [])),
959 nonliterals.CompoundDomain(*(domains or [])))
961 group_by_sequence = []
962 for candidate in (group_by_seq or []):
963 ctx = req.session.eval_context(candidate, context)
964 group_by = ctx.get('group_by')
967 elif isinstance(group_by, basestring):
968 group_by_sequence.append(group_by)
970 group_by_sequence.extend(group_by)
975 'group_by': group_by_sequence
978 @openerpweb.jsonrequest
979 def save_session_action(self, req, the_action):
981 This method store an action object in the session object and returns an integer
982 identifying that action. The method get_session_action() can be used to get
985 :param the_action: The action to save in the session.
986 :type the_action: anything
987 :return: A key identifying the saved action.
990 saved_actions = req.httpsession.get('saved_actions')
991 if not saved_actions:
992 saved_actions = {"next":0, "actions":{}}
993 req.httpsession['saved_actions'] = saved_actions
994 # we don't allow more than 10 stored actions
995 if len(saved_actions["actions"]) >= 10:
996 del saved_actions["actions"][min(saved_actions["actions"])]
997 key = saved_actions["next"]
998 saved_actions["actions"][key] = the_action
999 saved_actions["next"] = key + 1
1002 @openerpweb.jsonrequest
1003 def get_session_action(self, req, key):
1005 Gets back a previously saved action. This method can return None if the action
1006 was saved since too much time (this case should be handled in a smart way).
1008 :param key: The key given by save_session_action()
1010 :return: The saved action or None.
1013 saved_actions = req.httpsession.get('saved_actions')
1014 if not saved_actions:
1016 return saved_actions["actions"].get(key)
1018 @openerpweb.jsonrequest
1019 def check(self, req):
1020 req.session.assert_valid()
1023 @openerpweb.jsonrequest
1024 def destroy(self, req):
1025 req.session._suicide = True
1027 class Menu(openerpweb.Controller):
1028 _cp_path = "/web/menu"
1030 @openerpweb.jsonrequest
1031 def load(self, req):
1032 return {'data': self.do_load(req)}
1034 @openerpweb.jsonrequest
1035 def get_user_roots(self, req):
1036 return self.do_get_user_roots(req)
1038 def do_get_user_roots(self, req):
1039 """ Return all root menu ids visible for the session user.
1041 :param req: A request object, with an OpenERP session attribute
1042 :type req: < session -> OpenERPSession >
1043 :return: the root menu ids
1047 context = s.eval_context(req.context)
1048 Menus = s.model('ir.ui.menu')
1049 # If a menu action is defined use its domain to get the root menu items
1050 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1052 menu_domain = [('parent_id', '=', False)]
1054 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1056 menu_domain = ast.literal_eval(domain_string)
1058 return Menus.search(menu_domain, 0, False, False, context)
1060 def do_load(self, req):
1061 """ Loads all menu items (all applications and their sub-menus).
1063 :param req: A request object, with an OpenERP session attribute
1064 :type req: < session -> OpenERPSession >
1065 :return: the menu root
1066 :rtype: dict('children': menu_nodes)
1068 context = req.session.eval_context(req.context)
1069 Menus = req.session.model('ir.ui.menu')
1071 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1072 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1074 # menus are loaded fully unlike a regular tree view, cause there are a
1075 # limited number of items (752 when all 6.1 addons are installed)
1076 menu_ids = Menus.search([], 0, False, False, context)
1077 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1078 # adds roots at the end of the sequence, so that they will overwrite
1079 # equivalent menu items from full menu read when put into id:item
1080 # mapping, resulting in children being correctly set on the roots.
1081 menu_items.extend(menu_roots)
1083 # make a tree using parent_id
1084 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1085 for menu_item in menu_items:
1086 if menu_item['parent_id']:
1087 parent = menu_item['parent_id'][0]
1090 if parent in menu_items_map:
1091 menu_items_map[parent].setdefault(
1092 'children', []).append(menu_item)
1094 # sort by sequence a tree using parent_id
1095 for menu_item in menu_items:
1096 menu_item.setdefault('children', []).sort(
1097 key=operator.itemgetter('sequence'))
1101 @openerpweb.jsonrequest
1102 def action(self, req, menu_id):
1103 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1104 [('ir.ui.menu', menu_id)], False)
1105 return {"action": actions}
1107 class DataSet(openerpweb.Controller):
1108 _cp_path = "/web/dataset"
1110 @openerpweb.jsonrequest
1111 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1112 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1113 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1115 """ Performs a search() followed by a read() (if needed) using the
1116 provided search criteria
1118 :param req: a JSON-RPC request object
1119 :type req: openerpweb.JsonRequest
1120 :param str model: the name of the model to search on
1121 :param fields: a list of the fields to return in the result records
1123 :param int offset: from which index should the results start being returned
1124 :param int limit: the maximum number of records to return
1125 :param list domain: the search domain for the query
1126 :param list sort: sorting directives
1127 :returns: A structure (dict) with two keys: ids (all the ids matching
1128 the (domain, context) pair) and records (paginated records
1129 matching fields selection set)
1132 Model = req.session.model(model)
1134 context, domain = eval_context_and_domain(
1135 req.session, req.context, domain)
1137 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1138 if limit and len(ids) == limit:
1139 length = Model.search_count(domain, context)
1141 length = len(ids) + (offset or 0)
1142 if fields and fields == ['id']:
1143 # shortcut read if we only want the ids
1146 'records': [{'id': id} for id in ids]
1149 records = Model.read(ids, fields or False, context)
1150 records.sort(key=lambda obj: ids.index(obj['id']))
1156 @openerpweb.jsonrequest
1157 def load(self, req, model, id, fields):
1158 m = req.session.model(model)
1160 r = m.read([id], False, req.session.eval_context(req.context))
1163 return {'value': value}
1165 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1166 has_domain = domain_id is not None and domain_id < len(args)
1167 has_context = context_id is not None and context_id < len(args)
1169 domain = args[domain_id] if has_domain else []
1170 context = args[context_id] if has_context else {}
1171 c, d = eval_context_and_domain(req.session, context, domain)
1175 args[context_id] = c
1177 return self._call_kw(req, model, method, args, {})
1179 def _call_kw(self, req, model, method, args, kwargs):
1180 for i in xrange(len(args)):
1181 if isinstance(args[i], nonliterals.BaseContext):
1182 args[i] = req.session.eval_context(args[i])
1183 elif isinstance(args[i], nonliterals.BaseDomain):
1184 args[i] = req.session.eval_domain(args[i])
1185 for k in kwargs.keys():
1186 if isinstance(kwargs[k], nonliterals.BaseContext):
1187 kwargs[k] = req.session.eval_context(kwargs[k])
1188 elif isinstance(kwargs[k], nonliterals.BaseDomain):
1189 kwargs[k] = req.session.eval_domain(kwargs[k])
1191 # Temporary implements future display_name special field for model#read()
1192 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1193 if 'display_name' in args[1]:
1194 names = req.session.model(model).name_get(args[0], **kwargs)
1195 args[1].remove('display_name')
1196 r = getattr(req.session.model(model), method)(*args, **kwargs)
1197 for i in range(len(r)):
1198 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1201 return getattr(req.session.model(model), method)(*args, **kwargs)
1203 @openerpweb.jsonrequest
1204 def onchange(self, req, model, method, args, context_id=None):
1205 """ Support method for handling onchange calls: behaves much like call
1206 with the following differences:
1208 * Does not take a domain_id
1209 * Is aware of the return value's structure, and will parse the domains
1210 if needed in order to return either parsed literal domains (in JSON)
1211 or non-literal domain instances, allowing those domains to be used
1215 :type req: web.common.http.JsonRequest
1216 :param str model: object type on which to call the method
1217 :param str method: name of the onchange handler method
1218 :param list args: arguments to call the onchange handler with
1219 :param int context_id: index of the context object in the list of
1221 :return: result of the onchange call with all domains parsed
1223 result = self.call_common(req, model, method, args, context_id=context_id)
1224 if not result or 'domain' not in result:
1227 result['domain'] = dict(
1228 (k, parse_domain(v, req.session))
1229 for k, v in result['domain'].iteritems())
1233 @openerpweb.jsonrequest
1234 def call(self, req, model, method, args, domain_id=None, context_id=None):
1235 return self.call_common(req, model, method, args, domain_id, context_id)
1237 @openerpweb.jsonrequest
1238 def call_kw(self, req, model, method, args, kwargs):
1239 return self._call_kw(req, model, method, args, kwargs)
1241 @openerpweb.jsonrequest
1242 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1243 action = self.call_common(req, model, method, args, domain_id, context_id)
1244 if isinstance(action, dict) and action.get('type') != '':
1245 return clean_action(req, action)
1248 @openerpweb.jsonrequest
1249 def exec_workflow(self, req, model, id, signal):
1250 return req.session.exec_workflow(model, id, signal)
1252 @openerpweb.jsonrequest
1253 def resequence(self, req, model, ids, field='sequence', offset=0):
1254 """ Re-sequences a number of records in the model, by their ids
1256 The re-sequencing starts at the first model of ``ids``, the sequence
1257 number is incremented by one after each record and starts at ``offset``
1259 :param ids: identifiers of the records to resequence, in the new sequence order
1261 :param str field: field used for sequence specification, defaults to
1263 :param int offset: sequence number for first record in ``ids``, allows
1264 starting the resequencing from an arbitrary number,
1267 m = req.session.model(model)
1268 if not m.fields_get([field]):
1270 # python 2.6 has no start parameter
1271 for i, id in enumerate(ids):
1272 m.write(id, { field: i + offset })
1275 class View(openerpweb.Controller):
1276 _cp_path = "/web/view"
1278 def fields_view_get(self, req, model, view_id, view_type,
1279 transform=True, toolbar=False, submenu=False):
1280 Model = req.session.model(model)
1281 context = req.session.eval_context(req.context)
1282 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1283 # todo fme?: check that we should pass the evaluated context here
1284 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1285 if toolbar and transform:
1286 self.process_toolbar(req, fvg['toolbar'])
1289 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1290 # depending on how it feels, xmlrpclib.ServerProxy can translate
1291 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1292 # enjoy unicode strings which can not be trivially converted to
1293 # strings, and it blows up during parsing.
1295 # So ensure we fix this retardation by converting view xml back to
1297 if isinstance(fvg['arch'], unicode):
1298 arch = fvg['arch'].encode('utf-8')
1301 fvg['arch_string'] = arch
1304 evaluation_context = session.evaluation_context(context or {})
1305 xml = self.transform_view(arch, session, evaluation_context)
1307 xml = ElementTree.fromstring(arch)
1308 fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
1310 if 'id' in fvg['fields']:
1311 # Special case for id's
1312 id_field = fvg['fields']['id']
1313 id_field['original_type'] = id_field['type']
1314 id_field['type'] = 'id'
1316 for field in fvg['fields'].itervalues():
1317 if field.get('views'):
1318 for view in field["views"].itervalues():
1319 self.process_view(session, view, None, transform)
1320 if field.get('domain'):
1321 field["domain"] = parse_domain(field["domain"], session)
1322 if field.get('context'):
1323 field["context"] = parse_context(field["context"], session)
1325 def process_toolbar(self, req, toolbar):
1327 The toolbar is a mapping of section_key: [action_descriptor]
1329 We need to clean all those actions in order to ensure correct
1332 for actions in toolbar.itervalues():
1333 for action in actions:
1334 if 'context' in action:
1335 action['context'] = parse_context(
1336 action['context'], req.session)
1337 if 'domain' in action:
1338 action['domain'] = parse_domain(
1339 action['domain'], req.session)
1341 @openerpweb.jsonrequest
1342 def add_custom(self, req, view_id, arch):
1343 CustomView = req.session.model('ir.ui.view.custom')
1345 'user_id': req.session._uid,
1348 }, req.session.eval_context(req.context))
1349 return {'result': True}
1351 @openerpweb.jsonrequest
1352 def undo_custom(self, req, view_id, reset=False):
1353 CustomView = req.session.model('ir.ui.view.custom')
1354 context = req.session.eval_context(req.context)
1355 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1356 0, False, False, context)
1359 CustomView.unlink(vcustom, context)
1361 CustomView.unlink([vcustom[0]], context)
1362 return {'result': True}
1363 return {'result': False}
1365 def transform_view(self, view_string, session, context=None):
1366 # transform nodes on the fly via iterparse, instead of
1367 # doing it statically on the parsing result
1368 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1370 for event, elem in parser:
1371 if event == "start":
1374 self.parse_domains_and_contexts(elem, session)
1377 def parse_domains_and_contexts(self, elem, session):
1378 """ Converts domains and contexts from the view into Python objects,
1379 either literals if they can be parsed by literal_eval or a special
1380 placeholder object if the domain or context refers to free variables.
1382 :param elem: the current node being parsed
1383 :type param: xml.etree.ElementTree.Element
1384 :param session: OpenERP session object, used to store and retrieve
1386 :type session: openerpweb.openerpweb.OpenERPSession
1388 for el in ['domain', 'filter_domain']:
1389 domain = elem.get(el, '').strip()
1391 elem.set(el, parse_domain(domain, session))
1392 elem.set(el + '_string', domain)
1393 for el in ['context', 'default_get']:
1394 context_string = elem.get(el, '').strip()
1396 elem.set(el, parse_context(context_string, session))
1397 elem.set(el + '_string', context_string)
1399 @openerpweb.jsonrequest
1400 def load(self, req, model, view_id, view_type, toolbar=False):
1401 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1403 class TreeView(View):
1404 _cp_path = "/web/treeview"
1406 @openerpweb.jsonrequest
1407 def action(self, req, model, id):
1408 return load_actions_from_ir_values(
1409 req,'action', 'tree_but_open',[(model, id)],
1412 class SearchView(View):
1413 _cp_path = "/web/searchview"
1415 @openerpweb.jsonrequest
1416 def load(self, req, model, view_id):
1417 fields_view = self.fields_view_get(req, model, view_id, 'search')
1418 return {'fields_view': fields_view}
1420 @openerpweb.jsonrequest
1421 def fields_get(self, req, model):
1422 Model = req.session.model(model)
1423 fields = Model.fields_get(False, req.session.eval_context(req.context))
1424 for field in fields.values():
1425 # shouldn't convert the views too?
1426 if field.get('domain'):
1427 field["domain"] = parse_domain(field["domain"], req.session)
1428 if field.get('context'):
1429 field["context"] = parse_context(field["context"], req.session)
1430 return {'fields': fields}
1432 @openerpweb.jsonrequest
1433 def get_filters(self, req, model):
1434 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1435 Model = req.session.model("ir.filters")
1436 filters = Model.get_filters(model)
1437 for filter in filters:
1439 parsed_context = parse_context(filter["context"], req.session)
1440 filter["context"] = (parsed_context
1441 if not isinstance(parsed_context, nonliterals.BaseContext)
1442 else req.session.eval_context(parsed_context))
1444 parsed_domain = parse_domain(filter["domain"], req.session)
1445 filter["domain"] = (parsed_domain
1446 if not isinstance(parsed_domain, nonliterals.BaseDomain)
1447 else req.session.eval_domain(parsed_domain))
1449 logger.exception("Failed to parse custom filter %s in %s",
1450 filter['name'], model)
1451 filter['disabled'] = True
1452 del filter['context']
1453 del filter['domain']
1456 class Binary(openerpweb.Controller):
1457 _cp_path = "/web/binary"
1459 @openerpweb.httprequest
1460 def image(self, req, model, id, field, **kw):
1461 last_update = '__last_update'
1462 Model = req.session.model(model)
1463 context = req.session.eval_context(req.context)
1464 headers = [('Content-Type', 'image/png')]
1465 etag = req.httprequest.headers.get('If-None-Match')
1466 hashed_session = hashlib.md5(req.session_id).hexdigest()
1467 id = None if not id else simplejson.loads(id)
1468 if type(id) is list:
1471 if not id and hashed_session == etag:
1472 return werkzeug.wrappers.Response(status=304)
1474 date = Model.read([id], [last_update], context)[0].get(last_update)
1475 if hashlib.md5(date).hexdigest() == etag:
1476 return werkzeug.wrappers.Response(status=304)
1478 retag = hashed_session
1481 res = Model.default_get([field], context).get(field)
1484 res = Model.read([id], [last_update, field], context)[0]
1485 retag = hashlib.md5(res.get(last_update)).hexdigest()
1486 image_base64 = res.get(field)
1488 if kw.get('resize'):
1489 resize = kw.get('resize').split(',');
1490 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1491 width = int(resize[0])
1492 height = int(resize[1])
1493 # resize maximum 500*500
1494 if width > 500: width = 500
1495 if height > 500: height = 500
1496 image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1498 image_data = base64.b64decode(image_base64)
1500 except (TypeError, xmlrpclib.Fault):
1501 image_data = self.placeholder(req)
1502 headers.append(('ETag', retag))
1503 headers.append(('Content-Length', len(image_data)))
1505 ncache = int(kw.get('cache'))
1506 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1509 return req.make_response(image_data, headers)
1510 def placeholder(self, req):
1511 addons_path = openerpweb.addons_manifest['web']['addons_path']
1512 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1514 @openerpweb.httprequest
1515 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1516 """ Download link for files stored as binary fields.
1518 If the ``id`` parameter is omitted, fetches the default value for the
1519 binary field (via ``default_get``), otherwise fetches the field for
1520 that precise record.
1522 :param req: OpenERP request
1523 :type req: :class:`web.common.http.HttpRequest`
1524 :param str model: name of the model to fetch the binary from
1525 :param str field: binary field
1526 :param str id: id of the record from which to fetch the binary
1527 :param str filename_field: field holding the file's name, if any
1528 :returns: :class:`werkzeug.wrappers.Response`
1530 Model = req.session.model(model)
1531 context = req.session.eval_context(req.context)
1534 fields.append(filename_field)
1536 res = Model.read([int(id)], fields, context)[0]
1538 res = Model.default_get(fields, context)
1539 filecontent = base64.b64decode(res.get(field, ''))
1541 return req.not_found()
1543 filename = '%s_%s' % (model.replace('.', '_'), id)
1545 filename = res.get(filename_field, '') or filename
1546 return req.make_response(filecontent,
1547 [('Content-Type', 'application/octet-stream'),
1548 ('Content-Disposition', content_disposition(filename, req))])
1550 @openerpweb.httprequest
1551 def saveas_ajax(self, req, data, token):
1552 jdata = simplejson.loads(data)
1553 model = jdata['model']
1554 field = jdata['field']
1555 id = jdata.get('id', None)
1556 filename_field = jdata.get('filename_field', None)
1557 context = jdata.get('context', dict())
1559 context = req.session.eval_context(context)
1560 Model = req.session.model(model)
1563 fields.append(filename_field)
1565 res = Model.read([int(id)], fields, context)[0]
1567 res = Model.default_get(fields, context)
1568 filecontent = base64.b64decode(res.get(field, ''))
1570 raise ValueError("No content found for field '%s' on '%s:%s'" %
1573 filename = '%s_%s' % (model.replace('.', '_'), id)
1575 filename = res.get(filename_field, '') or filename
1576 return req.make_response(filecontent,
1577 headers=[('Content-Type', 'application/octet-stream'),
1578 ('Content-Disposition', content_disposition(filename, req))],
1579 cookies={'fileToken': int(token)})
1581 @openerpweb.httprequest
1582 def upload(self, req, callback, ufile):
1583 # TODO: might be useful to have a configuration flag for max-length file uploads
1585 out = """<script language="javascript" type="text/javascript">
1586 var win = window.top.window;
1587 win.jQuery(win).trigger(%s, %s);
1590 args = [len(data), ufile.filename,
1591 ufile.content_type, base64.b64encode(data)]
1592 except Exception, e:
1593 args = [False, e.message]
1594 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1596 @openerpweb.httprequest
1597 def upload_attachment(self, req, callback, model, id, ufile):
1598 context = req.session.eval_context(req.context)
1599 Model = req.session.model('ir.attachment')
1601 out = """<script language="javascript" type="text/javascript">
1602 var win = window.top.window;
1603 win.jQuery(win).trigger(%s, %s);
1605 attachment_id = Model.create({
1606 'name': ufile.filename,
1607 'datas': base64.encodestring(ufile.read()),
1608 'datas_fname': ufile.filename,
1613 'filename': ufile.filename,
1616 except Exception, e:
1617 args = { 'error': e.message }
1618 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1620 class Action(openerpweb.Controller):
1621 _cp_path = "/web/action"
1623 @openerpweb.jsonrequest
1624 def load(self, req, action_id, do_not_eval=False):
1625 Actions = req.session.model('ir.actions.actions')
1627 context = req.session.eval_context(req.context)
1630 action_id = int(action_id)
1633 module, xmlid = action_id.split('.', 1)
1634 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1635 assert model.startswith('ir.actions.')
1637 action_id = 0 # force failed read
1639 base_action = Actions.read([action_id], ['type'], context)
1642 action_type = base_action[0]['type']
1643 if action_type == 'ir.actions.report.xml':
1644 ctx.update({'bin_size': True})
1646 action = req.session.model(action_type).read([action_id], False, ctx)
1648 value = clean_action(req, action[0], do_not_eval)
1651 @openerpweb.jsonrequest
1652 def run(self, req, action_id):
1653 return_action = req.session.model('ir.actions.server').run(
1654 [action_id], req.session.eval_context(req.context))
1656 return clean_action(req, return_action)
1661 _cp_path = "/web/export"
1663 @openerpweb.jsonrequest
1664 def formats(self, req):
1665 """ Returns all valid export formats
1667 :returns: for each export format, a pair of identifier and printable name
1668 :rtype: [(str, str)]
1672 for path, controller in openerpweb.controllers_path.iteritems()
1673 if path.startswith(self._cp_path)
1674 if hasattr(controller, 'fmt')
1675 ], key=operator.itemgetter("label"))
1677 def fields_get(self, req, model):
1678 Model = req.session.model(model)
1679 fields = Model.fields_get(False, req.session.eval_context(req.context))
1682 @openerpweb.jsonrequest
1683 def get_fields(self, req, model, prefix='', parent_name= '',
1684 import_compat=True, parent_field_type=None,
1687 if import_compat and parent_field_type == "many2one":
1690 fields = self.fields_get(req, model)
1693 fields.pop('id', None)
1695 fields['.id'] = fields.pop('id', {'string': 'ID'})
1697 fields_sequence = sorted(fields.iteritems(),
1698 key=lambda field: field[1].get('string', ''))
1701 for field_name, field in fields_sequence:
1703 if exclude and field_name in exclude:
1705 if field.get('readonly'):
1706 # If none of the field's states unsets readonly, skip the field
1707 if all(dict(attrs).get('readonly', True)
1708 for attrs in field.get('states', {}).values()):
1711 id = prefix + (prefix and '/'or '') + field_name
1712 name = parent_name + (parent_name and '/' or '') + field['string']
1713 record = {'id': id, 'string': name,
1714 'value': id, 'children': False,
1715 'field_type': field.get('type'),
1716 'required': field.get('required'),
1717 'relation_field': field.get('relation_field')}
1718 records.append(record)
1720 if len(name.split('/')) < 3 and 'relation' in field:
1721 ref = field.pop('relation')
1722 record['value'] += '/id'
1723 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1725 if not import_compat or field['type'] == 'one2many':
1726 # m2m field in import_compat is childless
1727 record['children'] = True
1731 @openerpweb.jsonrequest
1732 def namelist(self,req, model, export_id):
1733 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1734 export = req.session.model("ir.exports").read([export_id])[0]
1735 export_fields_list = req.session.model("ir.exports.line").read(
1736 export['export_fields'])
1738 fields_data = self.fields_info(
1739 req, model, map(operator.itemgetter('name'), export_fields_list))
1742 {'name': field['name'], 'label': fields_data[field['name']]}
1743 for field in export_fields_list
1746 def fields_info(self, req, model, export_fields):
1748 fields = self.fields_get(req, model)
1750 # To make fields retrieval more efficient, fetch all sub-fields of a
1751 # given field at the same time. Because the order in the export list is
1752 # arbitrary, this requires ordering all sub-fields of a given field
1753 # together so they can be fetched at the same time
1755 # Works the following way:
1756 # * sort the list of fields to export, the default sorting order will
1757 # put the field itself (if present, for xmlid) and all of its
1758 # sub-fields right after it
1759 # * then, group on: the first field of the path (which is the same for
1760 # a field and for its subfields and the length of splitting on the
1761 # first '/', which basically means grouping the field on one side and
1762 # all of the subfields on the other. This way, we have the field (for
1763 # the xmlid) with length 1, and all of the subfields with the same
1764 # base but a length "flag" of 2
1765 # * if we have a normal field (length 1), just add it to the info
1766 # mapping (with its string) as-is
1767 # * otherwise, recursively call fields_info via graft_subfields.
1768 # all graft_subfields does is take the result of fields_info (on the
1769 # field's model) and prepend the current base (current field), which
1770 # rebuilds the whole sub-tree for the field
1772 # result: because we're not fetching the fields_get for half the
1773 # database models, fetching a namelist with a dozen fields (including
1774 # relational data) falls from ~6s to ~300ms (on the leads model).
1775 # export lists with no sub-fields (e.g. import_compatible lists with
1776 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1777 # there's a single fields_get to execute)
1778 for (base, length), subfields in itertools.groupby(
1779 sorted(export_fields),
1780 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1781 subfields = list(subfields)
1783 # subfields is a seq of $base/*rest, and not loaded yet
1784 info.update(self.graft_subfields(
1785 req, fields[base]['relation'], base, fields[base]['string'],
1789 info[base] = fields[base]['string']
1793 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1794 export_fields = [field.split('/', 1)[1] for field in fields]
1796 (prefix + '/' + k, prefix_string + '/' + v)
1797 for k, v in self.fields_info(req, model, export_fields).iteritems())
1799 #noinspection PyPropertyDefinition
1801 def content_type(self):
1802 """ Provides the format's content type """
1803 raise NotImplementedError()
1805 def filename(self, base):
1806 """ Creates a valid filename for the format (with extension) from the
1807 provided base name (exension-less)
1809 raise NotImplementedError()
1811 def from_data(self, fields, rows):
1812 """ Conversion method from OpenERP's export data to whatever the
1813 current export class outputs
1815 :params list fields: a list of fields to export
1816 :params list rows: a list of records to export
1820 raise NotImplementedError()
1822 @openerpweb.httprequest
1823 def index(self, req, data, token):
1824 model, fields, ids, domain, import_compat = \
1825 operator.itemgetter('model', 'fields', 'ids', 'domain',
1827 simplejson.loads(data))
1829 context = req.session.eval_context(req.context)
1830 Model = req.session.model(model)
1831 ids = ids or Model.search(domain, 0, False, False, context)
1833 field_names = map(operator.itemgetter('name'), fields)
1834 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1837 columns_headers = field_names
1839 columns_headers = [val['label'].strip() for val in fields]
1842 return req.make_response(self.from_data(columns_headers, import_data),
1843 headers=[('Content-Disposition',
1844 content_disposition(self.filename(model), req)),
1845 ('Content-Type', self.content_type)],
1846 cookies={'fileToken': int(token)})
1848 class CSVExport(Export):
1849 _cp_path = '/web/export/csv'
1850 fmt = {'tag': 'csv', 'label': 'CSV'}
1853 def content_type(self):
1854 return 'text/csv;charset=utf8'
1856 def filename(self, base):
1857 return base + '.csv'
1859 def from_data(self, fields, rows):
1861 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1863 writer.writerow([name.encode('utf-8') for name in fields])
1868 if isinstance(d, basestring):
1869 d = d.replace('\n',' ').replace('\t',' ')
1871 d = d.encode('utf-8')
1872 except UnicodeError:
1874 if d is False: d = None
1876 writer.writerow(row)
1883 class ExcelExport(Export):
1884 _cp_path = '/web/export/xls'
1888 'error': None if xlwt else "XLWT required"
1892 def content_type(self):
1893 return 'application/vnd.ms-excel'
1895 def filename(self, base):
1896 return base + '.xls'
1898 def from_data(self, fields, rows):
1899 workbook = xlwt.Workbook()
1900 worksheet = workbook.add_sheet('Sheet 1')
1902 for i, fieldname in enumerate(fields):
1903 worksheet.write(0, i, fieldname)
1904 worksheet.col(i).width = 8000 # around 220 pixels
1906 style = xlwt.easyxf('align: wrap yes')
1908 for row_index, row in enumerate(rows):
1909 for cell_index, cell_value in enumerate(row):
1910 if isinstance(cell_value, basestring):
1911 cell_value = re.sub("\r", " ", cell_value)
1912 if cell_value is False: cell_value = None
1913 worksheet.write(row_index + 1, cell_index, cell_value, style)
1922 class Reports(View):
1923 _cp_path = "/web/report"
1924 POLLING_DELAY = 0.25
1926 'doc': 'application/vnd.ms-word',
1927 'html': 'text/html',
1928 'odt': 'application/vnd.oasis.opendocument.text',
1929 'pdf': 'application/pdf',
1930 'sxw': 'application/vnd.sun.xml.writer',
1931 'xls': 'application/vnd.ms-excel',
1934 @openerpweb.httprequest
1935 def index(self, req, action, token):
1936 action = simplejson.loads(action)
1938 report_srv = req.session.proxy("report")
1939 context = req.session.eval_context(
1940 nonliterals.CompoundContext(
1941 req.context or {}, action[ "context"]))
1944 report_ids = context["active_ids"]
1945 if 'report_type' in action:
1946 report_data['report_type'] = action['report_type']
1947 if 'datas' in action:
1948 if 'ids' in action['datas']:
1949 report_ids = action['datas'].pop('ids')
1950 report_data.update(action['datas'])
1952 report_id = report_srv.report(
1953 req.session._db, req.session._uid, req.session._password,
1954 action["report_name"], report_ids,
1955 report_data, context)
1957 report_struct = None
1959 report_struct = report_srv.report_get(
1960 req.session._db, req.session._uid, req.session._password, report_id)
1961 if report_struct["state"]:
1964 time.sleep(self.POLLING_DELAY)
1966 report = base64.b64decode(report_struct['result'])
1967 if report_struct.get('code') == 'zlib':
1968 report = zlib.decompress(report)
1969 report_mimetype = self.TYPES_MAPPING.get(
1970 report_struct['format'], 'octet-stream')
1971 file_name = action.get('name', 'report')
1972 if 'name' not in action:
1973 reports = req.session.model('ir.actions.report.xml')
1974 res_id = reports.search([('report_name', '=', action['report_name']),],
1975 0, False, False, context)
1977 file_name = reports.read(res_id[0], ['name'], context)['name']
1979 file_name = action['report_name']
1980 file_name = '%s.%s' % (file_name, report_struct['format'])
1982 return req.make_response(report,
1984 ('Content-Disposition', content_disposition(file_name, req)),
1985 ('Content-Type', report_mimetype),
1986 ('Content-Length', len(report))],
1987 cookies={'fileToken': int(token)})
1989 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: