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()
261 def concat_js(file_list):
262 content, checksum = concat_files(file_list, intersperse=';')
263 content = rjsmin(content)
264 return content, checksum
267 """convert FS path into web path"""
268 return '/'.join(path.split(os.path.sep))
270 def manifest_glob(req, addons, key):
272 addons = module_boot(req)
274 addons = addons.split(',')
277 manifest = openerpweb.addons_manifest.get(addon, None)
280 # ensure does not ends with /
281 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
282 globlist = manifest.get(key, [])
283 for pattern in globlist:
284 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
285 r.append((path, fs2web(path[len(addons_path):])))
288 def manifest_list(req, mods, extension):
290 path = '/web/webclient/' + extension
292 path += '?mods=' + mods
294 files = manifest_glob(req, mods, extension)
295 i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
296 req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
298 return [wp for _fp, wp in files]
300 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
302 def get_last_modified(files):
303 """ Returns the modification time of the most recently modified
306 :param list(str) files: names of files to check
307 :return: most recent modification time amongst the fileset
308 :rtype: datetime.datetime
312 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
314 return datetime.datetime(1970, 1, 1)
316 def make_conditional(req, response, last_modified=None, etag=None):
317 """ Makes the provided response conditional based upon the request,
318 and mandates revalidation from clients
320 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
321 setting ``last_modified`` and ``etag`` correctly on the response object
323 :param req: OpenERP request
324 :type req: web.common.http.WebRequest
325 :param response: Werkzeug response
326 :type response: werkzeug.wrappers.Response
327 :param datetime.datetime last_modified: last modification date of the response content
328 :param str etag: some sort of checksum of the content (deep etag)
329 :return: the response object provided
330 :rtype: werkzeug.wrappers.Response
332 response.cache_control.must_revalidate = True
333 response.cache_control.max_age = 0
335 response.last_modified = last_modified
337 response.set_etag(etag)
338 return response.make_conditional(req.httprequest)
340 def login_and_redirect(req, db, login, key, redirect_url='/'):
341 req.session.authenticate(db, login, key, {})
342 return set_cookie_and_redirect(req, redirect_url)
344 def set_cookie_and_redirect(req, redirect_url):
345 redirect = werkzeug.utils.redirect(redirect_url, 303)
346 redirect.autocorrect_location_header = False
347 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
348 redirect.set_cookie('instance0|session_id', cookie_val)
351 def eval_context_and_domain(session, context, domain=None):
352 e_context = session.eval_context(context)
353 # should we give the evaluated context as an evaluation context to the domain?
354 e_domain = session.eval_domain(domain or [])
356 return e_context, e_domain
358 def load_actions_from_ir_values(req, key, key2, models, meta):
359 context = req.session.eval_context(req.context)
360 Values = req.session.model('ir.values')
361 actions = Values.get(key, key2, models, meta, context)
363 return [(id, name, clean_action(req, action))
364 for id, name, action in actions]
366 def clean_action(req, action, do_not_eval=False):
367 action.setdefault('flags', {})
369 context = req.session.eval_context(req.context)
370 eval_ctx = req.session.evaluation_context(context)
373 # values come from the server, we can just eval them
374 if action.get('context') and isinstance(action.get('context'), basestring):
375 action['context'] = eval( action['context'], eval_ctx ) or {}
377 if action.get('domain') and isinstance(action.get('domain'), basestring):
378 action['domain'] = eval( action['domain'], eval_ctx ) or []
380 if 'context' in action:
381 action['context'] = parse_context(action['context'], req.session)
382 if 'domain' in action:
383 action['domain'] = parse_domain(action['domain'], req.session)
385 action_type = action.setdefault('type', 'ir.actions.act_window_close')
386 if action_type == 'ir.actions.act_window':
387 return fix_view_modes(action)
390 # I think generate_views,fix_view_modes should go into js ActionManager
391 def generate_views(action):
393 While the server generates a sequence called "views" computing dependencies
394 between a bunch of stuff for views coming directly from the database
395 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
396 to return custom view dictionaries generated on the fly.
398 In that case, there is no ``views`` key available on the action.
400 Since the web client relies on ``action['views']``, generate it here from
401 ``view_mode`` and ``view_id``.
403 Currently handles two different cases:
405 * no view_id, multiple view_mode
406 * single view_id, single view_mode
408 :param dict action: action descriptor dictionary to generate a views key for
410 view_id = action.get('view_id') or False
411 if isinstance(view_id, (list, tuple)):
414 # providing at least one view mode is a requirement, not an option
415 view_modes = action['view_mode'].split(',')
417 if len(view_modes) > 1:
419 raise ValueError('Non-db action dictionaries should provide '
420 'either multiple view modes or a single view '
421 'mode and an optional view id.\n\n Got view '
422 'modes %r and view id %r for action %r' % (
423 view_modes, view_id, action))
424 action['views'] = [(False, mode) for mode in view_modes]
426 action['views'] = [(view_id, view_modes[0])]
428 def fix_view_modes(action):
429 """ For historical reasons, OpenERP has weird dealings in relation to
430 view_mode and the view_type attribute (on window actions):
432 * one of the view modes is ``tree``, which stands for both list views
434 * the choice is made by checking ``view_type``, which is either
435 ``form`` for a list view or ``tree`` for an actual tree view
437 This methods simply folds the view_type into view_mode by adding a
438 new view mode ``list`` which is the result of the ``tree`` view_mode
439 in conjunction with the ``form`` view_type.
441 TODO: this should go into the doc, some kind of "peculiarities" section
443 :param dict action: an action descriptor
444 :returns: nothing, the action is modified in place
446 if not action.get('views'):
447 generate_views(action)
449 if action.pop('view_type', 'form') != 'form':
452 if 'view_mode' in action:
453 action['view_mode'] = ','.join(
454 mode if mode != 'tree' else 'list'
455 for mode in action['view_mode'].split(','))
457 [id, mode if mode != 'tree' else 'list']
458 for id, mode in action['views']
463 def parse_domain(domain, session):
464 """ Parses an arbitrary string containing a domain, transforms it
465 to either a literal domain or a :class:`nonliterals.Domain`
467 :param domain: the domain to parse, if the domain is not a string it
468 is assumed to be a literal domain and is returned as-is
469 :param session: Current OpenERP session
470 :type session: openerpweb.OpenERPSession
472 if not isinstance(domain, basestring):
475 return ast.literal_eval(domain)
478 return nonliterals.Domain(session, domain)
480 def parse_context(context, session):
481 """ Parses an arbitrary string containing a context, transforms it
482 to either a literal context or a :class:`nonliterals.Context`
484 :param context: the context to parse, if the context is not a string it
485 is assumed to be a literal domain and is returned as-is
486 :param session: Current OpenERP session
487 :type session: openerpweb.OpenERPSession
489 if not isinstance(context, basestring):
492 return ast.literal_eval(context)
494 return nonliterals.Context(session, context)
496 def _local_web_translations(trans_file):
499 with open(trans_file) as t_file:
500 po = babel.messages.pofile.read_po(t_file)
504 if x.id and x.string and "openerp-web" in x.auto_comments:
505 messages.append({'id': x.id, 'string': x.string})
508 def xml2json_from_elementtree(el, preserve_whitespaces=False):
510 Simple and straightforward XML-to-JSON converter in Python
512 http://code.google.com/p/xml2json-direct/
516 ns, name = el.tag.rsplit("}", 1)
518 res["namespace"] = ns[1:]
522 for k, v in el.items():
525 if el.text and (preserve_whitespaces or el.text.strip() != ''):
528 kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
529 if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
530 kids.append(kid.tail)
531 res["children"] = kids
534 def content_disposition(filename, req):
535 filename = filename.encode('utf8')
536 escaped = urllib2.quote(filename)
537 browser = req.httprequest.user_agent.browser
538 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
539 if browser == 'msie' and version < 9:
540 return "attachment; filename=%s" % escaped
541 elif browser == 'safari':
542 return "attachment; filename=%s" % filename
544 return "attachment; filename*=UTF-8''%s" % escaped
547 #----------------------------------------------------------
548 # OpenERP Web web Controllers
549 #----------------------------------------------------------
551 html_template = """<!DOCTYPE html>
552 <html style="height: 100%%">
554 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
555 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
556 <title>OpenERP</title>
557 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
558 <link rel="stylesheet" href="/web/static/src/css/full.css" />
561 <script type="text/javascript">
563 var s = new openerp.init(%(modules)s);
570 <script src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
572 var test = function() {
577 if (window.localStorage && false) {
578 if (! localStorage.getItem("hasShownGFramePopup")) {
580 localStorage.setItem("hasShownGFramePopup", true);
591 class Home(openerpweb.Controller):
594 @openerpweb.httprequest
595 def index(self, req, s_action=None, **kw):
596 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, None, 'js'))
597 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, None, 'css'))
599 r = html_template % {
602 'modules': simplejson.dumps(module_boot(req)),
603 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
607 @openerpweb.httprequest
608 def login(self, req, db, login, key):
609 return login_and_redirect(req, db, login, key)
611 class WebClient(openerpweb.Controller):
612 _cp_path = "/web/webclient"
614 @openerpweb.jsonrequest
615 def csslist(self, req, mods=None):
616 return manifest_list(req, mods, 'css')
618 @openerpweb.jsonrequest
619 def jslist(self, req, mods=None):
620 return manifest_list(req, mods, 'js')
622 @openerpweb.jsonrequest
623 def qweblist(self, req, mods=None):
624 return manifest_list(req, mods, 'qweb')
626 @openerpweb.httprequest
627 def css(self, req, mods=None):
628 files = list(manifest_glob(req, mods, 'css'))
629 last_modified = get_last_modified(f[0] for f in files)
630 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
631 return werkzeug.wrappers.Response(status=304)
633 file_map = dict(files)
635 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
636 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
639 """read the a css file and absolutify all relative uris"""
640 with open(f, 'rb') as fp:
641 data = fp.read().decode('utf-8')
644 web_dir = os.path.dirname(path)
648 r"""@import \1%s/""" % (web_dir,),
654 r"""url(\1%s/""" % (web_dir,),
657 return data.encode('utf-8')
659 content, checksum = concat_files((f[0] for f in files), reader)
661 return make_conditional(
662 req, req.make_response(content, [('Content-Type', 'text/css')]),
663 last_modified, checksum)
665 @openerpweb.httprequest
666 def js(self, req, mods=None):
667 files = [f[0] for f in manifest_glob(req, mods, 'js')]
668 last_modified = get_last_modified(files)
669 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
670 return werkzeug.wrappers.Response(status=304)
672 content, checksum = concat_js(files)
674 return make_conditional(
675 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
676 last_modified, checksum)
678 @openerpweb.httprequest
679 def qweb(self, req, mods=None):
680 files = [f[0] for f in manifest_glob(req, mods, 'qweb')]
681 last_modified = get_last_modified(files)
682 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
683 return werkzeug.wrappers.Response(status=304)
685 content, checksum = concat_xml(files)
687 return make_conditional(
688 req, req.make_response(content, [('Content-Type', 'text/xml')]),
689 last_modified, checksum)
691 @openerpweb.jsonrequest
692 def bootstrap_translations(self, req, mods):
693 """ Load local translations from *.po files, as a temporary solution
694 until we have established a valid session. This is meant only
695 for translating the login page and db management chrome, using
696 the browser's language. """
697 # For performance reasons we only load a single translation, so for
698 # sub-languages (that should only be partially translated) we load the
699 # main language PO instead - that should be enough for the login screen.
700 lang = req.lang.split('_')[0]
702 translations_per_module = {}
703 for addon_name in mods:
704 if openerpweb.addons_manifest[addon_name].get('bootstrap'):
705 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
706 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
707 if not os.path.exists(f_name):
709 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
711 return {"modules": translations_per_module,
712 "lang_parameters": None}
714 @openerpweb.jsonrequest
715 def translations(self, req, mods, lang):
716 res_lang = req.session.model('res.lang')
717 ids = res_lang.search([("code", "=", lang)])
720 lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
721 "grouping", "decimal_point", "thousands_sep"])
723 # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
724 # done server-side when the language is loaded, so we only need to load the user's lang.
725 ir_translation = req.session.model('ir.translation')
726 translations_per_module = {}
727 messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
728 ('comments','like','openerp-web'),('value','!=',False),
730 ['module','src','value','lang'], order='module')
731 for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
732 translations_per_module.setdefault(mod,{'messages':[]})
733 translations_per_module[mod]['messages'].extend({'id': m['src'],
734 'string': m['value']} \
736 return {"modules": translations_per_module,
737 "lang_parameters": lang_params}
739 @openerpweb.jsonrequest
740 def version_info(self, req):
742 "version": openerp.release.version
745 class Proxy(openerpweb.Controller):
746 _cp_path = '/web/proxy'
748 @openerpweb.jsonrequest
749 def load(self, req, path):
750 """ Proxies an HTTP request through a JSON request.
752 It is strongly recommended to not request binary files through this,
753 as the result will be a binary data blob as well.
755 :param req: OpenERP request
756 :param path: actual request path
757 :return: file content
759 from werkzeug.test import Client
760 from werkzeug.wrappers import BaseResponse
762 return Client(req.httprequest.app, BaseResponse).get(path).data
764 class Database(openerpweb.Controller):
765 _cp_path = "/web/database"
767 @openerpweb.jsonrequest
768 def get_list(self, req):
771 @openerpweb.jsonrequest
772 def create(self, req, fields):
773 params = dict(map(operator.itemgetter('name', 'value'), fields))
774 return req.session.proxy("db").create_database(
775 params['super_admin_pwd'],
777 bool(params.get('demo_data')),
779 params['create_admin_pwd'])
781 @openerpweb.jsonrequest
782 def duplicate(self, req, fields):
783 params = dict(map(operator.itemgetter('name', 'value'), fields))
784 return req.session.proxy("db").duplicate_database(
785 params['super_admin_pwd'],
786 params['db_original_name'],
789 @openerpweb.jsonrequest
790 def drop(self, req, fields):
791 password, db = operator.itemgetter(
792 'drop_pwd', 'drop_db')(
793 dict(map(operator.itemgetter('name', 'value'), fields)))
796 return req.session.proxy("db").drop(password, db)
797 except xmlrpclib.Fault, e:
798 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
799 return {'error': e.faultCode, 'title': 'Drop Database'}
800 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
802 @openerpweb.httprequest
803 def backup(self, req, backup_db, backup_pwd, token):
805 db_dump = base64.b64decode(
806 req.session.proxy("db").dump(backup_pwd, backup_db))
807 filename = "%(db)s_%(timestamp)s.dump" % {
809 'timestamp': datetime.datetime.utcnow().strftime(
810 "%Y-%m-%d_%H-%M-%SZ")
812 return req.make_response(db_dump,
813 [('Content-Type', 'application/octet-stream; charset=binary'),
814 ('Content-Disposition', content_disposition(filename, req))],
815 {'fileToken': int(token)}
817 except xmlrpclib.Fault, e:
818 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
820 @openerpweb.httprequest
821 def restore(self, req, db_file, restore_pwd, new_db):
823 data = base64.b64encode(db_file.read())
824 req.session.proxy("db").restore(restore_pwd, new_db, data)
826 except xmlrpclib.Fault, e:
827 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
828 raise Exception("AccessDenied")
830 @openerpweb.jsonrequest
831 def change_password(self, req, fields):
832 old_password, new_password = operator.itemgetter(
833 'old_pwd', 'new_pwd')(
834 dict(map(operator.itemgetter('name', 'value'), fields)))
836 return req.session.proxy("db").change_admin_password(old_password, new_password)
837 except xmlrpclib.Fault, e:
838 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
839 return {'error': e.faultCode, 'title': 'Change Password'}
840 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
842 class Session(openerpweb.Controller):
843 _cp_path = "/web/session"
845 def session_info(self, req):
846 req.session.ensure_valid()
848 "session_id": req.session_id,
849 "uid": req.session._uid,
850 "context": req.session.get_context() if req.session._uid else {},
851 "db": req.session._db,
852 "login": req.session._login,
855 @openerpweb.jsonrequest
856 def get_session_info(self, req):
857 return self.session_info(req)
859 @openerpweb.jsonrequest
860 def authenticate(self, req, db, login, password, base_location=None):
861 wsgienv = req.httprequest.environ
863 base_location=base_location,
864 HTTP_HOST=wsgienv['HTTP_HOST'],
865 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
867 req.session.authenticate(db, login, password, env)
869 return self.session_info(req)
871 @openerpweb.jsonrequest
872 def change_password (self,req,fields):
873 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
874 dict(map(operator.itemgetter('name', 'value'), fields)))
875 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
876 return {'error':'All passwords have to be filled.','title': 'Change Password'}
877 if new_password != confirm_password:
878 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
880 if req.session.model('res.users').change_password(
881 old_password, new_password):
882 return {'new_password':new_password}
884 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
885 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
887 @openerpweb.jsonrequest
888 def sc_list(self, req):
889 return req.session.model('ir.ui.view_sc').get_sc(
890 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
892 @openerpweb.jsonrequest
893 def get_lang_list(self, req):
896 'lang_list': (req.session.proxy("db").list_lang() or []),
900 return {"error": e, "title": "Languages"}
902 @openerpweb.jsonrequest
903 def modules(self, req):
904 # return all installed modules. Web client is smart enough to not load a module twice
905 return module_installed(req)
907 @openerpweb.jsonrequest
908 def eval_domain_and_context(self, req, contexts, domains,
910 """ Evaluates sequences of domains and contexts, composing them into
911 a single context, domain or group_by sequence.
913 :param list contexts: list of contexts to merge together. Contexts are
914 evaluated in sequence, all previous contexts
915 are part of their own evaluation context
916 (starting at the session context).
917 :param list domains: list of domains to merge together. Domains are
918 evaluated in sequence and appended to one another
919 (implicit AND), their evaluation domain is the
920 result of merging all contexts.
921 :param list group_by_seq: list of domains (which may be in a different
922 order than the ``contexts`` parameter),
923 evaluated in sequence, their ``'group_by'``
924 key is extracted if they have one.
929 the global context created by merging all of
933 the concatenation of all domains
936 a list of fields to group by, potentially empty (in which case
937 no group by should be performed)
939 context, domain = eval_context_and_domain(req.session,
940 nonliterals.CompoundContext(*(contexts or [])),
941 nonliterals.CompoundDomain(*(domains or [])))
943 group_by_sequence = []
944 for candidate in (group_by_seq or []):
945 ctx = req.session.eval_context(candidate, context)
946 group_by = ctx.get('group_by')
949 elif isinstance(group_by, basestring):
950 group_by_sequence.append(group_by)
952 group_by_sequence.extend(group_by)
957 'group_by': group_by_sequence
960 @openerpweb.jsonrequest
961 def save_session_action(self, req, the_action):
963 This method store an action object in the session object and returns an integer
964 identifying that action. The method get_session_action() can be used to get
967 :param the_action: The action to save in the session.
968 :type the_action: anything
969 :return: A key identifying the saved action.
972 saved_actions = req.httpsession.get('saved_actions')
973 if not saved_actions:
974 saved_actions = {"next":0, "actions":{}}
975 req.httpsession['saved_actions'] = saved_actions
976 # we don't allow more than 10 stored actions
977 if len(saved_actions["actions"]) >= 10:
978 del saved_actions["actions"][min(saved_actions["actions"])]
979 key = saved_actions["next"]
980 saved_actions["actions"][key] = the_action
981 saved_actions["next"] = key + 1
984 @openerpweb.jsonrequest
985 def get_session_action(self, req, key):
987 Gets back a previously saved action. This method can return None if the action
988 was saved since too much time (this case should be handled in a smart way).
990 :param key: The key given by save_session_action()
992 :return: The saved action or None.
995 saved_actions = req.httpsession.get('saved_actions')
996 if not saved_actions:
998 return saved_actions["actions"].get(key)
1000 @openerpweb.jsonrequest
1001 def check(self, req):
1002 req.session.assert_valid()
1005 @openerpweb.jsonrequest
1006 def destroy(self, req):
1007 req.session._suicide = True
1009 class Menu(openerpweb.Controller):
1010 _cp_path = "/web/menu"
1012 @openerpweb.jsonrequest
1013 def load(self, req):
1014 return {'data': self.do_load(req)}
1016 @openerpweb.jsonrequest
1017 def get_user_roots(self, req):
1018 return self.do_get_user_roots(req)
1020 def do_get_user_roots(self, req):
1021 """ Return all root menu ids visible for the session user.
1023 :param req: A request object, with an OpenERP session attribute
1024 :type req: < session -> OpenERPSession >
1025 :return: the root menu ids
1029 context = s.eval_context(req.context)
1030 Menus = s.model('ir.ui.menu')
1031 # If a menu action is defined use its domain to get the root menu items
1032 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1034 menu_domain = [('parent_id', '=', False)]
1036 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1038 menu_domain = ast.literal_eval(domain_string)
1040 return Menus.search(menu_domain, 0, False, False, context)
1042 def do_load(self, req):
1043 """ Loads all menu items (all applications and their sub-menus).
1045 :param req: A request object, with an OpenERP session attribute
1046 :type req: < session -> OpenERPSession >
1047 :return: the menu root
1048 :rtype: dict('children': menu_nodes)
1050 context = req.session.eval_context(req.context)
1051 Menus = req.session.model('ir.ui.menu')
1053 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1054 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1056 # menus are loaded fully unlike a regular tree view, cause there are a
1057 # limited number of items (752 when all 6.1 addons are installed)
1058 menu_ids = Menus.search([], 0, False, False, context)
1059 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1060 # adds roots at the end of the sequence, so that they will overwrite
1061 # equivalent menu items from full menu read when put into id:item
1062 # mapping, resulting in children being correctly set on the roots.
1063 menu_items.extend(menu_roots)
1065 # make a tree using parent_id
1066 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1067 for menu_item in menu_items:
1068 if menu_item['parent_id']:
1069 parent = menu_item['parent_id'][0]
1072 if parent in menu_items_map:
1073 menu_items_map[parent].setdefault(
1074 'children', []).append(menu_item)
1076 # sort by sequence a tree using parent_id
1077 for menu_item in menu_items:
1078 menu_item.setdefault('children', []).sort(
1079 key=operator.itemgetter('sequence'))
1083 @openerpweb.jsonrequest
1084 def action(self, req, menu_id):
1085 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1086 [('ir.ui.menu', menu_id)], False)
1087 return {"action": actions}
1089 class DataSet(openerpweb.Controller):
1090 _cp_path = "/web/dataset"
1092 @openerpweb.jsonrequest
1093 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1094 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1095 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1097 """ Performs a search() followed by a read() (if needed) using the
1098 provided search criteria
1100 :param req: a JSON-RPC request object
1101 :type req: openerpweb.JsonRequest
1102 :param str model: the name of the model to search on
1103 :param fields: a list of the fields to return in the result records
1105 :param int offset: from which index should the results start being returned
1106 :param int limit: the maximum number of records to return
1107 :param list domain: the search domain for the query
1108 :param list sort: sorting directives
1109 :returns: A structure (dict) with two keys: ids (all the ids matching
1110 the (domain, context) pair) and records (paginated records
1111 matching fields selection set)
1114 Model = req.session.model(model)
1116 context, domain = eval_context_and_domain(
1117 req.session, req.context, domain)
1119 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1120 if limit and len(ids) == limit:
1121 length = Model.search_count(domain, context)
1123 length = len(ids) + (offset or 0)
1124 if fields and fields == ['id']:
1125 # shortcut read if we only want the ids
1128 'records': [{'id': id} for id in ids]
1131 records = Model.read(ids, fields or False, context)
1132 records.sort(key=lambda obj: ids.index(obj['id']))
1138 @openerpweb.jsonrequest
1139 def load(self, req, model, id, fields):
1140 m = req.session.model(model)
1142 r = m.read([id], False, req.session.eval_context(req.context))
1145 return {'value': value}
1147 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1148 has_domain = domain_id is not None and domain_id < len(args)
1149 has_context = context_id is not None and context_id < len(args)
1151 domain = args[domain_id] if has_domain else []
1152 context = args[context_id] if has_context else {}
1153 c, d = eval_context_and_domain(req.session, context, domain)
1157 args[context_id] = c
1159 return self._call_kw(req, model, method, args, {})
1161 def _call_kw(self, req, model, method, args, kwargs):
1162 for i in xrange(len(args)):
1163 if isinstance(args[i], nonliterals.BaseContext):
1164 args[i] = req.session.eval_context(args[i])
1165 elif isinstance(args[i], nonliterals.BaseDomain):
1166 args[i] = req.session.eval_domain(args[i])
1167 for k in kwargs.keys():
1168 if isinstance(kwargs[k], nonliterals.BaseContext):
1169 kwargs[k] = req.session.eval_context(kwargs[k])
1170 elif isinstance(kwargs[k], nonliterals.BaseDomain):
1171 kwargs[k] = req.session.eval_domain(kwargs[k])
1173 # Temporary implements future display_name special field for model#read()
1174 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1175 if 'display_name' in args[1]:
1176 names = req.session.model(model).name_get(args[0], **kwargs)
1177 args[1].remove('display_name')
1178 r = getattr(req.session.model(model), method)(*args, **kwargs)
1179 for i in range(len(r)):
1180 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1183 return getattr(req.session.model(model), method)(*args, **kwargs)
1185 @openerpweb.jsonrequest
1186 def onchange(self, req, model, method, args, context_id=None):
1187 """ Support method for handling onchange calls: behaves much like call
1188 with the following differences:
1190 * Does not take a domain_id
1191 * Is aware of the return value's structure, and will parse the domains
1192 if needed in order to return either parsed literal domains (in JSON)
1193 or non-literal domain instances, allowing those domains to be used
1197 :type req: web.common.http.JsonRequest
1198 :param str model: object type on which to call the method
1199 :param str method: name of the onchange handler method
1200 :param list args: arguments to call the onchange handler with
1201 :param int context_id: index of the context object in the list of
1203 :return: result of the onchange call with all domains parsed
1205 result = self.call_common(req, model, method, args, context_id=context_id)
1206 if not result or 'domain' not in result:
1209 result['domain'] = dict(
1210 (k, parse_domain(v, req.session))
1211 for k, v in result['domain'].iteritems())
1215 @openerpweb.jsonrequest
1216 def call(self, req, model, method, args, domain_id=None, context_id=None):
1217 return self.call_common(req, model, method, args, domain_id, context_id)
1219 @openerpweb.jsonrequest
1220 def call_kw(self, req, model, method, args, kwargs):
1221 return self._call_kw(req, model, method, args, kwargs)
1223 @openerpweb.jsonrequest
1224 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1225 action = self.call_common(req, model, method, args, domain_id, context_id)
1226 if isinstance(action, dict) and action.get('type') != '':
1227 return clean_action(req, action)
1230 @openerpweb.jsonrequest
1231 def exec_workflow(self, req, model, id, signal):
1232 return req.session.exec_workflow(model, id, signal)
1234 @openerpweb.jsonrequest
1235 def resequence(self, req, model, ids, field='sequence', offset=0):
1236 """ Re-sequences a number of records in the model, by their ids
1238 The re-sequencing starts at the first model of ``ids``, the sequence
1239 number is incremented by one after each record and starts at ``offset``
1241 :param ids: identifiers of the records to resequence, in the new sequence order
1243 :param str field: field used for sequence specification, defaults to
1245 :param int offset: sequence number for first record in ``ids``, allows
1246 starting the resequencing from an arbitrary number,
1249 m = req.session.model(model)
1250 if not m.fields_get([field]):
1252 # python 2.6 has no start parameter
1253 for i, id in enumerate(ids):
1254 m.write(id, { field: i + offset })
1257 class View(openerpweb.Controller):
1258 _cp_path = "/web/view"
1260 def fields_view_get(self, req, model, view_id, view_type,
1261 transform=True, toolbar=False, submenu=False):
1262 Model = req.session.model(model)
1263 context = req.session.eval_context(req.context)
1264 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1265 # todo fme?: check that we should pass the evaluated context here
1266 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1267 if toolbar and transform:
1268 self.process_toolbar(req, fvg['toolbar'])
1271 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1272 # depending on how it feels, xmlrpclib.ServerProxy can translate
1273 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1274 # enjoy unicode strings which can not be trivially converted to
1275 # strings, and it blows up during parsing.
1277 # So ensure we fix this retardation by converting view xml back to
1279 if isinstance(fvg['arch'], unicode):
1280 arch = fvg['arch'].encode('utf-8')
1283 fvg['arch_string'] = arch
1286 evaluation_context = session.evaluation_context(context or {})
1287 xml = self.transform_view(arch, session, evaluation_context)
1289 xml = ElementTree.fromstring(arch)
1290 fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
1292 if 'id' in fvg['fields']:
1293 # Special case for id's
1294 id_field = fvg['fields']['id']
1295 id_field['original_type'] = id_field['type']
1296 id_field['type'] = 'id'
1298 for field in fvg['fields'].itervalues():
1299 if field.get('views'):
1300 for view in field["views"].itervalues():
1301 self.process_view(session, view, None, transform)
1302 if field.get('domain'):
1303 field["domain"] = parse_domain(field["domain"], session)
1304 if field.get('context'):
1305 field["context"] = parse_context(field["context"], session)
1307 def process_toolbar(self, req, toolbar):
1309 The toolbar is a mapping of section_key: [action_descriptor]
1311 We need to clean all those actions in order to ensure correct
1314 for actions in toolbar.itervalues():
1315 for action in actions:
1316 if 'context' in action:
1317 action['context'] = parse_context(
1318 action['context'], req.session)
1319 if 'domain' in action:
1320 action['domain'] = parse_domain(
1321 action['domain'], req.session)
1323 @openerpweb.jsonrequest
1324 def add_custom(self, req, view_id, arch):
1325 CustomView = req.session.model('ir.ui.view.custom')
1327 'user_id': req.session._uid,
1330 }, req.session.eval_context(req.context))
1331 return {'result': True}
1333 @openerpweb.jsonrequest
1334 def undo_custom(self, req, view_id, reset=False):
1335 CustomView = req.session.model('ir.ui.view.custom')
1336 context = req.session.eval_context(req.context)
1337 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1338 0, False, False, context)
1341 CustomView.unlink(vcustom, context)
1343 CustomView.unlink([vcustom[0]], context)
1344 return {'result': True}
1345 return {'result': False}
1347 def transform_view(self, view_string, session, context=None):
1348 # transform nodes on the fly via iterparse, instead of
1349 # doing it statically on the parsing result
1350 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1352 for event, elem in parser:
1353 if event == "start":
1356 self.parse_domains_and_contexts(elem, session)
1359 def parse_domains_and_contexts(self, elem, session):
1360 """ Converts domains and contexts from the view into Python objects,
1361 either literals if they can be parsed by literal_eval or a special
1362 placeholder object if the domain or context refers to free variables.
1364 :param elem: the current node being parsed
1365 :type param: xml.etree.ElementTree.Element
1366 :param session: OpenERP session object, used to store and retrieve
1368 :type session: openerpweb.openerpweb.OpenERPSession
1370 for el in ['domain', 'filter_domain']:
1371 domain = elem.get(el, '').strip()
1373 elem.set(el, parse_domain(domain, session))
1374 elem.set(el + '_string', domain)
1375 for el in ['context', 'default_get']:
1376 context_string = elem.get(el, '').strip()
1378 elem.set(el, parse_context(context_string, session))
1379 elem.set(el + '_string', context_string)
1381 @openerpweb.jsonrequest
1382 def load(self, req, model, view_id, view_type, toolbar=False):
1383 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1385 class TreeView(View):
1386 _cp_path = "/web/treeview"
1388 @openerpweb.jsonrequest
1389 def action(self, req, model, id):
1390 return load_actions_from_ir_values(
1391 req,'action', 'tree_but_open',[(model, id)],
1394 class SearchView(View):
1395 _cp_path = "/web/searchview"
1397 @openerpweb.jsonrequest
1398 def load(self, req, model, view_id):
1399 fields_view = self.fields_view_get(req, model, view_id, 'search')
1400 return {'fields_view': fields_view}
1402 @openerpweb.jsonrequest
1403 def fields_get(self, req, model):
1404 Model = req.session.model(model)
1405 fields = Model.fields_get(False, req.session.eval_context(req.context))
1406 for field in fields.values():
1407 # shouldn't convert the views too?
1408 if field.get('domain'):
1409 field["domain"] = parse_domain(field["domain"], req.session)
1410 if field.get('context'):
1411 field["context"] = parse_context(field["context"], req.session)
1412 return {'fields': fields}
1414 @openerpweb.jsonrequest
1415 def get_filters(self, req, model):
1416 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1417 Model = req.session.model("ir.filters")
1418 filters = Model.get_filters(model)
1419 for filter in filters:
1421 parsed_context = parse_context(filter["context"], req.session)
1422 filter["context"] = (parsed_context
1423 if not isinstance(parsed_context, nonliterals.BaseContext)
1424 else req.session.eval_context(parsed_context))
1426 parsed_domain = parse_domain(filter["domain"], req.session)
1427 filter["domain"] = (parsed_domain
1428 if not isinstance(parsed_domain, nonliterals.BaseDomain)
1429 else req.session.eval_domain(parsed_domain))
1431 logger.exception("Failed to parse custom filter %s in %s",
1432 filter['name'], model)
1433 filter['disabled'] = True
1434 del filter['context']
1435 del filter['domain']
1438 class Binary(openerpweb.Controller):
1439 _cp_path = "/web/binary"
1441 @openerpweb.httprequest
1442 def image(self, req, model, id, field, **kw):
1443 last_update = '__last_update'
1444 Model = req.session.model(model)
1445 context = req.session.eval_context(req.context)
1446 headers = [('Content-Type', 'image/png')]
1447 etag = req.httprequest.headers.get('If-None-Match')
1448 hashed_session = hashlib.md5(req.session_id).hexdigest()
1449 id = None if not id else simplejson.loads(id)
1450 if type(id) is list:
1453 if not id and hashed_session == etag:
1454 return werkzeug.wrappers.Response(status=304)
1456 date = Model.read([id], [last_update], context)[0].get(last_update)
1457 if hashlib.md5(date).hexdigest() == etag:
1458 return werkzeug.wrappers.Response(status=304)
1460 retag = hashed_session
1463 res = Model.default_get([field], context).get(field)
1466 res = Model.read([id], [last_update, field], context)[0]
1467 retag = hashlib.md5(res.get(last_update)).hexdigest()
1468 image_base64 = res.get(field)
1470 if kw.get('resize'):
1471 resize = kw.get('resize').split(',');
1472 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1473 width = int(resize[0])
1474 height = int(resize[1])
1475 # resize maximum 500*500
1476 if width > 500: width = 500
1477 if height > 500: height = 500
1478 image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1480 image_data = base64.b64decode(image_base64)
1482 except (TypeError, xmlrpclib.Fault):
1483 image_data = self.placeholder(req)
1484 headers.append(('ETag', retag))
1485 headers.append(('Content-Length', len(image_data)))
1487 ncache = int(kw.get('cache'))
1488 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1491 return req.make_response(image_data, headers)
1492 def placeholder(self, req):
1493 addons_path = openerpweb.addons_manifest['web']['addons_path']
1494 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1496 @openerpweb.httprequest
1497 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1498 """ Download link for files stored as binary fields.
1500 If the ``id`` parameter is omitted, fetches the default value for the
1501 binary field (via ``default_get``), otherwise fetches the field for
1502 that precise record.
1504 :param req: OpenERP request
1505 :type req: :class:`web.common.http.HttpRequest`
1506 :param str model: name of the model to fetch the binary from
1507 :param str field: binary field
1508 :param str id: id of the record from which to fetch the binary
1509 :param str filename_field: field holding the file's name, if any
1510 :returns: :class:`werkzeug.wrappers.Response`
1512 Model = req.session.model(model)
1513 context = req.session.eval_context(req.context)
1516 fields.append(filename_field)
1518 res = Model.read([int(id)], fields, context)[0]
1520 res = Model.default_get(fields, context)
1521 filecontent = base64.b64decode(res.get(field, ''))
1523 return req.not_found()
1525 filename = '%s_%s' % (model.replace('.', '_'), id)
1527 filename = res.get(filename_field, '') or filename
1528 return req.make_response(filecontent,
1529 [('Content-Type', 'application/octet-stream'),
1530 ('Content-Disposition', content_disposition(filename, req))])
1532 @openerpweb.httprequest
1533 def saveas_ajax(self, req, data, token):
1534 jdata = simplejson.loads(data)
1535 model = jdata['model']
1536 field = jdata['field']
1537 id = jdata.get('id', None)
1538 filename_field = jdata.get('filename_field', None)
1539 context = jdata.get('context', dict())
1541 context = req.session.eval_context(context)
1542 Model = req.session.model(model)
1545 fields.append(filename_field)
1547 res = Model.read([int(id)], fields, context)[0]
1549 res = Model.default_get(fields, context)
1550 filecontent = base64.b64decode(res.get(field, ''))
1552 raise ValueError("No content found for field '%s' on '%s:%s'" %
1555 filename = '%s_%s' % (model.replace('.', '_'), id)
1557 filename = res.get(filename_field, '') or filename
1558 return req.make_response(filecontent,
1559 headers=[('Content-Type', 'application/octet-stream'),
1560 ('Content-Disposition', content_disposition(filename, req))],
1561 cookies={'fileToken': int(token)})
1563 @openerpweb.httprequest
1564 def upload(self, req, callback, ufile):
1565 # TODO: might be useful to have a configuration flag for max-length file uploads
1567 out = """<script language="javascript" type="text/javascript">
1568 var win = window.top.window;
1569 win.jQuery(win).trigger(%s, %s);
1572 args = [len(data), ufile.filename,
1573 ufile.content_type, base64.b64encode(data)]
1574 except Exception, e:
1575 args = [False, e.message]
1576 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1578 @openerpweb.httprequest
1579 def upload_attachment(self, req, callback, model, id, ufile):
1580 context = req.session.eval_context(req.context)
1581 Model = req.session.model('ir.attachment')
1583 out = """<script language="javascript" type="text/javascript">
1584 var win = window.top.window;
1585 win.jQuery(win).trigger(%s, %s);
1587 attachment_id = Model.create({
1588 'name': ufile.filename,
1589 'datas': base64.encodestring(ufile.read()),
1590 'datas_fname': ufile.filename,
1595 'filename': ufile.filename,
1598 except Exception, e:
1599 args = { 'error': e.message }
1600 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1602 class Action(openerpweb.Controller):
1603 _cp_path = "/web/action"
1605 @openerpweb.jsonrequest
1606 def load(self, req, action_id, do_not_eval=False):
1607 Actions = req.session.model('ir.actions.actions')
1609 context = req.session.eval_context(req.context)
1612 action_id = int(action_id)
1615 module, xmlid = action_id.split('.', 1)
1616 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1617 assert model.startswith('ir.actions.')
1619 action_id = 0 # force failed read
1621 base_action = Actions.read([action_id], ['type'], context)
1624 action_type = base_action[0]['type']
1625 if action_type == 'ir.actions.report.xml':
1626 ctx.update({'bin_size': True})
1628 action = req.session.model(action_type).read([action_id], False, ctx)
1630 value = clean_action(req, action[0], do_not_eval)
1633 @openerpweb.jsonrequest
1634 def run(self, req, action_id):
1635 return_action = req.session.model('ir.actions.server').run(
1636 [action_id], req.session.eval_context(req.context))
1638 return clean_action(req, return_action)
1643 _cp_path = "/web/export"
1645 @openerpweb.jsonrequest
1646 def formats(self, req):
1647 """ Returns all valid export formats
1649 :returns: for each export format, a pair of identifier and printable name
1650 :rtype: [(str, str)]
1654 for path, controller in openerpweb.controllers_path.iteritems()
1655 if path.startswith(self._cp_path)
1656 if hasattr(controller, 'fmt')
1657 ], key=operator.itemgetter("label"))
1659 def fields_get(self, req, model):
1660 Model = req.session.model(model)
1661 fields = Model.fields_get(False, req.session.eval_context(req.context))
1664 @openerpweb.jsonrequest
1665 def get_fields(self, req, model, prefix='', parent_name= '',
1666 import_compat=True, parent_field_type=None,
1669 if import_compat and parent_field_type == "many2one":
1672 fields = self.fields_get(req, model)
1675 fields.pop('id', None)
1677 fields['.id'] = fields.pop('id', {'string': 'ID'})
1679 fields_sequence = sorted(fields.iteritems(),
1680 key=lambda field: field[1].get('string', ''))
1683 for field_name, field in fields_sequence:
1685 if exclude and field_name in exclude:
1687 if field.get('readonly'):
1688 # If none of the field's states unsets readonly, skip the field
1689 if all(dict(attrs).get('readonly', True)
1690 for attrs in field.get('states', {}).values()):
1693 id = prefix + (prefix and '/'or '') + field_name
1694 name = parent_name + (parent_name and '/' or '') + field['string']
1695 record = {'id': id, 'string': name,
1696 'value': id, 'children': False,
1697 'field_type': field.get('type'),
1698 'required': field.get('required'),
1699 'relation_field': field.get('relation_field')}
1700 records.append(record)
1702 if len(name.split('/')) < 3 and 'relation' in field:
1703 ref = field.pop('relation')
1704 record['value'] += '/id'
1705 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1707 if not import_compat or field['type'] == 'one2many':
1708 # m2m field in import_compat is childless
1709 record['children'] = True
1713 @openerpweb.jsonrequest
1714 def namelist(self,req, model, export_id):
1715 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1716 export = req.session.model("ir.exports").read([export_id])[0]
1717 export_fields_list = req.session.model("ir.exports.line").read(
1718 export['export_fields'])
1720 fields_data = self.fields_info(
1721 req, model, map(operator.itemgetter('name'), export_fields_list))
1724 {'name': field['name'], 'label': fields_data[field['name']]}
1725 for field in export_fields_list
1728 def fields_info(self, req, model, export_fields):
1730 fields = self.fields_get(req, model)
1732 # To make fields retrieval more efficient, fetch all sub-fields of a
1733 # given field at the same time. Because the order in the export list is
1734 # arbitrary, this requires ordering all sub-fields of a given field
1735 # together so they can be fetched at the same time
1737 # Works the following way:
1738 # * sort the list of fields to export, the default sorting order will
1739 # put the field itself (if present, for xmlid) and all of its
1740 # sub-fields right after it
1741 # * then, group on: the first field of the path (which is the same for
1742 # a field and for its subfields and the length of splitting on the
1743 # first '/', which basically means grouping the field on one side and
1744 # all of the subfields on the other. This way, we have the field (for
1745 # the xmlid) with length 1, and all of the subfields with the same
1746 # base but a length "flag" of 2
1747 # * if we have a normal field (length 1), just add it to the info
1748 # mapping (with its string) as-is
1749 # * otherwise, recursively call fields_info via graft_subfields.
1750 # all graft_subfields does is take the result of fields_info (on the
1751 # field's model) and prepend the current base (current field), which
1752 # rebuilds the whole sub-tree for the field
1754 # result: because we're not fetching the fields_get for half the
1755 # database models, fetching a namelist with a dozen fields (including
1756 # relational data) falls from ~6s to ~300ms (on the leads model).
1757 # export lists with no sub-fields (e.g. import_compatible lists with
1758 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1759 # there's a single fields_get to execute)
1760 for (base, length), subfields in itertools.groupby(
1761 sorted(export_fields),
1762 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1763 subfields = list(subfields)
1765 # subfields is a seq of $base/*rest, and not loaded yet
1766 info.update(self.graft_subfields(
1767 req, fields[base]['relation'], base, fields[base]['string'],
1771 info[base] = fields[base]['string']
1775 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1776 export_fields = [field.split('/', 1)[1] for field in fields]
1778 (prefix + '/' + k, prefix_string + '/' + v)
1779 for k, v in self.fields_info(req, model, export_fields).iteritems())
1781 #noinspection PyPropertyDefinition
1783 def content_type(self):
1784 """ Provides the format's content type """
1785 raise NotImplementedError()
1787 def filename(self, base):
1788 """ Creates a valid filename for the format (with extension) from the
1789 provided base name (exension-less)
1791 raise NotImplementedError()
1793 def from_data(self, fields, rows):
1794 """ Conversion method from OpenERP's export data to whatever the
1795 current export class outputs
1797 :params list fields: a list of fields to export
1798 :params list rows: a list of records to export
1802 raise NotImplementedError()
1804 @openerpweb.httprequest
1805 def index(self, req, data, token):
1806 model, fields, ids, domain, import_compat = \
1807 operator.itemgetter('model', 'fields', 'ids', 'domain',
1809 simplejson.loads(data))
1811 context = req.session.eval_context(req.context)
1812 Model = req.session.model(model)
1813 ids = ids or Model.search(domain, 0, False, False, context)
1815 field_names = map(operator.itemgetter('name'), fields)
1816 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1819 columns_headers = field_names
1821 columns_headers = [val['label'].strip() for val in fields]
1824 return req.make_response(self.from_data(columns_headers, import_data),
1825 headers=[('Content-Disposition',
1826 content_disposition(self.filename(model), req)),
1827 ('Content-Type', self.content_type)],
1828 cookies={'fileToken': int(token)})
1830 class CSVExport(Export):
1831 _cp_path = '/web/export/csv'
1832 fmt = {'tag': 'csv', 'label': 'CSV'}
1835 def content_type(self):
1836 return 'text/csv;charset=utf8'
1838 def filename(self, base):
1839 return base + '.csv'
1841 def from_data(self, fields, rows):
1843 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1845 writer.writerow([name.encode('utf-8') for name in fields])
1850 if isinstance(d, basestring):
1851 d = d.replace('\n',' ').replace('\t',' ')
1853 d = d.encode('utf-8')
1854 except UnicodeError:
1856 if d is False: d = None
1858 writer.writerow(row)
1865 class ExcelExport(Export):
1866 _cp_path = '/web/export/xls'
1870 'error': None if xlwt else "XLWT required"
1874 def content_type(self):
1875 return 'application/vnd.ms-excel'
1877 def filename(self, base):
1878 return base + '.xls'
1880 def from_data(self, fields, rows):
1881 workbook = xlwt.Workbook()
1882 worksheet = workbook.add_sheet('Sheet 1')
1884 for i, fieldname in enumerate(fields):
1885 worksheet.write(0, i, fieldname)
1886 worksheet.col(i).width = 8000 # around 220 pixels
1888 style = xlwt.easyxf('align: wrap yes')
1890 for row_index, row in enumerate(rows):
1891 for cell_index, cell_value in enumerate(row):
1892 if isinstance(cell_value, basestring):
1893 cell_value = re.sub("\r", " ", cell_value)
1894 if cell_value is False: cell_value = None
1895 worksheet.write(row_index + 1, cell_index, cell_value, style)
1904 class Reports(View):
1905 _cp_path = "/web/report"
1906 POLLING_DELAY = 0.25
1908 'doc': 'application/vnd.ms-word',
1909 'html': 'text/html',
1910 'odt': 'application/vnd.oasis.opendocument.text',
1911 'pdf': 'application/pdf',
1912 'sxw': 'application/vnd.sun.xml.writer',
1913 'xls': 'application/vnd.ms-excel',
1916 @openerpweb.httprequest
1917 def index(self, req, action, token):
1918 action = simplejson.loads(action)
1920 report_srv = req.session.proxy("report")
1921 context = req.session.eval_context(
1922 nonliterals.CompoundContext(
1923 req.context or {}, action[ "context"]))
1926 report_ids = context["active_ids"]
1927 if 'report_type' in action:
1928 report_data['report_type'] = action['report_type']
1929 if 'datas' in action:
1930 if 'ids' in action['datas']:
1931 report_ids = action['datas'].pop('ids')
1932 report_data.update(action['datas'])
1934 report_id = report_srv.report(
1935 req.session._db, req.session._uid, req.session._password,
1936 action["report_name"], report_ids,
1937 report_data, context)
1939 report_struct = None
1941 report_struct = report_srv.report_get(
1942 req.session._db, req.session._uid, req.session._password, report_id)
1943 if report_struct["state"]:
1946 time.sleep(self.POLLING_DELAY)
1948 report = base64.b64decode(report_struct['result'])
1949 if report_struct.get('code') == 'zlib':
1950 report = zlib.decompress(report)
1951 report_mimetype = self.TYPES_MAPPING.get(
1952 report_struct['format'], 'octet-stream')
1953 file_name = action.get('name', 'report')
1954 if 'name' not in action:
1955 reports = req.session.model('ir.actions.report.xml')
1956 res_id = reports.search([('report_name', '=', action['report_name']),],
1957 0, False, False, context)
1959 file_name = reports.read(res_id[0], ['name'], context)['name']
1961 file_name = action['report_name']
1962 file_name = '%s.%s' % (file_name, report_struct['format'])
1964 return req.make_response(report,
1966 ('Content-Disposition', content_disposition(file_name, req)),
1967 ('Content-Type', report_mimetype),
1968 ('Content-Length', len(report))],
1969 cookies={'fileToken': int(token)})
1971 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: