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
266 def manifest_glob(req, addons, key):
268 addons = module_boot(req)
270 addons = addons.split(',')
273 manifest = openerpweb.addons_manifest.get(addon, None)
276 # ensure does not ends with /
277 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
278 globlist = manifest.get(key, [])
279 for pattern in globlist:
280 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
281 r.append((path, path[len(addons_path):]))
284 def manifest_list(req, mods, extension):
286 path = '/web/webclient/' + extension
288 path += '?mods=' + mods
290 files = manifest_glob(req, mods, extension)
291 i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
292 req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
294 return [wp for _fp, wp in files]
296 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
298 def get_last_modified(files):
299 """ Returns the modification time of the most recently modified
302 :param list(str) files: names of files to check
303 :return: most recent modification time amongst the fileset
304 :rtype: datetime.datetime
308 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
310 return datetime.datetime(1970, 1, 1)
312 def make_conditional(req, response, last_modified=None, etag=None):
313 """ Makes the provided response conditional based upon the request,
314 and mandates revalidation from clients
316 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
317 setting ``last_modified`` and ``etag`` correctly on the response object
319 :param req: OpenERP request
320 :type req: web.common.http.WebRequest
321 :param response: Werkzeug response
322 :type response: werkzeug.wrappers.Response
323 :param datetime.datetime last_modified: last modification date of the response content
324 :param str etag: some sort of checksum of the content (deep etag)
325 :return: the response object provided
326 :rtype: werkzeug.wrappers.Response
328 response.cache_control.must_revalidate = True
329 response.cache_control.max_age = 0
331 response.last_modified = last_modified
333 response.set_etag(etag)
334 return response.make_conditional(req.httprequest)
336 def login_and_redirect(req, db, login, key, redirect_url='/'):
337 req.session.authenticate(db, login, key, {})
338 return set_cookie_and_redirect(req, redirect_url)
340 def set_cookie_and_redirect(req, redirect_url):
341 redirect = werkzeug.utils.redirect(redirect_url, 303)
342 redirect.autocorrect_location_header = False
343 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
344 redirect.set_cookie('instance0|session_id', cookie_val)
347 def eval_context_and_domain(session, context, domain=None):
348 e_context = session.eval_context(context)
349 # should we give the evaluated context as an evaluation context to the domain?
350 e_domain = session.eval_domain(domain or [])
352 return e_context, e_domain
354 def load_actions_from_ir_values(req, key, key2, models, meta):
355 context = req.session.eval_context(req.context)
356 Values = req.session.model('ir.values')
357 actions = Values.get(key, key2, models, meta, context)
359 return [(id, name, clean_action(req, action))
360 for id, name, action in actions]
362 def clean_action(req, action, do_not_eval=False):
363 action.setdefault('flags', {})
365 context = req.session.eval_context(req.context)
366 eval_ctx = req.session.evaluation_context(context)
369 # values come from the server, we can just eval them
370 if action.get('context') and isinstance(action.get('context'), basestring):
371 action['context'] = eval( action['context'], eval_ctx ) or {}
373 if action.get('domain') and isinstance(action.get('domain'), basestring):
374 action['domain'] = eval( action['domain'], eval_ctx ) or []
376 if 'context' in action:
377 action['context'] = parse_context(action['context'], req.session)
378 if 'domain' in action:
379 action['domain'] = parse_domain(action['domain'], req.session)
381 action_type = action.setdefault('type', 'ir.actions.act_window_close')
382 if action_type == 'ir.actions.act_window':
383 return fix_view_modes(action)
386 # I think generate_views,fix_view_modes should go into js ActionManager
387 def generate_views(action):
389 While the server generates a sequence called "views" computing dependencies
390 between a bunch of stuff for views coming directly from the database
391 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
392 to return custom view dictionaries generated on the fly.
394 In that case, there is no ``views`` key available on the action.
396 Since the web client relies on ``action['views']``, generate it here from
397 ``view_mode`` and ``view_id``.
399 Currently handles two different cases:
401 * no view_id, multiple view_mode
402 * single view_id, single view_mode
404 :param dict action: action descriptor dictionary to generate a views key for
406 view_id = action.get('view_id') or False
407 if isinstance(view_id, (list, tuple)):
410 # providing at least one view mode is a requirement, not an option
411 view_modes = action['view_mode'].split(',')
413 if len(view_modes) > 1:
415 raise ValueError('Non-db action dictionaries should provide '
416 'either multiple view modes or a single view '
417 'mode and an optional view id.\n\n Got view '
418 'modes %r and view id %r for action %r' % (
419 view_modes, view_id, action))
420 action['views'] = [(False, mode) for mode in view_modes]
422 action['views'] = [(view_id, view_modes[0])]
424 def fix_view_modes(action):
425 """ For historical reasons, OpenERP has weird dealings in relation to
426 view_mode and the view_type attribute (on window actions):
428 * one of the view modes is ``tree``, which stands for both list views
430 * the choice is made by checking ``view_type``, which is either
431 ``form`` for a list view or ``tree`` for an actual tree view
433 This methods simply folds the view_type into view_mode by adding a
434 new view mode ``list`` which is the result of the ``tree`` view_mode
435 in conjunction with the ``form`` view_type.
437 TODO: this should go into the doc, some kind of "peculiarities" section
439 :param dict action: an action descriptor
440 :returns: nothing, the action is modified in place
442 if not action.get('views'):
443 generate_views(action)
445 if action.pop('view_type', 'form') != 'form':
448 if 'view_mode' in action:
449 action['view_mode'] = ','.join(
450 mode if mode != 'tree' else 'list'
451 for mode in action['view_mode'].split(','))
453 [id, mode if mode != 'tree' else 'list']
454 for id, mode in action['views']
459 def parse_domain(domain, session):
460 """ Parses an arbitrary string containing a domain, transforms it
461 to either a literal domain or a :class:`nonliterals.Domain`
463 :param domain: the domain to parse, if the domain is not a string it
464 is assumed to be a literal domain and is returned as-is
465 :param session: Current OpenERP session
466 :type session: openerpweb.OpenERPSession
468 if not isinstance(domain, basestring):
471 return ast.literal_eval(domain)
474 return nonliterals.Domain(session, domain)
476 def parse_context(context, session):
477 """ Parses an arbitrary string containing a context, transforms it
478 to either a literal context or a :class:`nonliterals.Context`
480 :param context: the context to parse, if the context is not a string it
481 is assumed to be a literal domain and is returned as-is
482 :param session: Current OpenERP session
483 :type session: openerpweb.OpenERPSession
485 if not isinstance(context, basestring):
488 return ast.literal_eval(context)
490 return nonliterals.Context(session, context)
492 def _local_web_translations(trans_file):
495 with open(trans_file) as t_file:
496 po = babel.messages.pofile.read_po(t_file)
500 if x.id and x.string and "openerp-web" in x.auto_comments:
501 messages.append({'id': x.id, 'string': x.string})
504 def xml2json_from_elementtree(el, preserve_whitespaces=False):
506 Simple and straightforward XML-to-JSON converter in Python
508 http://code.google.com/p/xml2json-direct/
512 ns, name = el.tag.rsplit("}", 1)
514 res["namespace"] = ns[1:]
518 for k, v in el.items():
521 if el.text and (preserve_whitespaces or el.text.strip() != ''):
524 kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
525 if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
526 kids.append(kid.tail)
527 res["children"] = kids
530 def content_disposition(filename, req):
531 filename = filename.encode('utf8')
532 escaped = urllib2.quote(filename)
533 browser = req.httprequest.user_agent.browser
534 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
535 if browser == 'msie' and version < 9:
536 return "attachment; filename=%s" % escaped
537 elif browser == 'safari':
538 return "attachment; filename=%s" % filename
540 return "attachment; filename*=UTF-8''%s" % escaped
543 #----------------------------------------------------------
544 # OpenERP Web web Controllers
545 #----------------------------------------------------------
547 html_template = """<!DOCTYPE html>
548 <html style="height: 100%%">
550 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
551 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
552 <title>OpenERP</title>
553 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
554 <link rel="stylesheet" href="/web/static/src/css/full.css" />
557 <script type="text/javascript">
559 var s = new openerp.init(%(modules)s);
566 <script src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
568 var test = function() {
573 if (window.localStorage && false) {
574 if (! localStorage.getItem("hasShownGFramePopup")) {
576 localStorage.setItem("hasShownGFramePopup", true);
587 class Home(openerpweb.Controller):
590 @openerpweb.httprequest
591 def index(self, req, s_action=None, **kw):
592 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, None, 'js'))
593 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, None, 'css'))
595 r = html_template % {
598 'modules': simplejson.dumps(module_boot(req)),
599 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
603 @openerpweb.httprequest
604 def login(self, req, db, login, key):
605 return login_and_redirect(req, db, login, key)
607 class WebClient(openerpweb.Controller):
608 _cp_path = "/web/webclient"
610 @openerpweb.jsonrequest
611 def csslist(self, req, mods=None):
612 return manifest_list(req, mods, 'css')
614 @openerpweb.jsonrequest
615 def jslist(self, req, mods=None):
616 return manifest_list(req, mods, 'js')
618 @openerpweb.jsonrequest
619 def qweblist(self, req, mods=None):
620 return manifest_list(req, mods, 'qweb')
622 @openerpweb.httprequest
623 def css(self, req, mods=None):
624 files = list(manifest_glob(req, mods, 'css'))
625 last_modified = get_last_modified(f[0] for f in files)
626 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
627 return werkzeug.wrappers.Response(status=304)
629 file_map = dict(files)
631 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
632 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
635 """read the a css file and absolutify all relative uris"""
636 with open(f, 'rb') as fp:
637 data = fp.read().decode('utf-8')
640 # convert FS path into web path
641 web_dir = '/'.join(os.path.dirname(path).split(os.path.sep))
645 r"""@import \1%s/""" % (web_dir,),
651 r"""url(\1%s/""" % (web_dir,),
654 return data.encode('utf-8')
656 content, checksum = concat_files((f[0] for f in files), reader)
658 return make_conditional(
659 req, req.make_response(content, [('Content-Type', 'text/css')]),
660 last_modified, checksum)
662 @openerpweb.httprequest
663 def js(self, req, mods=None):
664 files = [f[0] for f in manifest_glob(req, mods, 'js')]
665 last_modified = get_last_modified(files)
666 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
667 return werkzeug.wrappers.Response(status=304)
669 content, checksum = concat_js(files)
671 return make_conditional(
672 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
673 last_modified, checksum)
675 @openerpweb.httprequest
676 def qweb(self, req, mods=None):
677 files = [f[0] for f in manifest_glob(req, mods, 'qweb')]
678 last_modified = get_last_modified(files)
679 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
680 return werkzeug.wrappers.Response(status=304)
682 content, checksum = concat_xml(files)
684 return make_conditional(
685 req, req.make_response(content, [('Content-Type', 'text/xml')]),
686 last_modified, checksum)
688 @openerpweb.jsonrequest
689 def bootstrap_translations(self, req, mods):
690 """ Load local translations from *.po files, as a temporary solution
691 until we have established a valid session. This is meant only
692 for translating the login page and db management chrome, using
693 the browser's language. """
694 # For performance reasons we only load a single translation, so for
695 # sub-languages (that should only be partially translated) we load the
696 # main language PO instead - that should be enough for the login screen.
697 lang = req.lang.split('_')[0]
699 translations_per_module = {}
700 for addon_name in mods:
701 if openerpweb.addons_manifest[addon_name].get('bootstrap'):
702 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
703 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
704 if not os.path.exists(f_name):
706 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
708 return {"modules": translations_per_module,
709 "lang_parameters": None}
711 @openerpweb.jsonrequest
712 def translations(self, req, mods, lang):
713 res_lang = req.session.model('res.lang')
714 ids = res_lang.search([("code", "=", lang)])
717 lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
718 "grouping", "decimal_point", "thousands_sep"])
720 # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
721 # done server-side when the language is loaded, so we only need to load the user's lang.
722 ir_translation = req.session.model('ir.translation')
723 translations_per_module = {}
724 messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
725 ('comments','like','openerp-web'),('value','!=',False),
727 ['module','src','value','lang'], order='module')
728 for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
729 translations_per_module.setdefault(mod,{'messages':[]})
730 translations_per_module[mod]['messages'].extend({'id': m['src'],
731 'string': m['value']} \
733 return {"modules": translations_per_module,
734 "lang_parameters": lang_params}
736 @openerpweb.jsonrequest
737 def version_info(self, req):
739 "version": openerp.release.version
742 class Proxy(openerpweb.Controller):
743 _cp_path = '/web/proxy'
745 @openerpweb.jsonrequest
746 def load(self, req, path):
747 """ Proxies an HTTP request through a JSON request.
749 It is strongly recommended to not request binary files through this,
750 as the result will be a binary data blob as well.
752 :param req: OpenERP request
753 :param path: actual request path
754 :return: file content
756 from werkzeug.test import Client
757 from werkzeug.wrappers import BaseResponse
759 return Client(req.httprequest.app, BaseResponse).get(path).data
761 class Database(openerpweb.Controller):
762 _cp_path = "/web/database"
764 @openerpweb.jsonrequest
765 def get_list(self, req):
768 @openerpweb.jsonrequest
769 def create(self, req, fields):
770 params = dict(map(operator.itemgetter('name', 'value'), fields))
771 return req.session.proxy("db").create_database(
772 params['super_admin_pwd'],
774 bool(params.get('demo_data')),
776 params['create_admin_pwd'])
778 @openerpweb.jsonrequest
779 def duplicate(self, req, fields):
780 params = dict(map(operator.itemgetter('name', 'value'), fields))
781 return req.session.proxy("db").duplicate_database(
782 params['super_admin_pwd'],
783 params['db_original_name'],
786 @openerpweb.jsonrequest
787 def drop(self, req, fields):
788 password, db = operator.itemgetter(
789 'drop_pwd', 'drop_db')(
790 dict(map(operator.itemgetter('name', 'value'), fields)))
793 return req.session.proxy("db").drop(password, db)
794 except xmlrpclib.Fault, e:
795 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
796 return {'error': e.faultCode, 'title': 'Drop Database'}
797 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
799 @openerpweb.httprequest
800 def backup(self, req, backup_db, backup_pwd, token):
802 db_dump = base64.b64decode(
803 req.session.proxy("db").dump(backup_pwd, backup_db))
804 filename = "%(db)s_%(timestamp)s.dump" % {
806 'timestamp': datetime.datetime.utcnow().strftime(
807 "%Y-%m-%d_%H-%M-%SZ")
809 return req.make_response(db_dump,
810 [('Content-Type', 'application/octet-stream; charset=binary'),
811 ('Content-Disposition', content_disposition(filename, req))],
812 {'fileToken': int(token)}
814 except xmlrpclib.Fault, e:
815 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
817 @openerpweb.httprequest
818 def restore(self, req, db_file, restore_pwd, new_db):
820 data = base64.b64encode(db_file.read())
821 req.session.proxy("db").restore(restore_pwd, new_db, data)
823 except xmlrpclib.Fault, e:
824 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
825 raise Exception("AccessDenied")
827 @openerpweb.jsonrequest
828 def change_password(self, req, fields):
829 old_password, new_password = operator.itemgetter(
830 'old_pwd', 'new_pwd')(
831 dict(map(operator.itemgetter('name', 'value'), fields)))
833 return req.session.proxy("db").change_admin_password(old_password, new_password)
834 except xmlrpclib.Fault, e:
835 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
836 return {'error': e.faultCode, 'title': 'Change Password'}
837 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
839 class Session(openerpweb.Controller):
840 _cp_path = "/web/session"
842 def session_info(self, req):
843 req.session.ensure_valid()
845 "session_id": req.session_id,
846 "uid": req.session._uid,
847 "context": req.session.get_context() if req.session._uid else {},
848 "db": req.session._db,
849 "login": req.session._login,
852 @openerpweb.jsonrequest
853 def get_session_info(self, req):
854 return self.session_info(req)
856 @openerpweb.jsonrequest
857 def authenticate(self, req, db, login, password, base_location=None):
858 wsgienv = req.httprequest.environ
860 base_location=base_location,
861 HTTP_HOST=wsgienv['HTTP_HOST'],
862 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
864 req.session.authenticate(db, login, password, env)
866 return self.session_info(req)
868 @openerpweb.jsonrequest
869 def change_password (self,req,fields):
870 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
871 dict(map(operator.itemgetter('name', 'value'), fields)))
872 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
873 return {'error':'All passwords have to be filled.','title': 'Change Password'}
874 if new_password != confirm_password:
875 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
877 if req.session.model('res.users').change_password(
878 old_password, new_password):
879 return {'new_password':new_password}
881 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
882 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
884 @openerpweb.jsonrequest
885 def sc_list(self, req):
886 return req.session.model('ir.ui.view_sc').get_sc(
887 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
889 @openerpweb.jsonrequest
890 def get_lang_list(self, req):
893 'lang_list': (req.session.proxy("db").list_lang() or []),
897 return {"error": e, "title": "Languages"}
899 @openerpweb.jsonrequest
900 def modules(self, req):
901 # return all installed modules. Web client is smart enough to not load a module twice
902 return module_installed(req)
904 @openerpweb.jsonrequest
905 def eval_domain_and_context(self, req, contexts, domains,
907 """ Evaluates sequences of domains and contexts, composing them into
908 a single context, domain or group_by sequence.
910 :param list contexts: list of contexts to merge together. Contexts are
911 evaluated in sequence, all previous contexts
912 are part of their own evaluation context
913 (starting at the session context).
914 :param list domains: list of domains to merge together. Domains are
915 evaluated in sequence and appended to one another
916 (implicit AND), their evaluation domain is the
917 result of merging all contexts.
918 :param list group_by_seq: list of domains (which may be in a different
919 order than the ``contexts`` parameter),
920 evaluated in sequence, their ``'group_by'``
921 key is extracted if they have one.
926 the global context created by merging all of
930 the concatenation of all domains
933 a list of fields to group by, potentially empty (in which case
934 no group by should be performed)
936 context, domain = eval_context_and_domain(req.session,
937 nonliterals.CompoundContext(*(contexts or [])),
938 nonliterals.CompoundDomain(*(domains or [])))
940 group_by_sequence = []
941 for candidate in (group_by_seq or []):
942 ctx = req.session.eval_context(candidate, context)
943 group_by = ctx.get('group_by')
946 elif isinstance(group_by, basestring):
947 group_by_sequence.append(group_by)
949 group_by_sequence.extend(group_by)
954 'group_by': group_by_sequence
957 @openerpweb.jsonrequest
958 def save_session_action(self, req, the_action):
960 This method store an action object in the session object and returns an integer
961 identifying that action. The method get_session_action() can be used to get
964 :param the_action: The action to save in the session.
965 :type the_action: anything
966 :return: A key identifying the saved action.
969 saved_actions = req.httpsession.get('saved_actions')
970 if not saved_actions:
971 saved_actions = {"next":0, "actions":{}}
972 req.httpsession['saved_actions'] = saved_actions
973 # we don't allow more than 10 stored actions
974 if len(saved_actions["actions"]) >= 10:
975 del saved_actions["actions"][min(saved_actions["actions"])]
976 key = saved_actions["next"]
977 saved_actions["actions"][key] = the_action
978 saved_actions["next"] = key + 1
981 @openerpweb.jsonrequest
982 def get_session_action(self, req, key):
984 Gets back a previously saved action. This method can return None if the action
985 was saved since too much time (this case should be handled in a smart way).
987 :param key: The key given by save_session_action()
989 :return: The saved action or None.
992 saved_actions = req.httpsession.get('saved_actions')
993 if not saved_actions:
995 return saved_actions["actions"].get(key)
997 @openerpweb.jsonrequest
998 def check(self, req):
999 req.session.assert_valid()
1002 @openerpweb.jsonrequest
1003 def destroy(self, req):
1004 req.session._suicide = True
1006 class Menu(openerpweb.Controller):
1007 _cp_path = "/web/menu"
1009 @openerpweb.jsonrequest
1010 def load(self, req):
1011 return {'data': self.do_load(req)}
1013 @openerpweb.jsonrequest
1014 def get_user_roots(self, req):
1015 return self.do_get_user_roots(req)
1017 def do_get_user_roots(self, req):
1018 """ Return all root menu ids visible for the session user.
1020 :param req: A request object, with an OpenERP session attribute
1021 :type req: < session -> OpenERPSession >
1022 :return: the root menu ids
1026 context = s.eval_context(req.context)
1027 Menus = s.model('ir.ui.menu')
1028 # If a menu action is defined use its domain to get the root menu items
1029 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1031 menu_domain = [('parent_id', '=', False)]
1033 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1035 menu_domain = ast.literal_eval(domain_string)
1037 return Menus.search(menu_domain, 0, False, False, context)
1039 def do_load(self, req):
1040 """ Loads all menu items (all applications and their sub-menus).
1042 :param req: A request object, with an OpenERP session attribute
1043 :type req: < session -> OpenERPSession >
1044 :return: the menu root
1045 :rtype: dict('children': menu_nodes)
1047 context = req.session.eval_context(req.context)
1048 Menus = req.session.model('ir.ui.menu')
1050 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1051 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1053 # menus are loaded fully unlike a regular tree view, cause there are a
1054 # limited number of items (752 when all 6.1 addons are installed)
1055 menu_ids = Menus.search([], 0, False, False, context)
1056 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1057 # adds roots at the end of the sequence, so that they will overwrite
1058 # equivalent menu items from full menu read when put into id:item
1059 # mapping, resulting in children being correctly set on the roots.
1060 menu_items.extend(menu_roots)
1062 # make a tree using parent_id
1063 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1064 for menu_item in menu_items:
1065 if menu_item['parent_id']:
1066 parent = menu_item['parent_id'][0]
1069 if parent in menu_items_map:
1070 menu_items_map[parent].setdefault(
1071 'children', []).append(menu_item)
1073 # sort by sequence a tree using parent_id
1074 for menu_item in menu_items:
1075 menu_item.setdefault('children', []).sort(
1076 key=operator.itemgetter('sequence'))
1080 @openerpweb.jsonrequest
1081 def action(self, req, menu_id):
1082 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1083 [('ir.ui.menu', menu_id)], False)
1084 return {"action": actions}
1086 class DataSet(openerpweb.Controller):
1087 _cp_path = "/web/dataset"
1089 @openerpweb.jsonrequest
1090 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1091 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1092 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1094 """ Performs a search() followed by a read() (if needed) using the
1095 provided search criteria
1097 :param req: a JSON-RPC request object
1098 :type req: openerpweb.JsonRequest
1099 :param str model: the name of the model to search on
1100 :param fields: a list of the fields to return in the result records
1102 :param int offset: from which index should the results start being returned
1103 :param int limit: the maximum number of records to return
1104 :param list domain: the search domain for the query
1105 :param list sort: sorting directives
1106 :returns: A structure (dict) with two keys: ids (all the ids matching
1107 the (domain, context) pair) and records (paginated records
1108 matching fields selection set)
1111 Model = req.session.model(model)
1113 context, domain = eval_context_and_domain(
1114 req.session, req.context, domain)
1116 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1117 if limit and len(ids) == limit:
1118 length = Model.search_count(domain, context)
1120 length = len(ids) + (offset or 0)
1121 if fields and fields == ['id']:
1122 # shortcut read if we only want the ids
1125 'records': [{'id': id} for id in ids]
1128 records = Model.read(ids, fields or False, context)
1129 records.sort(key=lambda obj: ids.index(obj['id']))
1135 @openerpweb.jsonrequest
1136 def load(self, req, model, id, fields):
1137 m = req.session.model(model)
1139 r = m.read([id], False, req.session.eval_context(req.context))
1142 return {'value': value}
1144 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1145 has_domain = domain_id is not None and domain_id < len(args)
1146 has_context = context_id is not None and context_id < len(args)
1148 domain = args[domain_id] if has_domain else []
1149 context = args[context_id] if has_context else {}
1150 c, d = eval_context_and_domain(req.session, context, domain)
1154 args[context_id] = c
1156 return self._call_kw(req, model, method, args, {})
1158 def _call_kw(self, req, model, method, args, kwargs):
1159 for i in xrange(len(args)):
1160 if isinstance(args[i], nonliterals.BaseContext):
1161 args[i] = req.session.eval_context(args[i])
1162 elif isinstance(args[i], nonliterals.BaseDomain):
1163 args[i] = req.session.eval_domain(args[i])
1164 for k in kwargs.keys():
1165 if isinstance(kwargs[k], nonliterals.BaseContext):
1166 kwargs[k] = req.session.eval_context(kwargs[k])
1167 elif isinstance(kwargs[k], nonliterals.BaseDomain):
1168 kwargs[k] = req.session.eval_domain(kwargs[k])
1170 # Temporary implements future display_name special field for model#read()
1171 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1172 if 'display_name' in args[1]:
1173 names = req.session.model(model).name_get(args[0], **kwargs)
1174 args[1].remove('display_name')
1175 r = getattr(req.session.model(model), method)(*args, **kwargs)
1176 for i in range(len(r)):
1177 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1180 return getattr(req.session.model(model), method)(*args, **kwargs)
1182 @openerpweb.jsonrequest
1183 def onchange(self, req, model, method, args, context_id=None):
1184 """ Support method for handling onchange calls: behaves much like call
1185 with the following differences:
1187 * Does not take a domain_id
1188 * Is aware of the return value's structure, and will parse the domains
1189 if needed in order to return either parsed literal domains (in JSON)
1190 or non-literal domain instances, allowing those domains to be used
1194 :type req: web.common.http.JsonRequest
1195 :param str model: object type on which to call the method
1196 :param str method: name of the onchange handler method
1197 :param list args: arguments to call the onchange handler with
1198 :param int context_id: index of the context object in the list of
1200 :return: result of the onchange call with all domains parsed
1202 result = self.call_common(req, model, method, args, context_id=context_id)
1203 if not result or 'domain' not in result:
1206 result['domain'] = dict(
1207 (k, parse_domain(v, req.session))
1208 for k, v in result['domain'].iteritems())
1212 @openerpweb.jsonrequest
1213 def call(self, req, model, method, args, domain_id=None, context_id=None):
1214 return self.call_common(req, model, method, args, domain_id, context_id)
1216 @openerpweb.jsonrequest
1217 def call_kw(self, req, model, method, args, kwargs):
1218 return self._call_kw(req, model, method, args, kwargs)
1220 @openerpweb.jsonrequest
1221 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1222 action = self.call_common(req, model, method, args, domain_id, context_id)
1223 if isinstance(action, dict) and action.get('type') != '':
1224 return clean_action(req, action)
1227 @openerpweb.jsonrequest
1228 def exec_workflow(self, req, model, id, signal):
1229 return req.session.exec_workflow(model, id, signal)
1231 @openerpweb.jsonrequest
1232 def resequence(self, req, model, ids, field='sequence', offset=0):
1233 """ Re-sequences a number of records in the model, by their ids
1235 The re-sequencing starts at the first model of ``ids``, the sequence
1236 number is incremented by one after each record and starts at ``offset``
1238 :param ids: identifiers of the records to resequence, in the new sequence order
1240 :param str field: field used for sequence specification, defaults to
1242 :param int offset: sequence number for first record in ``ids``, allows
1243 starting the resequencing from an arbitrary number,
1246 m = req.session.model(model)
1247 if not m.fields_get([field]):
1249 # python 2.6 has no start parameter
1250 for i, id in enumerate(ids):
1251 m.write(id, { field: i + offset })
1254 class View(openerpweb.Controller):
1255 _cp_path = "/web/view"
1257 def fields_view_get(self, req, model, view_id, view_type,
1258 transform=True, toolbar=False, submenu=False):
1259 Model = req.session.model(model)
1260 context = req.session.eval_context(req.context)
1261 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1262 # todo fme?: check that we should pass the evaluated context here
1263 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1264 if toolbar and transform:
1265 self.process_toolbar(req, fvg['toolbar'])
1268 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1269 # depending on how it feels, xmlrpclib.ServerProxy can translate
1270 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1271 # enjoy unicode strings which can not be trivially converted to
1272 # strings, and it blows up during parsing.
1274 # So ensure we fix this retardation by converting view xml back to
1276 if isinstance(fvg['arch'], unicode):
1277 arch = fvg['arch'].encode('utf-8')
1280 fvg['arch_string'] = arch
1283 evaluation_context = session.evaluation_context(context or {})
1284 xml = self.transform_view(arch, session, evaluation_context)
1286 xml = ElementTree.fromstring(arch)
1287 fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
1289 if 'id' in fvg['fields']:
1290 # Special case for id's
1291 id_field = fvg['fields']['id']
1292 id_field['original_type'] = id_field['type']
1293 id_field['type'] = 'id'
1295 for field in fvg['fields'].itervalues():
1296 if field.get('views'):
1297 for view in field["views"].itervalues():
1298 self.process_view(session, view, None, transform)
1299 if field.get('domain'):
1300 field["domain"] = parse_domain(field["domain"], session)
1301 if field.get('context'):
1302 field["context"] = parse_context(field["context"], session)
1304 def process_toolbar(self, req, toolbar):
1306 The toolbar is a mapping of section_key: [action_descriptor]
1308 We need to clean all those actions in order to ensure correct
1311 for actions in toolbar.itervalues():
1312 for action in actions:
1313 if 'context' in action:
1314 action['context'] = parse_context(
1315 action['context'], req.session)
1316 if 'domain' in action:
1317 action['domain'] = parse_domain(
1318 action['domain'], req.session)
1320 @openerpweb.jsonrequest
1321 def add_custom(self, req, view_id, arch):
1322 CustomView = req.session.model('ir.ui.view.custom')
1324 'user_id': req.session._uid,
1327 }, req.session.eval_context(req.context))
1328 return {'result': True}
1330 @openerpweb.jsonrequest
1331 def undo_custom(self, req, view_id, reset=False):
1332 CustomView = req.session.model('ir.ui.view.custom')
1333 context = req.session.eval_context(req.context)
1334 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1335 0, False, False, context)
1338 CustomView.unlink(vcustom, context)
1340 CustomView.unlink([vcustom[0]], context)
1341 return {'result': True}
1342 return {'result': False}
1344 def transform_view(self, view_string, session, context=None):
1345 # transform nodes on the fly via iterparse, instead of
1346 # doing it statically on the parsing result
1347 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1349 for event, elem in parser:
1350 if event == "start":
1353 self.parse_domains_and_contexts(elem, session)
1356 def parse_domains_and_contexts(self, elem, session):
1357 """ Converts domains and contexts from the view into Python objects,
1358 either literals if they can be parsed by literal_eval or a special
1359 placeholder object if the domain or context refers to free variables.
1361 :param elem: the current node being parsed
1362 :type param: xml.etree.ElementTree.Element
1363 :param session: OpenERP session object, used to store and retrieve
1365 :type session: openerpweb.openerpweb.OpenERPSession
1367 for el in ['domain', 'filter_domain']:
1368 domain = elem.get(el, '').strip()
1370 elem.set(el, parse_domain(domain, session))
1371 elem.set(el + '_string', domain)
1372 for el in ['context', 'default_get']:
1373 context_string = elem.get(el, '').strip()
1375 elem.set(el, parse_context(context_string, session))
1376 elem.set(el + '_string', context_string)
1378 @openerpweb.jsonrequest
1379 def load(self, req, model, view_id, view_type, toolbar=False):
1380 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1382 class TreeView(View):
1383 _cp_path = "/web/treeview"
1385 @openerpweb.jsonrequest
1386 def action(self, req, model, id):
1387 return load_actions_from_ir_values(
1388 req,'action', 'tree_but_open',[(model, id)],
1391 class SearchView(View):
1392 _cp_path = "/web/searchview"
1394 @openerpweb.jsonrequest
1395 def load(self, req, model, view_id):
1396 fields_view = self.fields_view_get(req, model, view_id, 'search')
1397 return {'fields_view': fields_view}
1399 @openerpweb.jsonrequest
1400 def fields_get(self, req, model):
1401 Model = req.session.model(model)
1402 fields = Model.fields_get(False, req.session.eval_context(req.context))
1403 for field in fields.values():
1404 # shouldn't convert the views too?
1405 if field.get('domain'):
1406 field["domain"] = parse_domain(field["domain"], req.session)
1407 if field.get('context'):
1408 field["context"] = parse_context(field["context"], req.session)
1409 return {'fields': fields}
1411 @openerpweb.jsonrequest
1412 def get_filters(self, req, model):
1413 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1414 Model = req.session.model("ir.filters")
1415 filters = Model.get_filters(model)
1416 for filter in filters:
1418 parsed_context = parse_context(filter["context"], req.session)
1419 filter["context"] = (parsed_context
1420 if not isinstance(parsed_context, nonliterals.BaseContext)
1421 else req.session.eval_context(parsed_context))
1423 parsed_domain = parse_domain(filter["domain"], req.session)
1424 filter["domain"] = (parsed_domain
1425 if not isinstance(parsed_domain, nonliterals.BaseDomain)
1426 else req.session.eval_domain(parsed_domain))
1428 logger.exception("Failed to parse custom filter %s in %s",
1429 filter['name'], model)
1430 filter['disabled'] = True
1431 del filter['context']
1432 del filter['domain']
1435 class Binary(openerpweb.Controller):
1436 _cp_path = "/web/binary"
1438 @openerpweb.httprequest
1439 def image(self, req, model, id, field, **kw):
1440 last_update = '__last_update'
1441 Model = req.session.model(model)
1442 context = req.session.eval_context(req.context)
1443 headers = [('Content-Type', 'image/png')]
1444 etag = req.httprequest.headers.get('If-None-Match')
1445 hashed_session = hashlib.md5(req.session_id).hexdigest()
1446 id = None if not id else simplejson.loads(id)
1447 if type(id) is list:
1450 if not id and hashed_session == etag:
1451 return werkzeug.wrappers.Response(status=304)
1453 date = Model.read([id], [last_update], context)[0].get(last_update)
1454 if hashlib.md5(date).hexdigest() == etag:
1455 return werkzeug.wrappers.Response(status=304)
1457 retag = hashed_session
1460 res = Model.default_get([field], context).get(field)
1463 res = Model.read([id], [last_update, field], context)[0]
1464 retag = hashlib.md5(res.get(last_update)).hexdigest()
1465 image_base64 = res.get(field)
1467 if kw.get('resize'):
1468 resize = kw.get('resize').split(',');
1469 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1470 width = int(resize[0])
1471 height = int(resize[1])
1472 # resize maximum 500*500
1473 if width > 500: width = 500
1474 if height > 500: height = 500
1475 image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1477 image_data = base64.b64decode(image_base64)
1479 except (TypeError, xmlrpclib.Fault):
1480 image_data = self.placeholder(req)
1481 headers.append(('ETag', retag))
1482 headers.append(('Content-Length', len(image_data)))
1484 ncache = int(kw.get('cache'))
1485 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1488 return req.make_response(image_data, headers)
1489 def placeholder(self, req):
1490 addons_path = openerpweb.addons_manifest['web']['addons_path']
1491 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1493 @openerpweb.httprequest
1494 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1495 """ Download link for files stored as binary fields.
1497 If the ``id`` parameter is omitted, fetches the default value for the
1498 binary field (via ``default_get``), otherwise fetches the field for
1499 that precise record.
1501 :param req: OpenERP request
1502 :type req: :class:`web.common.http.HttpRequest`
1503 :param str model: name of the model to fetch the binary from
1504 :param str field: binary field
1505 :param str id: id of the record from which to fetch the binary
1506 :param str filename_field: field holding the file's name, if any
1507 :returns: :class:`werkzeug.wrappers.Response`
1509 Model = req.session.model(model)
1510 context = req.session.eval_context(req.context)
1513 fields.append(filename_field)
1515 res = Model.read([int(id)], fields, context)[0]
1517 res = Model.default_get(fields, context)
1518 filecontent = base64.b64decode(res.get(field, ''))
1520 return req.not_found()
1522 filename = '%s_%s' % (model.replace('.', '_'), id)
1524 filename = res.get(filename_field, '') or filename
1525 return req.make_response(filecontent,
1526 [('Content-Type', 'application/octet-stream'),
1527 ('Content-Disposition', content_disposition(filename, req))])
1529 @openerpweb.httprequest
1530 def saveas_ajax(self, req, data, token):
1531 jdata = simplejson.loads(data)
1532 model = jdata['model']
1533 field = jdata['field']
1534 id = jdata.get('id', None)
1535 filename_field = jdata.get('filename_field', None)
1536 context = jdata.get('context', dict())
1538 context = req.session.eval_context(context)
1539 Model = req.session.model(model)
1542 fields.append(filename_field)
1544 res = Model.read([int(id)], fields, context)[0]
1546 res = Model.default_get(fields, context)
1547 filecontent = base64.b64decode(res.get(field, ''))
1549 raise ValueError("No content found for field '%s' on '%s:%s'" %
1552 filename = '%s_%s' % (model.replace('.', '_'), id)
1554 filename = res.get(filename_field, '') or filename
1555 return req.make_response(filecontent,
1556 headers=[('Content-Type', 'application/octet-stream'),
1557 ('Content-Disposition', content_disposition(filename, req))],
1558 cookies={'fileToken': int(token)})
1560 @openerpweb.httprequest
1561 def upload(self, req, callback, ufile):
1562 # TODO: might be useful to have a configuration flag for max-length file uploads
1564 out = """<script language="javascript" type="text/javascript">
1565 var win = window.top.window;
1566 win.jQuery(win).trigger(%s, %s);
1569 args = [len(data), ufile.filename,
1570 ufile.content_type, base64.b64encode(data)]
1571 except Exception, e:
1572 args = [False, e.message]
1573 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1575 @openerpweb.httprequest
1576 def upload_attachment(self, req, callback, model, id, ufile):
1577 context = req.session.eval_context(req.context)
1578 Model = req.session.model('ir.attachment')
1580 out = """<script language="javascript" type="text/javascript">
1581 var win = window.top.window;
1582 win.jQuery(win).trigger(%s, %s);
1584 attachment_id = Model.create({
1585 'name': ufile.filename,
1586 'datas': base64.encodestring(ufile.read()),
1587 'datas_fname': ufile.filename,
1592 'filename': ufile.filename,
1595 except Exception, e:
1596 args = { 'error': e.message }
1597 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1599 class Action(openerpweb.Controller):
1600 _cp_path = "/web/action"
1602 @openerpweb.jsonrequest
1603 def load(self, req, action_id, do_not_eval=False):
1604 Actions = req.session.model('ir.actions.actions')
1606 context = req.session.eval_context(req.context)
1609 action_id = int(action_id)
1612 module, xmlid = action_id.split('.', 1)
1613 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1614 assert model.startswith('ir.actions.')
1616 action_id = 0 # force failed read
1618 base_action = Actions.read([action_id], ['type'], context)
1621 action_type = base_action[0]['type']
1622 if action_type == 'ir.actions.report.xml':
1623 ctx.update({'bin_size': True})
1625 action = req.session.model(action_type).read([action_id], False, ctx)
1627 value = clean_action(req, action[0], do_not_eval)
1630 @openerpweb.jsonrequest
1631 def run(self, req, action_id):
1632 return_action = req.session.model('ir.actions.server').run(
1633 [action_id], req.session.eval_context(req.context))
1635 return clean_action(req, return_action)
1640 _cp_path = "/web/export"
1642 @openerpweb.jsonrequest
1643 def formats(self, req):
1644 """ Returns all valid export formats
1646 :returns: for each export format, a pair of identifier and printable name
1647 :rtype: [(str, str)]
1651 for path, controller in openerpweb.controllers_path.iteritems()
1652 if path.startswith(self._cp_path)
1653 if hasattr(controller, 'fmt')
1654 ], key=operator.itemgetter("label"))
1656 def fields_get(self, req, model):
1657 Model = req.session.model(model)
1658 fields = Model.fields_get(False, req.session.eval_context(req.context))
1661 @openerpweb.jsonrequest
1662 def get_fields(self, req, model, prefix='', parent_name= '',
1663 import_compat=True, parent_field_type=None,
1666 if import_compat and parent_field_type == "many2one":
1669 fields = self.fields_get(req, model)
1672 fields.pop('id', None)
1674 fields['.id'] = fields.pop('id', {'string': 'ID'})
1676 fields_sequence = sorted(fields.iteritems(),
1677 key=lambda field: field[1].get('string', ''))
1680 for field_name, field in fields_sequence:
1682 if exclude and field_name in exclude:
1684 if field.get('readonly'):
1685 # If none of the field's states unsets readonly, skip the field
1686 if all(dict(attrs).get('readonly', True)
1687 for attrs in field.get('states', {}).values()):
1690 id = prefix + (prefix and '/'or '') + field_name
1691 name = parent_name + (parent_name and '/' or '') + field['string']
1692 record = {'id': id, 'string': name,
1693 'value': id, 'children': False,
1694 'field_type': field.get('type'),
1695 'required': field.get('required'),
1696 'relation_field': field.get('relation_field')}
1697 records.append(record)
1699 if len(name.split('/')) < 3 and 'relation' in field:
1700 ref = field.pop('relation')
1701 record['value'] += '/id'
1702 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1704 if not import_compat or field['type'] == 'one2many':
1705 # m2m field in import_compat is childless
1706 record['children'] = True
1710 @openerpweb.jsonrequest
1711 def namelist(self,req, model, export_id):
1712 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1713 export = req.session.model("ir.exports").read([export_id])[0]
1714 export_fields_list = req.session.model("ir.exports.line").read(
1715 export['export_fields'])
1717 fields_data = self.fields_info(
1718 req, model, map(operator.itemgetter('name'), export_fields_list))
1721 {'name': field['name'], 'label': fields_data[field['name']]}
1722 for field in export_fields_list
1725 def fields_info(self, req, model, export_fields):
1727 fields = self.fields_get(req, model)
1729 # To make fields retrieval more efficient, fetch all sub-fields of a
1730 # given field at the same time. Because the order in the export list is
1731 # arbitrary, this requires ordering all sub-fields of a given field
1732 # together so they can be fetched at the same time
1734 # Works the following way:
1735 # * sort the list of fields to export, the default sorting order will
1736 # put the field itself (if present, for xmlid) and all of its
1737 # sub-fields right after it
1738 # * then, group on: the first field of the path (which is the same for
1739 # a field and for its subfields and the length of splitting on the
1740 # first '/', which basically means grouping the field on one side and
1741 # all of the subfields on the other. This way, we have the field (for
1742 # the xmlid) with length 1, and all of the subfields with the same
1743 # base but a length "flag" of 2
1744 # * if we have a normal field (length 1), just add it to the info
1745 # mapping (with its string) as-is
1746 # * otherwise, recursively call fields_info via graft_subfields.
1747 # all graft_subfields does is take the result of fields_info (on the
1748 # field's model) and prepend the current base (current field), which
1749 # rebuilds the whole sub-tree for the field
1751 # result: because we're not fetching the fields_get for half the
1752 # database models, fetching a namelist with a dozen fields (including
1753 # relational data) falls from ~6s to ~300ms (on the leads model).
1754 # export lists with no sub-fields (e.g. import_compatible lists with
1755 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1756 # there's a single fields_get to execute)
1757 for (base, length), subfields in itertools.groupby(
1758 sorted(export_fields),
1759 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1760 subfields = list(subfields)
1762 # subfields is a seq of $base/*rest, and not loaded yet
1763 info.update(self.graft_subfields(
1764 req, fields[base]['relation'], base, fields[base]['string'],
1768 info[base] = fields[base]['string']
1772 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1773 export_fields = [field.split('/', 1)[1] for field in fields]
1775 (prefix + '/' + k, prefix_string + '/' + v)
1776 for k, v in self.fields_info(req, model, export_fields).iteritems())
1778 #noinspection PyPropertyDefinition
1780 def content_type(self):
1781 """ Provides the format's content type """
1782 raise NotImplementedError()
1784 def filename(self, base):
1785 """ Creates a valid filename for the format (with extension) from the
1786 provided base name (exension-less)
1788 raise NotImplementedError()
1790 def from_data(self, fields, rows):
1791 """ Conversion method from OpenERP's export data to whatever the
1792 current export class outputs
1794 :params list fields: a list of fields to export
1795 :params list rows: a list of records to export
1799 raise NotImplementedError()
1801 @openerpweb.httprequest
1802 def index(self, req, data, token):
1803 model, fields, ids, domain, import_compat = \
1804 operator.itemgetter('model', 'fields', 'ids', 'domain',
1806 simplejson.loads(data))
1808 context = req.session.eval_context(req.context)
1809 Model = req.session.model(model)
1810 ids = ids or Model.search(domain, 0, False, False, context)
1812 field_names = map(operator.itemgetter('name'), fields)
1813 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1816 columns_headers = field_names
1818 columns_headers = [val['label'].strip() for val in fields]
1821 return req.make_response(self.from_data(columns_headers, import_data),
1822 headers=[('Content-Disposition',
1823 content_disposition(self.filename(model), req)),
1824 ('Content-Type', self.content_type)],
1825 cookies={'fileToken': int(token)})
1827 class CSVExport(Export):
1828 _cp_path = '/web/export/csv'
1829 fmt = {'tag': 'csv', 'label': 'CSV'}
1832 def content_type(self):
1833 return 'text/csv;charset=utf8'
1835 def filename(self, base):
1836 return base + '.csv'
1838 def from_data(self, fields, rows):
1840 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1842 writer.writerow([name.encode('utf-8') for name in fields])
1847 if isinstance(d, basestring):
1848 d = d.replace('\n',' ').replace('\t',' ')
1850 d = d.encode('utf-8')
1851 except UnicodeError:
1853 if d is False: d = None
1855 writer.writerow(row)
1862 class ExcelExport(Export):
1863 _cp_path = '/web/export/xls'
1867 'error': None if xlwt else "XLWT required"
1871 def content_type(self):
1872 return 'application/vnd.ms-excel'
1874 def filename(self, base):
1875 return base + '.xls'
1877 def from_data(self, fields, rows):
1878 workbook = xlwt.Workbook()
1879 worksheet = workbook.add_sheet('Sheet 1')
1881 for i, fieldname in enumerate(fields):
1882 worksheet.write(0, i, fieldname)
1883 worksheet.col(i).width = 8000 # around 220 pixels
1885 style = xlwt.easyxf('align: wrap yes')
1887 for row_index, row in enumerate(rows):
1888 for cell_index, cell_value in enumerate(row):
1889 if isinstance(cell_value, basestring):
1890 cell_value = re.sub("\r", " ", cell_value)
1891 if cell_value is False: cell_value = None
1892 worksheet.write(row_index + 1, cell_index, cell_value, style)
1901 class Reports(View):
1902 _cp_path = "/web/report"
1903 POLLING_DELAY = 0.25
1905 'doc': 'application/vnd.ms-word',
1906 'html': 'text/html',
1907 'odt': 'application/vnd.oasis.opendocument.text',
1908 'pdf': 'application/pdf',
1909 'sxw': 'application/vnd.sun.xml.writer',
1910 'xls': 'application/vnd.ms-excel',
1913 @openerpweb.httprequest
1914 def index(self, req, action, token):
1915 action = simplejson.loads(action)
1917 report_srv = req.session.proxy("report")
1918 context = req.session.eval_context(
1919 nonliterals.CompoundContext(
1920 req.context or {}, action[ "context"]))
1923 report_ids = context["active_ids"]
1924 if 'report_type' in action:
1925 report_data['report_type'] = action['report_type']
1926 if 'datas' in action:
1927 if 'ids' in action['datas']:
1928 report_ids = action['datas'].pop('ids')
1929 report_data.update(action['datas'])
1931 report_id = report_srv.report(
1932 req.session._db, req.session._uid, req.session._password,
1933 action["report_name"], report_ids,
1934 report_data, context)
1936 report_struct = None
1938 report_struct = report_srv.report_get(
1939 req.session._db, req.session._uid, req.session._password, report_id)
1940 if report_struct["state"]:
1943 time.sleep(self.POLLING_DELAY)
1945 report = base64.b64decode(report_struct['result'])
1946 if report_struct.get('code') == 'zlib':
1947 report = zlib.decompress(report)
1948 report_mimetype = self.TYPES_MAPPING.get(
1949 report_struct['format'], 'octet-stream')
1950 file_name = action.get('name', 'report')
1951 if 'name' not in action:
1952 reports = req.session.model('ir.actions.report.xml')
1953 res_id = reports.search([('report_name', '=', action['report_name']),],
1954 0, False, False, context)
1956 file_name = reports.read(res_id[0], ['name'], context)['name']
1958 file_name = action['report_name']
1959 file_name = '%s.%s' % (file_name, report_struct['format'])
1961 return req.make_response(report,
1963 ('Content-Disposition', content_disposition(file_name, req)),
1964 ('Content-Type', report_mimetype),
1965 ('Content-Length', len(report))],
1966 cookies={'fileToken': int(token)})
1968 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: