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':'All passwords have to be filled.','title': 'Change Password'}
894 if new_password != confirm_password:
895 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
897 if req.session.model('res.users').change_password(
898 old_password, new_password):
899 return {'new_password':new_password}
901 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
902 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
904 @openerpweb.jsonrequest
905 def sc_list(self, req):
906 return req.session.model('ir.ui.view_sc').get_sc(
907 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
909 @openerpweb.jsonrequest
910 def get_lang_list(self, req):
912 return req.session.proxy("db").list_lang() or []
914 return {"error": e, "title": "Languages"}
916 @openerpweb.jsonrequest
917 def modules(self, req):
918 # return all installed modules. Web client is smart enough to not load a module twice
919 return module_installed(req)
921 @openerpweb.jsonrequest
922 def eval_domain_and_context(self, req, contexts, domains,
924 """ Evaluates sequences of domains and contexts, composing them into
925 a single context, domain or group_by sequence.
927 :param list contexts: list of contexts to merge together. Contexts are
928 evaluated in sequence, all previous contexts
929 are part of their own evaluation context
930 (starting at the session context).
931 :param list domains: list of domains to merge together. Domains are
932 evaluated in sequence and appended to one another
933 (implicit AND), their evaluation domain is the
934 result of merging all contexts.
935 :param list group_by_seq: list of domains (which may be in a different
936 order than the ``contexts`` parameter),
937 evaluated in sequence, their ``'group_by'``
938 key is extracted if they have one.
943 the global context created by merging all of
947 the concatenation of all domains
950 a list of fields to group by, potentially empty (in which case
951 no group by should be performed)
953 context, domain = eval_context_and_domain(req.session,
954 nonliterals.CompoundContext(*(contexts or [])),
955 nonliterals.CompoundDomain(*(domains or [])))
957 group_by_sequence = []
958 for candidate in (group_by_seq or []):
959 ctx = req.session.eval_context(candidate, context)
960 group_by = ctx.get('group_by')
963 elif isinstance(group_by, basestring):
964 group_by_sequence.append(group_by)
966 group_by_sequence.extend(group_by)
971 'group_by': group_by_sequence
974 @openerpweb.jsonrequest
975 def save_session_action(self, req, the_action):
977 This method store an action object in the session object and returns an integer
978 identifying that action. The method get_session_action() can be used to get
981 :param the_action: The action to save in the session.
982 :type the_action: anything
983 :return: A key identifying the saved action.
986 saved_actions = req.httpsession.get('saved_actions')
987 if not saved_actions:
988 saved_actions = {"next":0, "actions":{}}
989 req.httpsession['saved_actions'] = saved_actions
990 # we don't allow more than 10 stored actions
991 if len(saved_actions["actions"]) >= 10:
992 del saved_actions["actions"][min(saved_actions["actions"])]
993 key = saved_actions["next"]
994 saved_actions["actions"][key] = the_action
995 saved_actions["next"] = key + 1
998 @openerpweb.jsonrequest
999 def get_session_action(self, req, key):
1001 Gets back a previously saved action. This method can return None if the action
1002 was saved since too much time (this case should be handled in a smart way).
1004 :param key: The key given by save_session_action()
1006 :return: The saved action or None.
1009 saved_actions = req.httpsession.get('saved_actions')
1010 if not saved_actions:
1012 return saved_actions["actions"].get(key)
1014 @openerpweb.jsonrequest
1015 def check(self, req):
1016 req.session.assert_valid()
1019 @openerpweb.jsonrequest
1020 def destroy(self, req):
1021 req.session._suicide = True
1023 class Menu(openerpweb.Controller):
1024 _cp_path = "/web/menu"
1026 @openerpweb.jsonrequest
1027 def load(self, req):
1028 return {'data': self.do_load(req)}
1030 @openerpweb.jsonrequest
1031 def get_user_roots(self, req):
1032 return self.do_get_user_roots(req)
1034 def do_get_user_roots(self, req):
1035 """ Return all root menu ids visible for the session user.
1037 :param req: A request object, with an OpenERP session attribute
1038 :type req: < session -> OpenERPSession >
1039 :return: the root menu ids
1043 context = s.eval_context(req.context)
1044 Menus = s.model('ir.ui.menu')
1045 # If a menu action is defined use its domain to get the root menu items
1046 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1048 menu_domain = [('parent_id', '=', False)]
1050 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1052 menu_domain = ast.literal_eval(domain_string)
1054 return Menus.search(menu_domain, 0, False, False, context)
1056 def do_load(self, req):
1057 """ Loads all menu items (all applications and their sub-menus).
1059 :param req: A request object, with an OpenERP session attribute
1060 :type req: < session -> OpenERPSession >
1061 :return: the menu root
1062 :rtype: dict('children': menu_nodes)
1064 context = req.session.eval_context(req.context)
1065 Menus = req.session.model('ir.ui.menu')
1067 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1068 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1070 # menus are loaded fully unlike a regular tree view, cause there are a
1071 # limited number of items (752 when all 6.1 addons are installed)
1072 menu_ids = Menus.search([], 0, False, False, context)
1073 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1074 # adds roots at the end of the sequence, so that they will overwrite
1075 # equivalent menu items from full menu read when put into id:item
1076 # mapping, resulting in children being correctly set on the roots.
1077 menu_items.extend(menu_roots)
1079 # make a tree using parent_id
1080 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1081 for menu_item in menu_items:
1082 if menu_item['parent_id']:
1083 parent = menu_item['parent_id'][0]
1086 if parent in menu_items_map:
1087 menu_items_map[parent].setdefault(
1088 'children', []).append(menu_item)
1090 # sort by sequence a tree using parent_id
1091 for menu_item in menu_items:
1092 menu_item.setdefault('children', []).sort(
1093 key=operator.itemgetter('sequence'))
1097 @openerpweb.jsonrequest
1098 def action(self, req, menu_id):
1099 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1100 [('ir.ui.menu', menu_id)], False)
1101 return {"action": actions}
1103 class DataSet(openerpweb.Controller):
1104 _cp_path = "/web/dataset"
1106 @openerpweb.jsonrequest
1107 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1108 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1109 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1111 """ Performs a search() followed by a read() (if needed) using the
1112 provided search criteria
1114 :param req: a JSON-RPC request object
1115 :type req: openerpweb.JsonRequest
1116 :param str model: the name of the model to search on
1117 :param fields: a list of the fields to return in the result records
1119 :param int offset: from which index should the results start being returned
1120 :param int limit: the maximum number of records to return
1121 :param list domain: the search domain for the query
1122 :param list sort: sorting directives
1123 :returns: A structure (dict) with two keys: ids (all the ids matching
1124 the (domain, context) pair) and records (paginated records
1125 matching fields selection set)
1128 Model = req.session.model(model)
1130 context, domain = eval_context_and_domain(
1131 req.session, req.context, domain)
1133 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1134 if limit and len(ids) == limit:
1135 length = Model.search_count(domain, context)
1137 length = len(ids) + (offset or 0)
1138 if fields and fields == ['id']:
1139 # shortcut read if we only want the ids
1142 'records': [{'id': id} for id in ids]
1145 records = Model.read(ids, fields or False, context)
1146 records.sort(key=lambda obj: ids.index(obj['id']))
1152 @openerpweb.jsonrequest
1153 def load(self, req, model, id, fields):
1154 m = req.session.model(model)
1156 r = m.read([id], False, req.session.eval_context(req.context))
1159 return {'value': value}
1161 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1162 has_domain = domain_id is not None and domain_id < len(args)
1163 has_context = context_id is not None and context_id < len(args)
1165 domain = args[domain_id] if has_domain else []
1166 context = args[context_id] if has_context else {}
1167 c, d = eval_context_and_domain(req.session, context, domain)
1171 args[context_id] = c
1173 return self._call_kw(req, model, method, args, {})
1175 def _call_kw(self, req, model, method, args, kwargs):
1176 for i in xrange(len(args)):
1177 if isinstance(args[i], nonliterals.BaseContext):
1178 args[i] = req.session.eval_context(args[i])
1179 elif isinstance(args[i], nonliterals.BaseDomain):
1180 args[i] = req.session.eval_domain(args[i])
1181 for k in kwargs.keys():
1182 if isinstance(kwargs[k], nonliterals.BaseContext):
1183 kwargs[k] = req.session.eval_context(kwargs[k])
1184 elif isinstance(kwargs[k], nonliterals.BaseDomain):
1185 kwargs[k] = req.session.eval_domain(kwargs[k])
1187 # Temporary implements future display_name special field for model#read()
1188 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1189 if 'display_name' in args[1]:
1190 names = req.session.model(model).name_get(args[0], **kwargs)
1191 args[1].remove('display_name')
1192 r = getattr(req.session.model(model), method)(*args, **kwargs)
1193 for i in range(len(r)):
1194 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1197 return getattr(req.session.model(model), method)(*args, **kwargs)
1199 @openerpweb.jsonrequest
1200 def onchange(self, req, model, method, args, context_id=None):
1201 """ Support method for handling onchange calls: behaves much like call
1202 with the following differences:
1204 * Does not take a domain_id
1205 * Is aware of the return value's structure, and will parse the domains
1206 if needed in order to return either parsed literal domains (in JSON)
1207 or non-literal domain instances, allowing those domains to be used
1211 :type req: web.common.http.JsonRequest
1212 :param str model: object type on which to call the method
1213 :param str method: name of the onchange handler method
1214 :param list args: arguments to call the onchange handler with
1215 :param int context_id: index of the context object in the list of
1217 :return: result of the onchange call with all domains parsed
1219 result = self.call_common(req, model, method, args, context_id=context_id)
1220 if not result or 'domain' not in result:
1223 result['domain'] = dict(
1224 (k, parse_domain(v, req.session))
1225 for k, v in result['domain'].iteritems())
1229 @openerpweb.jsonrequest
1230 def call(self, req, model, method, args, domain_id=None, context_id=None):
1231 return self.call_common(req, model, method, args, domain_id, context_id)
1233 @openerpweb.jsonrequest
1234 def call_kw(self, req, model, method, args, kwargs):
1235 return self._call_kw(req, model, method, args, kwargs)
1237 @openerpweb.jsonrequest
1238 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1239 action = self.call_common(req, model, method, args, domain_id, context_id)
1240 if isinstance(action, dict) and action.get('type') != '':
1241 return clean_action(req, action)
1244 @openerpweb.jsonrequest
1245 def exec_workflow(self, req, model, id, signal):
1246 return req.session.exec_workflow(model, id, signal)
1248 @openerpweb.jsonrequest
1249 def resequence(self, req, model, ids, field='sequence', offset=0):
1250 """ Re-sequences a number of records in the model, by their ids
1252 The re-sequencing starts at the first model of ``ids``, the sequence
1253 number is incremented by one after each record and starts at ``offset``
1255 :param ids: identifiers of the records to resequence, in the new sequence order
1257 :param str field: field used for sequence specification, defaults to
1259 :param int offset: sequence number for first record in ``ids``, allows
1260 starting the resequencing from an arbitrary number,
1263 m = req.session.model(model)
1264 if not m.fields_get([field]):
1266 # python 2.6 has no start parameter
1267 for i, id in enumerate(ids):
1268 m.write(id, { field: i + offset })
1271 class View(openerpweb.Controller):
1272 _cp_path = "/web/view"
1274 def fields_view_get(self, req, model, view_id, view_type,
1275 transform=True, toolbar=False, submenu=False):
1276 Model = req.session.model(model)
1277 context = req.session.eval_context(req.context)
1278 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1279 # todo fme?: check that we should pass the evaluated context here
1280 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1281 if toolbar and transform:
1282 self.process_toolbar(req, fvg['toolbar'])
1285 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1286 # depending on how it feels, xmlrpclib.ServerProxy can translate
1287 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1288 # enjoy unicode strings which can not be trivially converted to
1289 # strings, and it blows up during parsing.
1291 # So ensure we fix this retardation by converting view xml back to
1293 if isinstance(fvg['arch'], unicode):
1294 arch = fvg['arch'].encode('utf-8')
1297 fvg['arch_string'] = arch
1300 evaluation_context = session.evaluation_context(context or {})
1301 xml = self.transform_view(arch, session, evaluation_context)
1303 xml = ElementTree.fromstring(arch)
1304 fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
1306 if 'id' in fvg['fields']:
1307 # Special case for id's
1308 id_field = fvg['fields']['id']
1309 id_field['original_type'] = id_field['type']
1310 id_field['type'] = 'id'
1312 for field in fvg['fields'].itervalues():
1313 if field.get('views'):
1314 for view in field["views"].itervalues():
1315 self.process_view(session, view, None, transform)
1316 if field.get('domain'):
1317 field["domain"] = parse_domain(field["domain"], session)
1318 if field.get('context'):
1319 field["context"] = parse_context(field["context"], session)
1321 def process_toolbar(self, req, toolbar):
1323 The toolbar is a mapping of section_key: [action_descriptor]
1325 We need to clean all those actions in order to ensure correct
1328 for actions in toolbar.itervalues():
1329 for action in actions:
1330 if 'context' in action:
1331 action['context'] = parse_context(
1332 action['context'], req.session)
1333 if 'domain' in action:
1334 action['domain'] = parse_domain(
1335 action['domain'], req.session)
1337 @openerpweb.jsonrequest
1338 def add_custom(self, req, view_id, arch):
1339 CustomView = req.session.model('ir.ui.view.custom')
1341 'user_id': req.session._uid,
1344 }, req.session.eval_context(req.context))
1345 return {'result': True}
1347 @openerpweb.jsonrequest
1348 def undo_custom(self, req, view_id, reset=False):
1349 CustomView = req.session.model('ir.ui.view.custom')
1350 context = req.session.eval_context(req.context)
1351 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1352 0, False, False, context)
1355 CustomView.unlink(vcustom, context)
1357 CustomView.unlink([vcustom[0]], context)
1358 return {'result': True}
1359 return {'result': False}
1361 def transform_view(self, view_string, session, context=None):
1362 # transform nodes on the fly via iterparse, instead of
1363 # doing it statically on the parsing result
1364 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1366 for event, elem in parser:
1367 if event == "start":
1370 self.parse_domains_and_contexts(elem, session)
1373 def parse_domains_and_contexts(self, elem, session):
1374 """ Converts domains and contexts from the view into Python objects,
1375 either literals if they can be parsed by literal_eval or a special
1376 placeholder object if the domain or context refers to free variables.
1378 :param elem: the current node being parsed
1379 :type param: xml.etree.ElementTree.Element
1380 :param session: OpenERP session object, used to store and retrieve
1382 :type session: openerpweb.openerpweb.OpenERPSession
1384 for el in ['domain', 'filter_domain']:
1385 domain = elem.get(el, '').strip()
1387 elem.set(el, parse_domain(domain, session))
1388 elem.set(el + '_string', domain)
1389 for el in ['context', 'default_get']:
1390 context_string = elem.get(el, '').strip()
1392 elem.set(el, parse_context(context_string, session))
1393 elem.set(el + '_string', context_string)
1395 @openerpweb.jsonrequest
1396 def load(self, req, model, view_id, view_type, toolbar=False):
1397 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1399 class TreeView(View):
1400 _cp_path = "/web/treeview"
1402 @openerpweb.jsonrequest
1403 def action(self, req, model, id):
1404 return load_actions_from_ir_values(
1405 req,'action', 'tree_but_open',[(model, id)],
1408 class SearchView(View):
1409 _cp_path = "/web/searchview"
1411 @openerpweb.jsonrequest
1412 def load(self, req, model, view_id):
1413 fields_view = self.fields_view_get(req, model, view_id, 'search')
1414 return {'fields_view': fields_view}
1416 @openerpweb.jsonrequest
1417 def fields_get(self, req, model):
1418 Model = req.session.model(model)
1419 fields = Model.fields_get(False, req.session.eval_context(req.context))
1420 for field in fields.values():
1421 # shouldn't convert the views too?
1422 if field.get('domain'):
1423 field["domain"] = parse_domain(field["domain"], req.session)
1424 if field.get('context'):
1425 field["context"] = parse_context(field["context"], req.session)
1426 return {'fields': fields}
1428 @openerpweb.jsonrequest
1429 def get_filters(self, req, model):
1430 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1431 Model = req.session.model("ir.filters")
1432 filters = Model.get_filters(model)
1433 for filter in filters:
1435 parsed_context = parse_context(filter["context"], req.session)
1436 filter["context"] = (parsed_context
1437 if not isinstance(parsed_context, nonliterals.BaseContext)
1438 else req.session.eval_context(parsed_context))
1440 parsed_domain = parse_domain(filter["domain"], req.session)
1441 filter["domain"] = (parsed_domain
1442 if not isinstance(parsed_domain, nonliterals.BaseDomain)
1443 else req.session.eval_domain(parsed_domain))
1445 logger.exception("Failed to parse custom filter %s in %s",
1446 filter['name'], model)
1447 filter['disabled'] = True
1448 del filter['context']
1449 del filter['domain']
1452 class Binary(openerpweb.Controller):
1453 _cp_path = "/web/binary"
1455 @openerpweb.httprequest
1456 def image(self, req, model, id, field, **kw):
1457 last_update = '__last_update'
1458 Model = req.session.model(model)
1459 context = req.session.eval_context(req.context)
1460 headers = [('Content-Type', 'image/png')]
1461 etag = req.httprequest.headers.get('If-None-Match')
1462 hashed_session = hashlib.md5(req.session_id).hexdigest()
1463 id = None if not id else simplejson.loads(id)
1464 if type(id) is list:
1467 if not id and hashed_session == etag:
1468 return werkzeug.wrappers.Response(status=304)
1470 date = Model.read([id], [last_update], context)[0].get(last_update)
1471 if hashlib.md5(date).hexdigest() == etag:
1472 return werkzeug.wrappers.Response(status=304)
1474 retag = hashed_session
1477 res = Model.default_get([field], context).get(field)
1480 res = Model.read([id], [last_update, field], context)[0]
1481 retag = hashlib.md5(res.get(last_update)).hexdigest()
1482 image_base64 = res.get(field)
1484 if kw.get('resize'):
1485 resize = kw.get('resize').split(',');
1486 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1487 width = int(resize[0])
1488 height = int(resize[1])
1489 # resize maximum 500*500
1490 if width > 500: width = 500
1491 if height > 500: height = 500
1492 image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1494 image_data = base64.b64decode(image_base64)
1496 except (TypeError, xmlrpclib.Fault):
1497 image_data = self.placeholder(req)
1498 headers.append(('ETag', retag))
1499 headers.append(('Content-Length', len(image_data)))
1501 ncache = int(kw.get('cache'))
1502 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1505 return req.make_response(image_data, headers)
1506 def placeholder(self, req):
1507 addons_path = openerpweb.addons_manifest['web']['addons_path']
1508 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1510 @openerpweb.httprequest
1511 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1512 """ Download link for files stored as binary fields.
1514 If the ``id`` parameter is omitted, fetches the default value for the
1515 binary field (via ``default_get``), otherwise fetches the field for
1516 that precise record.
1518 :param req: OpenERP request
1519 :type req: :class:`web.common.http.HttpRequest`
1520 :param str model: name of the model to fetch the binary from
1521 :param str field: binary field
1522 :param str id: id of the record from which to fetch the binary
1523 :param str filename_field: field holding the file's name, if any
1524 :returns: :class:`werkzeug.wrappers.Response`
1526 Model = req.session.model(model)
1527 context = req.session.eval_context(req.context)
1530 fields.append(filename_field)
1532 res = Model.read([int(id)], fields, context)[0]
1534 res = Model.default_get(fields, context)
1535 filecontent = base64.b64decode(res.get(field, ''))
1537 return req.not_found()
1539 filename = '%s_%s' % (model.replace('.', '_'), id)
1541 filename = res.get(filename_field, '') or filename
1542 return req.make_response(filecontent,
1543 [('Content-Type', 'application/octet-stream'),
1544 ('Content-Disposition', content_disposition(filename, req))])
1546 @openerpweb.httprequest
1547 def saveas_ajax(self, req, data, token):
1548 jdata = simplejson.loads(data)
1549 model = jdata['model']
1550 field = jdata['field']
1551 id = jdata.get('id', None)
1552 filename_field = jdata.get('filename_field', None)
1553 context = jdata.get('context', dict())
1555 context = req.session.eval_context(context)
1556 Model = req.session.model(model)
1559 fields.append(filename_field)
1561 res = Model.read([int(id)], fields, context)[0]
1563 res = Model.default_get(fields, context)
1564 filecontent = base64.b64decode(res.get(field, ''))
1566 raise ValueError("No content found for field '%s' on '%s:%s'" %
1569 filename = '%s_%s' % (model.replace('.', '_'), id)
1571 filename = res.get(filename_field, '') or filename
1572 return req.make_response(filecontent,
1573 headers=[('Content-Type', 'application/octet-stream'),
1574 ('Content-Disposition', content_disposition(filename, req))],
1575 cookies={'fileToken': int(token)})
1577 @openerpweb.httprequest
1578 def upload(self, req, callback, ufile):
1579 # TODO: might be useful to have a configuration flag for max-length file uploads
1581 out = """<script language="javascript" type="text/javascript">
1582 var win = window.top.window;
1583 win.jQuery(win).trigger(%s, %s);
1586 args = [len(data), ufile.filename,
1587 ufile.content_type, base64.b64encode(data)]
1588 except Exception, e:
1589 args = [False, e.message]
1590 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1592 @openerpweb.httprequest
1593 def upload_attachment(self, req, callback, model, id, ufile):
1594 context = req.session.eval_context(req.context)
1595 Model = req.session.model('ir.attachment')
1597 out = """<script language="javascript" type="text/javascript">
1598 var win = window.top.window;
1599 win.jQuery(win).trigger(%s, %s);
1601 attachment_id = Model.create({
1602 'name': ufile.filename,
1603 'datas': base64.encodestring(ufile.read()),
1604 'datas_fname': ufile.filename,
1609 'filename': ufile.filename,
1612 except Exception, e:
1613 args = { 'error': e.message }
1614 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1616 class Action(openerpweb.Controller):
1617 _cp_path = "/web/action"
1619 @openerpweb.jsonrequest
1620 def load(self, req, action_id, do_not_eval=False):
1621 Actions = req.session.model('ir.actions.actions')
1623 context = req.session.eval_context(req.context)
1626 action_id = int(action_id)
1629 module, xmlid = action_id.split('.', 1)
1630 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1631 assert model.startswith('ir.actions.')
1633 action_id = 0 # force failed read
1635 base_action = Actions.read([action_id], ['type'], context)
1638 action_type = base_action[0]['type']
1639 if action_type == 'ir.actions.report.xml':
1640 ctx.update({'bin_size': True})
1642 action = req.session.model(action_type).read([action_id], False, ctx)
1644 value = clean_action(req, action[0], do_not_eval)
1647 @openerpweb.jsonrequest
1648 def run(self, req, action_id):
1649 return_action = req.session.model('ir.actions.server').run(
1650 [action_id], req.session.eval_context(req.context))
1652 return clean_action(req, return_action)
1657 _cp_path = "/web/export"
1659 @openerpweb.jsonrequest
1660 def formats(self, req):
1661 """ Returns all valid export formats
1663 :returns: for each export format, a pair of identifier and printable name
1664 :rtype: [(str, str)]
1668 for path, controller in openerpweb.controllers_path.iteritems()
1669 if path.startswith(self._cp_path)
1670 if hasattr(controller, 'fmt')
1671 ], key=operator.itemgetter("label"))
1673 def fields_get(self, req, model):
1674 Model = req.session.model(model)
1675 fields = Model.fields_get(False, req.session.eval_context(req.context))
1678 @openerpweb.jsonrequest
1679 def get_fields(self, req, model, prefix='', parent_name= '',
1680 import_compat=True, parent_field_type=None,
1683 if import_compat and parent_field_type == "many2one":
1686 fields = self.fields_get(req, model)
1689 fields.pop('id', None)
1691 fields['.id'] = fields.pop('id', {'string': 'ID'})
1693 fields_sequence = sorted(fields.iteritems(),
1694 key=lambda field: field[1].get('string', ''))
1697 for field_name, field in fields_sequence:
1699 if exclude and field_name in exclude:
1701 if field.get('readonly'):
1702 # If none of the field's states unsets readonly, skip the field
1703 if all(dict(attrs).get('readonly', True)
1704 for attrs in field.get('states', {}).values()):
1707 id = prefix + (prefix and '/'or '') + field_name
1708 name = parent_name + (parent_name and '/' or '') + field['string']
1709 record = {'id': id, 'string': name,
1710 'value': id, 'children': False,
1711 'field_type': field.get('type'),
1712 'required': field.get('required'),
1713 'relation_field': field.get('relation_field')}
1714 records.append(record)
1716 if len(name.split('/')) < 3 and 'relation' in field:
1717 ref = field.pop('relation')
1718 record['value'] += '/id'
1719 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1721 if not import_compat or field['type'] == 'one2many':
1722 # m2m field in import_compat is childless
1723 record['children'] = True
1727 @openerpweb.jsonrequest
1728 def namelist(self,req, model, export_id):
1729 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1730 export = req.session.model("ir.exports").read([export_id])[0]
1731 export_fields_list = req.session.model("ir.exports.line").read(
1732 export['export_fields'])
1734 fields_data = self.fields_info(
1735 req, model, map(operator.itemgetter('name'), export_fields_list))
1738 {'name': field['name'], 'label': fields_data[field['name']]}
1739 for field in export_fields_list
1742 def fields_info(self, req, model, export_fields):
1744 fields = self.fields_get(req, model)
1746 # To make fields retrieval more efficient, fetch all sub-fields of a
1747 # given field at the same time. Because the order in the export list is
1748 # arbitrary, this requires ordering all sub-fields of a given field
1749 # together so they can be fetched at the same time
1751 # Works the following way:
1752 # * sort the list of fields to export, the default sorting order will
1753 # put the field itself (if present, for xmlid) and all of its
1754 # sub-fields right after it
1755 # * then, group on: the first field of the path (which is the same for
1756 # a field and for its subfields and the length of splitting on the
1757 # first '/', which basically means grouping the field on one side and
1758 # all of the subfields on the other. This way, we have the field (for
1759 # the xmlid) with length 1, and all of the subfields with the same
1760 # base but a length "flag" of 2
1761 # * if we have a normal field (length 1), just add it to the info
1762 # mapping (with its string) as-is
1763 # * otherwise, recursively call fields_info via graft_subfields.
1764 # all graft_subfields does is take the result of fields_info (on the
1765 # field's model) and prepend the current base (current field), which
1766 # rebuilds the whole sub-tree for the field
1768 # result: because we're not fetching the fields_get for half the
1769 # database models, fetching a namelist with a dozen fields (including
1770 # relational data) falls from ~6s to ~300ms (on the leads model).
1771 # export lists with no sub-fields (e.g. import_compatible lists with
1772 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1773 # there's a single fields_get to execute)
1774 for (base, length), subfields in itertools.groupby(
1775 sorted(export_fields),
1776 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1777 subfields = list(subfields)
1779 # subfields is a seq of $base/*rest, and not loaded yet
1780 info.update(self.graft_subfields(
1781 req, fields[base]['relation'], base, fields[base]['string'],
1785 info[base] = fields[base]['string']
1789 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1790 export_fields = [field.split('/', 1)[1] for field in fields]
1792 (prefix + '/' + k, prefix_string + '/' + v)
1793 for k, v in self.fields_info(req, model, export_fields).iteritems())
1795 #noinspection PyPropertyDefinition
1797 def content_type(self):
1798 """ Provides the format's content type """
1799 raise NotImplementedError()
1801 def filename(self, base):
1802 """ Creates a valid filename for the format (with extension) from the
1803 provided base name (exension-less)
1805 raise NotImplementedError()
1807 def from_data(self, fields, rows):
1808 """ Conversion method from OpenERP's export data to whatever the
1809 current export class outputs
1811 :params list fields: a list of fields to export
1812 :params list rows: a list of records to export
1816 raise NotImplementedError()
1818 @openerpweb.httprequest
1819 def index(self, req, data, token):
1820 model, fields, ids, domain, import_compat = \
1821 operator.itemgetter('model', 'fields', 'ids', 'domain',
1823 simplejson.loads(data))
1825 context = req.session.eval_context(req.context)
1826 Model = req.session.model(model)
1827 ids = ids or Model.search(domain, 0, False, False, context)
1829 field_names = map(operator.itemgetter('name'), fields)
1830 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1833 columns_headers = field_names
1835 columns_headers = [val['label'].strip() for val in fields]
1838 return req.make_response(self.from_data(columns_headers, import_data),
1839 headers=[('Content-Disposition',
1840 content_disposition(self.filename(model), req)),
1841 ('Content-Type', self.content_type)],
1842 cookies={'fileToken': int(token)})
1844 class CSVExport(Export):
1845 _cp_path = '/web/export/csv'
1846 fmt = {'tag': 'csv', 'label': 'CSV'}
1849 def content_type(self):
1850 return 'text/csv;charset=utf8'
1852 def filename(self, base):
1853 return base + '.csv'
1855 def from_data(self, fields, rows):
1857 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1859 writer.writerow([name.encode('utf-8') for name in fields])
1864 if isinstance(d, basestring):
1865 d = d.replace('\n',' ').replace('\t',' ')
1867 d = d.encode('utf-8')
1868 except UnicodeError:
1870 if d is False: d = None
1872 writer.writerow(row)
1879 class ExcelExport(Export):
1880 _cp_path = '/web/export/xls'
1884 'error': None if xlwt else "XLWT required"
1888 def content_type(self):
1889 return 'application/vnd.ms-excel'
1891 def filename(self, base):
1892 return base + '.xls'
1894 def from_data(self, fields, rows):
1895 workbook = xlwt.Workbook()
1896 worksheet = workbook.add_sheet('Sheet 1')
1898 for i, fieldname in enumerate(fields):
1899 worksheet.write(0, i, fieldname)
1900 worksheet.col(i).width = 8000 # around 220 pixels
1902 style = xlwt.easyxf('align: wrap yes')
1904 for row_index, row in enumerate(rows):
1905 for cell_index, cell_value in enumerate(row):
1906 if isinstance(cell_value, basestring):
1907 cell_value = re.sub("\r", " ", cell_value)
1908 if cell_value is False: cell_value = None
1909 worksheet.write(row_index + 1, cell_index, cell_value, style)
1918 class Reports(View):
1919 _cp_path = "/web/report"
1920 POLLING_DELAY = 0.25
1922 'doc': 'application/vnd.ms-word',
1923 'html': 'text/html',
1924 'odt': 'application/vnd.oasis.opendocument.text',
1925 'pdf': 'application/pdf',
1926 'sxw': 'application/vnd.sun.xml.writer',
1927 'xls': 'application/vnd.ms-excel',
1930 @openerpweb.httprequest
1931 def index(self, req, action, token):
1932 action = simplejson.loads(action)
1934 report_srv = req.session.proxy("report")
1935 context = req.session.eval_context(
1936 nonliterals.CompoundContext(
1937 req.context or {}, action[ "context"]))
1940 report_ids = context["active_ids"]
1941 if 'report_type' in action:
1942 report_data['report_type'] = action['report_type']
1943 if 'datas' in action:
1944 if 'ids' in action['datas']:
1945 report_ids = action['datas'].pop('ids')
1946 report_data.update(action['datas'])
1948 report_id = report_srv.report(
1949 req.session._db, req.session._uid, req.session._password,
1950 action["report_name"], report_ids,
1951 report_data, context)
1953 report_struct = None
1955 report_struct = report_srv.report_get(
1956 req.session._db, req.session._uid, req.session._password, report_id)
1957 if report_struct["state"]:
1960 time.sleep(self.POLLING_DELAY)
1962 report = base64.b64decode(report_struct['result'])
1963 if report_struct.get('code') == 'zlib':
1964 report = zlib.decompress(report)
1965 report_mimetype = self.TYPES_MAPPING.get(
1966 report_struct['format'], 'octet-stream')
1967 file_name = action.get('name', 'report')
1968 if 'name' not in action:
1969 reports = req.session.model('ir.actions.report.xml')
1970 res_id = reports.search([('report_name', '=', action['report_name']),],
1971 0, False, False, context)
1973 file_name = reports.read(res_id[0], ['name'], context)['name']
1975 file_name = action['report_name']
1976 file_name = '%s.%s' % (file_name, report_struct['format'])
1978 return req.make_response(report,
1980 ('Content-Disposition', content_disposition(file_name, req)),
1981 ('Content-Type', report_mimetype),
1982 ('Content-Length', len(report))],
1983 cookies={'fileToken': int(token)})
1985 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: