1 # -*- coding: utf-8 -*-
19 from xml.etree import ElementTree
20 from cStringIO import StringIO
22 import babel.messages.pofile
24 import werkzeug.wrappers
33 from .. import nonliterals
36 #----------------------------------------------------------
38 #----------------------------------------------------------
41 """ Minify js with a clever regex.
42 Taken from http://opensource.perlig.de/rjsmin
43 Apache License, Version 2.0 """
45 """ Substitution callback """
46 groups = match.groups()
52 (groups[4] and '\n') or
53 (groups[5] and ' ') or
54 (groups[6] and ' ') or
55 (groups[7] and ' ') or
60 r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
61 r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
62 r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
63 r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
64 r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
65 r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
66 r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
67 r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
68 r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
69 r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
70 r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
71 r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
72 r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
73 r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
74 r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
75 r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
76 r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
77 r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
78 r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
79 r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
80 r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
85 proxy = req.session.proxy("db")
87 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
89 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
90 dbs = [i for i in dbs if re.match(r, i)]
94 # if only one db exists, return it else return False
99 except xmlrpclib.Fault:
100 # ignore access denied
104 def module_topological_sort(modules):
105 """ Return a list of module names sorted so that their dependencies of the
106 modules are listed before the module itself
108 modules is a dict of {module_name: dependencies}
110 :param modules: modules to sort
115 dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
116 # incoming edge: dependency on other module (if a depends on b, a has an
117 # incoming edge from b, aka there's an edge from b to a)
118 # outgoing edge: other module depending on this one
120 # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
121 #L ← Empty list that will contain the sorted nodes
123 #S ← Set of all nodes with no outgoing edges (modules on which no other
125 S = set(module for module in modules if module not in dependencies)
128 #function visit(node n)
130 #if n has not been visited yet then
134 #change: n not web module, can not be resolved, ignore
135 if n not in modules: return
136 #for each node m with an edge from m to n do (dependencies of n)
142 #for each node n in S do
148 def module_installed(req):
149 # Candidates module the current heuristic is the /static dir
150 loadable = openerpweb.addons_manifest.keys()
153 # Retrieve database installed modules
154 # TODO The following code should move to ir.module.module.list_installed_modules()
155 Modules = req.session.model('ir.module.module')
156 domain = [('state','=','installed'), ('name','in', loadable)]
157 for module in Modules.search_read(domain, ['name', 'dependencies_id']):
158 modules[module['name']] = []
159 deps = module.get('dependencies_id')
161 deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
162 dependencies = [i['name'] for i in deps_read]
163 modules[module['name']] = dependencies
165 sorted_modules = module_topological_sort(modules)
166 return sorted_modules
168 def module_installed_bypass_session(dbname):
169 loadable = openerpweb.addons_manifest.keys()
172 import openerp.modules.registry
173 registry = openerp.modules.registry.RegistryManager.get(dbname)
174 with registry.cursor() as cr:
175 m = registry.get('ir.module.module')
176 # TODO The following code should move to ir.module.module.list_installed_modules()
177 domain = [('state','=','installed'), ('name','in', loadable)]
178 ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
179 for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
180 modules[module['name']] = []
181 deps = module.get('dependencies_id')
183 deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
184 dependencies = [i['name'] for i in deps_read]
185 modules[module['name']] = dependencies
188 sorted_modules = module_topological_sort(modules)
189 return sorted_modules
191 def module_boot(req, db=None):
192 server_wide_modules = openerp.conf.server_wide_modules or ['web']
195 for i in server_wide_modules:
196 if i in openerpweb.addons_manifest:
198 monodb = db or db_monodb(req)
200 dbside = module_installed_bypass_session(monodb)
201 dbside = [i for i in dbside if i not in serverside]
202 addons = serverside + dbside
205 def concat_xml(file_list):
206 """Concatenate xml files
208 :param list(str) file_list: list of files to check
209 :returns: (concatenation_result, checksum)
212 checksum = hashlib.new('sha1')
214 return '', checksum.hexdigest()
217 for fname in file_list:
218 with open(fname, 'rb') as fp:
220 checksum.update(contents)
222 xml = ElementTree.parse(fp).getroot()
225 root = ElementTree.Element(xml.tag)
226 #elif root.tag != xml.tag:
227 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
229 for child in xml.getchildren():
231 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
233 def concat_files(file_list, reader=None, intersperse=""):
234 """ Concatenates contents of all provided files
236 :param list(str) file_list: list of files to check
237 :param function reader: reading procedure for each file
238 :param str intersperse: string to intersperse between file contents
239 :returns: (concatenation_result, checksum)
242 checksum = hashlib.new('sha1')
244 return '', checksum.hexdigest()
248 with open(f, 'rb') as fp:
252 for fname in file_list:
253 contents = reader(fname)
254 checksum.update(contents)
255 files_content.append(contents)
257 files_concat = intersperse.join(files_content)
258 return files_concat, checksum.hexdigest()
262 def concat_js(file_list):
263 content, checksum = concat_files(file_list, intersperse=';')
264 if checksum in concat_js_cache:
265 content = concat_js_cache[checksum]
267 content = rjsmin(content)
268 concat_js_cache[checksum] = content
269 return content, checksum
272 """convert FS path into web path"""
273 return '/'.join(path.split(os.path.sep))
275 def manifest_glob(req, extension, addons=None, db=None):
277 addons = module_boot(req, db=db)
279 addons = addons.split(',')
282 manifest = openerpweb.addons_manifest.get(addon, None)
285 # ensure does not ends with /
286 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
287 globlist = manifest.get(extension, [])
288 for pattern in globlist:
289 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
290 r.append((path, fs2web(path[len(addons_path):])))
293 def manifest_list(req, extension, mods=None, db=None):
295 path = '/web/webclient/' + extension
297 path += '?mods=' + mods
301 files = manifest_glob(req, extension, addons=mods, db=db)
302 i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
303 req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
305 return [wp for _fp, wp in files]
307 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
309 def get_last_modified(files):
310 """ Returns the modification time of the most recently modified
313 :param list(str) files: names of files to check
314 :return: most recent modification time amongst the fileset
315 :rtype: datetime.datetime
319 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
321 return datetime.datetime(1970, 1, 1)
323 def make_conditional(req, response, last_modified=None, etag=None):
324 """ Makes the provided response conditional based upon the request,
325 and mandates revalidation from clients
327 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
328 setting ``last_modified`` and ``etag`` correctly on the response object
330 :param req: OpenERP request
331 :type req: web.common.http.WebRequest
332 :param response: Werkzeug response
333 :type response: werkzeug.wrappers.Response
334 :param datetime.datetime last_modified: last modification date of the response content
335 :param str etag: some sort of checksum of the content (deep etag)
336 :return: the response object provided
337 :rtype: werkzeug.wrappers.Response
339 response.cache_control.must_revalidate = True
340 response.cache_control.max_age = 0
342 response.last_modified = last_modified
344 response.set_etag(etag)
345 return response.make_conditional(req.httprequest)
347 def login_and_redirect(req, db, login, key, redirect_url='/'):
348 req.session.authenticate(db, login, key, {})
349 return set_cookie_and_redirect(req, redirect_url)
351 def set_cookie_and_redirect(req, redirect_url):
352 redirect = werkzeug.utils.redirect(redirect_url, 303)
353 redirect.autocorrect_location_header = False
354 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
355 redirect.set_cookie('instance0|session_id', cookie_val)
358 def eval_context_and_domain(session, context, domain=None):
359 e_context = session.eval_context(context)
360 # should we give the evaluated context as an evaluation context to the domain?
361 e_domain = session.eval_domain(domain or [])
363 return e_context, e_domain
365 def load_actions_from_ir_values(req, key, key2, models, meta):
366 context = req.session.eval_context(req.context)
367 Values = req.session.model('ir.values')
368 actions = Values.get(key, key2, models, meta, context)
370 return [(id, name, clean_action(req, action))
371 for id, name, action in actions]
373 def clean_action(req, action, do_not_eval=False):
374 action.setdefault('flags', {})
376 context = req.session.eval_context(req.context)
377 eval_ctx = req.session.evaluation_context(context)
380 # values come from the server, we can just eval them
381 if action.get('context') and isinstance(action.get('context'), basestring):
382 action['context'] = eval( action['context'], eval_ctx ) or {}
384 if action.get('domain') and isinstance(action.get('domain'), basestring):
385 action['domain'] = eval( action['domain'], eval_ctx ) or []
387 if 'context' in action:
388 action['context'] = parse_context(action['context'], req.session)
389 if 'domain' in action:
390 action['domain'] = parse_domain(action['domain'], req.session)
392 action_type = action.setdefault('type', 'ir.actions.act_window_close')
393 if action_type == 'ir.actions.act_window':
394 return fix_view_modes(action)
397 # I think generate_views,fix_view_modes should go into js ActionManager
398 def generate_views(action):
400 While the server generates a sequence called "views" computing dependencies
401 between a bunch of stuff for views coming directly from the database
402 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
403 to return custom view dictionaries generated on the fly.
405 In that case, there is no ``views`` key available on the action.
407 Since the web client relies on ``action['views']``, generate it here from
408 ``view_mode`` and ``view_id``.
410 Currently handles two different cases:
412 * no view_id, multiple view_mode
413 * single view_id, single view_mode
415 :param dict action: action descriptor dictionary to generate a views key for
417 view_id = action.get('view_id') or False
418 if isinstance(view_id, (list, tuple)):
421 # providing at least one view mode is a requirement, not an option
422 view_modes = action['view_mode'].split(',')
424 if len(view_modes) > 1:
426 raise ValueError('Non-db action dictionaries should provide '
427 'either multiple view modes or a single view '
428 'mode and an optional view id.\n\n Got view '
429 'modes %r and view id %r for action %r' % (
430 view_modes, view_id, action))
431 action['views'] = [(False, mode) for mode in view_modes]
433 action['views'] = [(view_id, view_modes[0])]
435 def fix_view_modes(action):
436 """ For historical reasons, OpenERP has weird dealings in relation to
437 view_mode and the view_type attribute (on window actions):
439 * one of the view modes is ``tree``, which stands for both list views
441 * the choice is made by checking ``view_type``, which is either
442 ``form`` for a list view or ``tree`` for an actual tree view
444 This methods simply folds the view_type into view_mode by adding a
445 new view mode ``list`` which is the result of the ``tree`` view_mode
446 in conjunction with the ``form`` view_type.
448 TODO: this should go into the doc, some kind of "peculiarities" section
450 :param dict action: an action descriptor
451 :returns: nothing, the action is modified in place
453 if not action.get('views'):
454 generate_views(action)
456 if action.pop('view_type', 'form') != 'form':
459 if 'view_mode' in action:
460 action['view_mode'] = ','.join(
461 mode if mode != 'tree' else 'list'
462 for mode in action['view_mode'].split(','))
464 [id, mode if mode != 'tree' else 'list']
465 for id, mode in action['views']
470 def parse_domain(domain, session):
471 """ Parses an arbitrary string containing a domain, transforms it
472 to either a literal domain or a :class:`nonliterals.Domain`
474 :param domain: the domain to parse, if the domain is not a string it
475 is assumed to be a literal domain and is returned as-is
476 :param session: Current OpenERP session
477 :type session: openerpweb.OpenERPSession
479 if not isinstance(domain, basestring):
482 return ast.literal_eval(domain)
485 return nonliterals.Domain(session, domain)
487 def parse_context(context, session):
488 """ Parses an arbitrary string containing a context, transforms it
489 to either a literal context or a :class:`nonliterals.Context`
491 :param context: the context to parse, if the context is not a string it
492 is assumed to be a literal domain and is returned as-is
493 :param session: Current OpenERP session
494 :type session: openerpweb.OpenERPSession
496 if not isinstance(context, basestring):
499 return ast.literal_eval(context)
501 return nonliterals.Context(session, context)
503 def _local_web_translations(trans_file):
506 with open(trans_file) as t_file:
507 po = babel.messages.pofile.read_po(t_file)
511 if x.id and x.string and "openerp-web" in x.auto_comments:
512 messages.append({'id': x.id, 'string': x.string})
515 def xml2json_from_elementtree(el, preserve_whitespaces=False):
517 Simple and straightforward XML-to-JSON converter in Python
519 http://code.google.com/p/xml2json-direct/
523 ns, name = el.tag.rsplit("}", 1)
525 res["namespace"] = ns[1:]
529 for k, v in el.items():
532 if el.text and (preserve_whitespaces or el.text.strip() != ''):
535 kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
536 if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
537 kids.append(kid.tail)
538 res["children"] = kids
541 def content_disposition(filename, req):
542 filename = filename.encode('utf8')
543 escaped = urllib2.quote(filename)
544 browser = req.httprequest.user_agent.browser
545 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
546 if browser == 'msie' and version < 9:
547 return "attachment; filename=%s" % escaped
548 elif browser == 'safari':
549 return "attachment; filename=%s" % filename
551 return "attachment; filename*=UTF-8''%s" % escaped
554 #----------------------------------------------------------
555 # OpenERP Web web Controllers
556 #----------------------------------------------------------
558 html_template = """<!DOCTYPE html>
559 <html style="height: 100%%">
561 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
562 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
563 <title>OpenERP</title>
564 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
565 <link rel="stylesheet" href="/web/static/src/css/full.css" />
568 <script type="text/javascript">
570 var s = new openerp.init(%(modules)s);
577 <script src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
579 var test = function() {
584 if (window.localStorage && false) {
585 if (! localStorage.getItem("hasShownGFramePopup")) {
587 localStorage.setItem("hasShownGFramePopup", true);
598 class Home(openerpweb.Controller):
601 @openerpweb.httprequest
602 def index(self, req, s_action=None, db=None, **kw):
603 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, 'js', db=db))
604 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, 'css', db=db))
606 r = html_template % {
609 'modules': simplejson.dumps(module_boot(req, db=db)),
610 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
614 @openerpweb.httprequest
615 def login(self, req, db, login, key):
616 return login_and_redirect(req, db, login, key)
618 class WebClient(openerpweb.Controller):
619 _cp_path = "/web/webclient"
621 @openerpweb.jsonrequest
622 def csslist(self, req, mods=None):
623 return manifest_list(req, 'css', mods=mods)
625 @openerpweb.jsonrequest
626 def jslist(self, req, mods=None):
627 return manifest_list(req, 'js', mods=mods)
629 @openerpweb.jsonrequest
630 def qweblist(self, req, mods=None):
631 return manifest_list(req, 'qweb', mods=mods)
633 @openerpweb.httprequest
634 def css(self, req, mods=None, db=None):
635 files = list(manifest_glob(req, 'css', addons=mods, db=db))
636 last_modified = get_last_modified(f[0] for f in files)
637 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
638 return werkzeug.wrappers.Response(status=304)
640 file_map = dict(files)
642 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
643 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
646 """read the a css file and absolutify all relative uris"""
647 with open(f, 'rb') as fp:
648 data = fp.read().decode('utf-8')
651 web_dir = os.path.dirname(path)
655 r"""@import \1%s/""" % (web_dir,),
661 r"""url(\1%s/""" % (web_dir,),
664 return data.encode('utf-8')
666 content, checksum = concat_files((f[0] for f in files), reader)
668 return make_conditional(
669 req, req.make_response(content, [('Content-Type', 'text/css')]),
670 last_modified, checksum)
672 @openerpweb.httprequest
673 def js(self, req, mods=None, db=None):
674 files = [f[0] for f in manifest_glob(req, 'js', addons=mods, db=db)]
675 last_modified = get_last_modified(files)
676 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
677 return werkzeug.wrappers.Response(status=304)
679 content, checksum = concat_js(files)
681 return make_conditional(
682 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
683 last_modified, checksum)
685 @openerpweb.httprequest
686 def qweb(self, req, mods=None, db=None):
687 files = [f[0] for f in manifest_glob(req, 'qweb', addons=mods, db=db)]
688 last_modified = get_last_modified(files)
689 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
690 return werkzeug.wrappers.Response(status=304)
692 content, checksum = concat_xml(files)
694 return make_conditional(
695 req, req.make_response(content, [('Content-Type', 'text/xml')]),
696 last_modified, checksum)
698 @openerpweb.jsonrequest
699 def bootstrap_translations(self, req, mods):
700 """ Load local translations from *.po files, as a temporary solution
701 until we have established a valid session. This is meant only
702 for translating the login page and db management chrome, using
703 the browser's language. """
704 # For performance reasons we only load a single translation, so for
705 # sub-languages (that should only be partially translated) we load the
706 # main language PO instead - that should be enough for the login screen.
707 lang = req.lang.split('_')[0]
709 translations_per_module = {}
710 for addon_name in mods:
711 if openerpweb.addons_manifest[addon_name].get('bootstrap'):
712 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
713 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
714 if not os.path.exists(f_name):
716 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
718 return {"modules": translations_per_module,
719 "lang_parameters": None}
721 @openerpweb.jsonrequest
722 def translations(self, req, mods, lang):
723 res_lang = req.session.model('res.lang')
724 ids = res_lang.search([("code", "=", lang)])
727 lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
728 "grouping", "decimal_point", "thousands_sep"])
730 # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
731 # done server-side when the language is loaded, so we only need to load the user's lang.
732 ir_translation = req.session.model('ir.translation')
733 translations_per_module = {}
734 messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
735 ('comments','like','openerp-web'),('value','!=',False),
737 ['module','src','value','lang'], order='module')
738 for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
739 translations_per_module.setdefault(mod,{'messages':[]})
740 translations_per_module[mod]['messages'].extend({'id': m['src'],
741 'string': m['value']} \
743 return {"modules": translations_per_module,
744 "lang_parameters": lang_params}
746 @openerpweb.jsonrequest
747 def version_info(self, req):
749 "version": openerp.release.version
752 class Proxy(openerpweb.Controller):
753 _cp_path = '/web/proxy'
755 @openerpweb.jsonrequest
756 def load(self, req, path):
757 """ Proxies an HTTP request through a JSON request.
759 It is strongly recommended to not request binary files through this,
760 as the result will be a binary data blob as well.
762 :param req: OpenERP request
763 :param path: actual request path
764 :return: file content
766 from werkzeug.test import Client
767 from werkzeug.wrappers import BaseResponse
769 return Client(req.httprequest.app, BaseResponse).get(path).data
771 class Database(openerpweb.Controller):
772 _cp_path = "/web/database"
774 @openerpweb.jsonrequest
775 def get_list(self, req):
778 @openerpweb.jsonrequest
779 def create(self, req, fields):
780 params = dict(map(operator.itemgetter('name', 'value'), fields))
781 return req.session.proxy("db").create_database(
782 params['super_admin_pwd'],
784 bool(params.get('demo_data')),
786 params['create_admin_pwd'])
788 @openerpweb.jsonrequest
789 def duplicate(self, req, fields):
790 params = dict(map(operator.itemgetter('name', 'value'), fields))
791 return req.session.proxy("db").duplicate_database(
792 params['super_admin_pwd'],
793 params['db_original_name'],
796 @openerpweb.jsonrequest
797 def duplicate(self, req, fields):
798 params = dict(map(operator.itemgetter('name', 'value'), fields))
800 params['super_admin_pwd'],
801 params['db_original_name'],
805 return req.session.proxy("db").duplicate_database(*duplicate_attrs)
807 @openerpweb.jsonrequest
808 def drop(self, req, fields):
809 password, db = operator.itemgetter(
810 'drop_pwd', 'drop_db')(
811 dict(map(operator.itemgetter('name', 'value'), fields)))
814 return req.session.proxy("db").drop(password, db)
815 except xmlrpclib.Fault, e:
816 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
817 return {'error': e.faultCode, 'title': 'Drop Database'}
818 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
820 @openerpweb.httprequest
821 def backup(self, req, backup_db, backup_pwd, token):
823 db_dump = base64.b64decode(
824 req.session.proxy("db").dump(backup_pwd, backup_db))
825 filename = "%(db)s_%(timestamp)s.dump" % {
827 'timestamp': datetime.datetime.utcnow().strftime(
828 "%Y-%m-%d_%H-%M-%SZ")
830 return req.make_response(db_dump,
831 [('Content-Type', 'application/octet-stream; charset=binary'),
832 ('Content-Disposition', content_disposition(filename, req))],
833 {'fileToken': int(token)}
835 except xmlrpclib.Fault, e:
836 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
838 @openerpweb.httprequest
839 def restore(self, req, db_file, restore_pwd, new_db):
841 data = base64.b64encode(db_file.read())
842 req.session.proxy("db").restore(restore_pwd, new_db, data)
844 except xmlrpclib.Fault, e:
845 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
846 raise Exception("AccessDenied")
848 @openerpweb.jsonrequest
849 def change_password(self, req, fields):
850 old_password, new_password = operator.itemgetter(
851 'old_pwd', 'new_pwd')(
852 dict(map(operator.itemgetter('name', 'value'), fields)))
854 return req.session.proxy("db").change_admin_password(old_password, new_password)
855 except xmlrpclib.Fault, e:
856 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
857 return {'error': e.faultCode, 'title': 'Change Password'}
858 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
860 class Session(openerpweb.Controller):
861 _cp_path = "/web/session"
863 def session_info(self, req):
864 req.session.ensure_valid()
866 "session_id": req.session_id,
867 "uid": req.session._uid,
868 "context": req.session.get_context() if req.session._uid else {},
869 "db": req.session._db,
870 "login": req.session._login,
873 @openerpweb.jsonrequest
874 def get_session_info(self, req):
875 return self.session_info(req)
877 @openerpweb.jsonrequest
878 def authenticate(self, req, db, login, password, base_location=None):
879 wsgienv = req.httprequest.environ
881 base_location=base_location,
882 HTTP_HOST=wsgienv['HTTP_HOST'],
883 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
885 req.session.authenticate(db, login, password, env)
887 return self.session_info(req)
889 @openerpweb.jsonrequest
890 def change_password (self,req,fields):
891 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
892 dict(map(operator.itemgetter('name', 'value'), fields)))
893 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
894 return {'error':'All passwords have to be filled.','title': 'Change Password'}
895 if new_password != confirm_password:
896 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
898 if req.session.model('res.users').change_password(
899 old_password, new_password):
900 return {'new_password':new_password}
902 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
903 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
905 @openerpweb.jsonrequest
906 def sc_list(self, req):
907 return req.session.model('ir.ui.view_sc').get_sc(
908 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
910 @openerpweb.jsonrequest
911 def get_lang_list(self, req):
913 return req.session.proxy("db").list_lang() or []
915 return {"error": e, "title": "Languages"}
917 @openerpweb.jsonrequest
918 def modules(self, req):
919 # return all installed modules. Web client is smart enough to not load a module twice
920 return module_installed(req)
922 @openerpweb.jsonrequest
923 def eval_domain_and_context(self, req, contexts, domains,
925 """ Evaluates sequences of domains and contexts, composing them into
926 a single context, domain or group_by sequence.
928 :param list contexts: list of contexts to merge together. Contexts are
929 evaluated in sequence, all previous contexts
930 are part of their own evaluation context
931 (starting at the session context).
932 :param list domains: list of domains to merge together. Domains are
933 evaluated in sequence and appended to one another
934 (implicit AND), their evaluation domain is the
935 result of merging all contexts.
936 :param list group_by_seq: list of domains (which may be in a different
937 order than the ``contexts`` parameter),
938 evaluated in sequence, their ``'group_by'``
939 key is extracted if they have one.
944 the global context created by merging all of
948 the concatenation of all domains
951 a list of fields to group by, potentially empty (in which case
952 no group by should be performed)
954 context, domain = eval_context_and_domain(req.session,
955 nonliterals.CompoundContext(*(contexts or [])),
956 nonliterals.CompoundDomain(*(domains or [])))
958 group_by_sequence = []
959 for candidate in (group_by_seq or []):
960 ctx = req.session.eval_context(candidate, context)
961 group_by = ctx.get('group_by')
964 elif isinstance(group_by, basestring):
965 group_by_sequence.append(group_by)
967 group_by_sequence.extend(group_by)
972 'group_by': group_by_sequence
975 @openerpweb.jsonrequest
976 def save_session_action(self, req, the_action):
978 This method store an action object in the session object and returns an integer
979 identifying that action. The method get_session_action() can be used to get
982 :param the_action: The action to save in the session.
983 :type the_action: anything
984 :return: A key identifying the saved action.
987 saved_actions = req.httpsession.get('saved_actions')
988 if not saved_actions:
989 saved_actions = {"next":0, "actions":{}}
990 req.httpsession['saved_actions'] = saved_actions
991 # we don't allow more than 10 stored actions
992 if len(saved_actions["actions"]) >= 10:
993 del saved_actions["actions"][min(saved_actions["actions"])]
994 key = saved_actions["next"]
995 saved_actions["actions"][key] = the_action
996 saved_actions["next"] = key + 1
999 @openerpweb.jsonrequest
1000 def get_session_action(self, req, key):
1002 Gets back a previously saved action. This method can return None if the action
1003 was saved since too much time (this case should be handled in a smart way).
1005 :param key: The key given by save_session_action()
1007 :return: The saved action or None.
1010 saved_actions = req.httpsession.get('saved_actions')
1011 if not saved_actions:
1013 return saved_actions["actions"].get(key)
1015 @openerpweb.jsonrequest
1016 def check(self, req):
1017 req.session.assert_valid()
1020 @openerpweb.jsonrequest
1021 def destroy(self, req):
1022 req.session._suicide = True
1024 class Menu(openerpweb.Controller):
1025 _cp_path = "/web/menu"
1027 @openerpweb.jsonrequest
1028 def load(self, req):
1029 return {'data': self.do_load(req)}
1031 @openerpweb.jsonrequest
1032 def get_user_roots(self, req):
1033 return self.do_get_user_roots(req)
1035 def do_get_user_roots(self, req):
1036 """ Return all root menu ids visible for the session user.
1038 :param req: A request object, with an OpenERP session attribute
1039 :type req: < session -> OpenERPSession >
1040 :return: the root menu ids
1044 context = s.eval_context(req.context)
1045 Menus = s.model('ir.ui.menu')
1046 # If a menu action is defined use its domain to get the root menu items
1047 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1049 menu_domain = [('parent_id', '=', False)]
1051 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1053 menu_domain = ast.literal_eval(domain_string)
1055 return Menus.search(menu_domain, 0, False, False, context)
1057 def do_load(self, req):
1058 """ Loads all menu items (all applications and their sub-menus).
1060 :param req: A request object, with an OpenERP session attribute
1061 :type req: < session -> OpenERPSession >
1062 :return: the menu root
1063 :rtype: dict('children': menu_nodes)
1065 context = req.session.eval_context(req.context)
1066 Menus = req.session.model('ir.ui.menu')
1068 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1069 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1071 # menus are loaded fully unlike a regular tree view, cause there are a
1072 # limited number of items (752 when all 6.1 addons are installed)
1073 menu_ids = Menus.search([], 0, False, False, context)
1074 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1075 # adds roots at the end of the sequence, so that they will overwrite
1076 # equivalent menu items from full menu read when put into id:item
1077 # mapping, resulting in children being correctly set on the roots.
1078 menu_items.extend(menu_roots)
1080 # make a tree using parent_id
1081 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1082 for menu_item in menu_items:
1083 if menu_item['parent_id']:
1084 parent = menu_item['parent_id'][0]
1087 if parent in menu_items_map:
1088 menu_items_map[parent].setdefault(
1089 'children', []).append(menu_item)
1091 # sort by sequence a tree using parent_id
1092 for menu_item in menu_items:
1093 menu_item.setdefault('children', []).sort(
1094 key=operator.itemgetter('sequence'))
1098 @openerpweb.jsonrequest
1099 def action(self, req, menu_id):
1100 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1101 [('ir.ui.menu', menu_id)], False)
1102 return {"action": actions}
1104 class DataSet(openerpweb.Controller):
1105 _cp_path = "/web/dataset"
1107 @openerpweb.jsonrequest
1108 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1109 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1110 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1112 """ Performs a search() followed by a read() (if needed) using the
1113 provided search criteria
1115 :param req: a JSON-RPC request object
1116 :type req: openerpweb.JsonRequest
1117 :param str model: the name of the model to search on
1118 :param fields: a list of the fields to return in the result records
1120 :param int offset: from which index should the results start being returned
1121 :param int limit: the maximum number of records to return
1122 :param list domain: the search domain for the query
1123 :param list sort: sorting directives
1124 :returns: A structure (dict) with two keys: ids (all the ids matching
1125 the (domain, context) pair) and records (paginated records
1126 matching fields selection set)
1129 Model = req.session.model(model)
1131 context, domain = eval_context_and_domain(
1132 req.session, req.context, domain)
1134 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1135 if limit and len(ids) == limit:
1136 length = Model.search_count(domain, context)
1138 length = len(ids) + (offset or 0)
1139 if fields and fields == ['id']:
1140 # shortcut read if we only want the ids
1143 'records': [{'id': id} for id in ids]
1146 records = Model.read(ids, fields or False, context)
1147 records.sort(key=lambda obj: ids.index(obj['id']))
1153 @openerpweb.jsonrequest
1154 def load(self, req, model, id, fields):
1155 m = req.session.model(model)
1157 r = m.read([id], False, req.session.eval_context(req.context))
1160 return {'value': value}
1162 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1163 has_domain = domain_id is not None and domain_id < len(args)
1164 has_context = context_id is not None and context_id < len(args)
1166 domain = args[domain_id] if has_domain else []
1167 context = args[context_id] if has_context else {}
1168 c, d = eval_context_and_domain(req.session, context, domain)
1172 args[context_id] = c
1174 return self._call_kw(req, model, method, args, {})
1176 def _call_kw(self, req, model, method, args, kwargs):
1177 for i in xrange(len(args)):
1178 if isinstance(args[i], nonliterals.BaseContext):
1179 args[i] = req.session.eval_context(args[i])
1180 elif isinstance(args[i], nonliterals.BaseDomain):
1181 args[i] = req.session.eval_domain(args[i])
1182 for k in kwargs.keys():
1183 if isinstance(kwargs[k], nonliterals.BaseContext):
1184 kwargs[k] = req.session.eval_context(kwargs[k])
1185 elif isinstance(kwargs[k], nonliterals.BaseDomain):
1186 kwargs[k] = req.session.eval_domain(kwargs[k])
1188 # Temporary implements future display_name special field for model#read()
1189 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1190 if 'display_name' in args[1]:
1191 names = req.session.model(model).name_get(args[0], **kwargs)
1192 args[1].remove('display_name')
1193 r = getattr(req.session.model(model), method)(*args, **kwargs)
1194 for i in range(len(r)):
1195 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1198 return getattr(req.session.model(model), method)(*args, **kwargs)
1200 @openerpweb.jsonrequest
1201 def onchange(self, req, model, method, args, context_id=None):
1202 """ Support method for handling onchange calls: behaves much like call
1203 with the following differences:
1205 * Does not take a domain_id
1206 * Is aware of the return value's structure, and will parse the domains
1207 if needed in order to return either parsed literal domains (in JSON)
1208 or non-literal domain instances, allowing those domains to be used
1212 :type req: web.common.http.JsonRequest
1213 :param str model: object type on which to call the method
1214 :param str method: name of the onchange handler method
1215 :param list args: arguments to call the onchange handler with
1216 :param int context_id: index of the context object in the list of
1218 :return: result of the onchange call with all domains parsed
1220 result = self.call_common(req, model, method, args, context_id=context_id)
1221 if not result or 'domain' not in result:
1224 result['domain'] = dict(
1225 (k, parse_domain(v, req.session))
1226 for k, v in result['domain'].iteritems())
1230 @openerpweb.jsonrequest
1231 def call(self, req, model, method, args, domain_id=None, context_id=None):
1232 return self.call_common(req, model, method, args, domain_id, context_id)
1234 @openerpweb.jsonrequest
1235 def call_kw(self, req, model, method, args, kwargs):
1236 return self._call_kw(req, model, method, args, kwargs)
1238 @openerpweb.jsonrequest
1239 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1240 action = self.call_common(req, model, method, args, domain_id, context_id)
1241 if isinstance(action, dict) and action.get('type') != '':
1242 return clean_action(req, action)
1245 @openerpweb.jsonrequest
1246 def exec_workflow(self, req, model, id, signal):
1247 return req.session.exec_workflow(model, id, signal)
1249 @openerpweb.jsonrequest
1250 def resequence(self, req, model, ids, field='sequence', offset=0):
1251 """ Re-sequences a number of records in the model, by their ids
1253 The re-sequencing starts at the first model of ``ids``, the sequence
1254 number is incremented by one after each record and starts at ``offset``
1256 :param ids: identifiers of the records to resequence, in the new sequence order
1258 :param str field: field used for sequence specification, defaults to
1260 :param int offset: sequence number for first record in ``ids``, allows
1261 starting the resequencing from an arbitrary number,
1264 m = req.session.model(model)
1265 if not m.fields_get([field]):
1267 # python 2.6 has no start parameter
1268 for i, id in enumerate(ids):
1269 m.write(id, { field: i + offset })
1272 class View(openerpweb.Controller):
1273 _cp_path = "/web/view"
1275 def fields_view_get(self, req, model, view_id, view_type,
1276 transform=True, toolbar=False, submenu=False):
1277 Model = req.session.model(model)
1278 context = req.session.eval_context(req.context)
1279 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1280 # todo fme?: check that we should pass the evaluated context here
1281 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1282 if toolbar and transform:
1283 self.process_toolbar(req, fvg['toolbar'])
1286 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1287 # depending on how it feels, xmlrpclib.ServerProxy can translate
1288 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1289 # enjoy unicode strings which can not be trivially converted to
1290 # strings, and it blows up during parsing.
1292 # So ensure we fix this retardation by converting view xml back to
1294 if isinstance(fvg['arch'], unicode):
1295 arch = fvg['arch'].encode('utf-8')
1298 fvg['arch_string'] = arch
1301 evaluation_context = session.evaluation_context(context or {})
1302 xml = self.transform_view(arch, session, evaluation_context)
1304 xml = ElementTree.fromstring(arch)
1305 fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
1307 if 'id' in fvg['fields']:
1308 # Special case for id's
1309 id_field = fvg['fields']['id']
1310 id_field['original_type'] = id_field['type']
1311 id_field['type'] = 'id'
1313 for field in fvg['fields'].itervalues():
1314 if field.get('views'):
1315 for view in field["views"].itervalues():
1316 self.process_view(session, view, None, transform)
1317 if field.get('domain'):
1318 field["domain"] = parse_domain(field["domain"], session)
1319 if field.get('context'):
1320 field["context"] = parse_context(field["context"], session)
1322 def process_toolbar(self, req, toolbar):
1324 The toolbar is a mapping of section_key: [action_descriptor]
1326 We need to clean all those actions in order to ensure correct
1329 for actions in toolbar.itervalues():
1330 for action in actions:
1331 if 'context' in action:
1332 action['context'] = parse_context(
1333 action['context'], req.session)
1334 if 'domain' in action:
1335 action['domain'] = parse_domain(
1336 action['domain'], req.session)
1338 @openerpweb.jsonrequest
1339 def add_custom(self, req, view_id, arch):
1340 CustomView = req.session.model('ir.ui.view.custom')
1342 'user_id': req.session._uid,
1345 }, req.session.eval_context(req.context))
1346 return {'result': True}
1348 @openerpweb.jsonrequest
1349 def undo_custom(self, req, view_id, reset=False):
1350 CustomView = req.session.model('ir.ui.view.custom')
1351 context = req.session.eval_context(req.context)
1352 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1353 0, False, False, context)
1356 CustomView.unlink(vcustom, context)
1358 CustomView.unlink([vcustom[0]], context)
1359 return {'result': True}
1360 return {'result': False}
1362 def transform_view(self, view_string, session, context=None):
1363 # transform nodes on the fly via iterparse, instead of
1364 # doing it statically on the parsing result
1365 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1367 for event, elem in parser:
1368 if event == "start":
1371 self.parse_domains_and_contexts(elem, session)
1374 def parse_domains_and_contexts(self, elem, session):
1375 """ Converts domains and contexts from the view into Python objects,
1376 either literals if they can be parsed by literal_eval or a special
1377 placeholder object if the domain or context refers to free variables.
1379 :param elem: the current node being parsed
1380 :type param: xml.etree.ElementTree.Element
1381 :param session: OpenERP session object, used to store and retrieve
1383 :type session: openerpweb.openerpweb.OpenERPSession
1385 for el in ['domain', 'filter_domain']:
1386 domain = elem.get(el, '').strip()
1388 elem.set(el, parse_domain(domain, session))
1389 elem.set(el + '_string', domain)
1390 for el in ['context', 'default_get']:
1391 context_string = elem.get(el, '').strip()
1393 elem.set(el, parse_context(context_string, session))
1394 elem.set(el + '_string', context_string)
1396 @openerpweb.jsonrequest
1397 def load(self, req, model, view_id, view_type, toolbar=False):
1398 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1400 class TreeView(View):
1401 _cp_path = "/web/treeview"
1403 @openerpweb.jsonrequest
1404 def action(self, req, model, id):
1405 return load_actions_from_ir_values(
1406 req,'action', 'tree_but_open',[(model, id)],
1409 class SearchView(View):
1410 _cp_path = "/web/searchview"
1412 @openerpweb.jsonrequest
1413 def load(self, req, model, view_id):
1414 fields_view = self.fields_view_get(req, model, view_id, 'search')
1415 return {'fields_view': fields_view}
1417 @openerpweb.jsonrequest
1418 def fields_get(self, req, model):
1419 Model = req.session.model(model)
1420 fields = Model.fields_get(False, req.session.eval_context(req.context))
1421 for field in fields.values():
1422 # shouldn't convert the views too?
1423 if field.get('domain'):
1424 field["domain"] = parse_domain(field["domain"], req.session)
1425 if field.get('context'):
1426 field["context"] = parse_context(field["context"], req.session)
1427 return {'fields': fields}
1429 @openerpweb.jsonrequest
1430 def get_filters(self, req, model):
1431 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1432 Model = req.session.model("ir.filters")
1433 filters = Model.get_filters(model)
1434 for filter in filters:
1436 parsed_context = parse_context(filter["context"], req.session)
1437 filter["context"] = (parsed_context
1438 if not isinstance(parsed_context, nonliterals.BaseContext)
1439 else req.session.eval_context(parsed_context))
1441 parsed_domain = parse_domain(filter["domain"], req.session)
1442 filter["domain"] = (parsed_domain
1443 if not isinstance(parsed_domain, nonliterals.BaseDomain)
1444 else req.session.eval_domain(parsed_domain))
1446 logger.exception("Failed to parse custom filter %s in %s",
1447 filter['name'], model)
1448 filter['disabled'] = True
1449 del filter['context']
1450 del filter['domain']
1453 class Binary(openerpweb.Controller):
1454 _cp_path = "/web/binary"
1456 @openerpweb.httprequest
1457 def image(self, req, model, id, field, **kw):
1458 last_update = '__last_update'
1459 Model = req.session.model(model)
1460 context = req.session.eval_context(req.context)
1461 headers = [('Content-Type', 'image/png')]
1462 etag = req.httprequest.headers.get('If-None-Match')
1463 hashed_session = hashlib.md5(req.session_id).hexdigest()
1464 id = None if not id else simplejson.loads(id)
1465 if type(id) is list:
1468 if not id and hashed_session == etag:
1469 return werkzeug.wrappers.Response(status=304)
1471 date = Model.read([id], [last_update], context)[0].get(last_update)
1472 if hashlib.md5(date).hexdigest() == etag:
1473 return werkzeug.wrappers.Response(status=304)
1475 retag = hashed_session
1478 res = Model.default_get([field], context).get(field)
1481 res = Model.read([id], [last_update, field], context)[0]
1482 retag = hashlib.md5(res.get(last_update)).hexdigest()
1483 image_base64 = res.get(field)
1485 if kw.get('resize'):
1486 resize = kw.get('resize').split(',');
1487 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1488 width = int(resize[0])
1489 height = int(resize[1])
1490 # resize maximum 500*500
1491 if width > 500: width = 500
1492 if height > 500: height = 500
1493 image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1495 image_data = base64.b64decode(image_base64)
1497 except (TypeError, xmlrpclib.Fault):
1498 image_data = self.placeholder(req)
1499 headers.append(('ETag', retag))
1500 headers.append(('Content-Length', len(image_data)))
1502 ncache = int(kw.get('cache'))
1503 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1506 return req.make_response(image_data, headers)
1507 def placeholder(self, req):
1508 addons_path = openerpweb.addons_manifest['web']['addons_path']
1509 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1511 @openerpweb.httprequest
1512 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1513 """ Download link for files stored as binary fields.
1515 If the ``id`` parameter is omitted, fetches the default value for the
1516 binary field (via ``default_get``), otherwise fetches the field for
1517 that precise record.
1519 :param req: OpenERP request
1520 :type req: :class:`web.common.http.HttpRequest`
1521 :param str model: name of the model to fetch the binary from
1522 :param str field: binary field
1523 :param str id: id of the record from which to fetch the binary
1524 :param str filename_field: field holding the file's name, if any
1525 :returns: :class:`werkzeug.wrappers.Response`
1527 Model = req.session.model(model)
1528 context = req.session.eval_context(req.context)
1531 fields.append(filename_field)
1533 res = Model.read([int(id)], fields, context)[0]
1535 res = Model.default_get(fields, context)
1536 filecontent = base64.b64decode(res.get(field, ''))
1538 return req.not_found()
1540 filename = '%s_%s' % (model.replace('.', '_'), id)
1542 filename = res.get(filename_field, '') or filename
1543 return req.make_response(filecontent,
1544 [('Content-Type', 'application/octet-stream'),
1545 ('Content-Disposition', content_disposition(filename, req))])
1547 @openerpweb.httprequest
1548 def saveas_ajax(self, req, data, token):
1549 jdata = simplejson.loads(data)
1550 model = jdata['model']
1551 field = jdata['field']
1552 id = jdata.get('id', None)
1553 filename_field = jdata.get('filename_field', None)
1554 context = jdata.get('context', dict())
1556 context = req.session.eval_context(context)
1557 Model = req.session.model(model)
1560 fields.append(filename_field)
1562 res = Model.read([int(id)], fields, context)[0]
1564 res = Model.default_get(fields, context)
1565 filecontent = base64.b64decode(res.get(field, ''))
1567 raise ValueError("No content found for field '%s' on '%s:%s'" %
1570 filename = '%s_%s' % (model.replace('.', '_'), id)
1572 filename = res.get(filename_field, '') or filename
1573 return req.make_response(filecontent,
1574 headers=[('Content-Type', 'application/octet-stream'),
1575 ('Content-Disposition', content_disposition(filename, req))],
1576 cookies={'fileToken': int(token)})
1578 @openerpweb.httprequest
1579 def upload(self, req, callback, ufile):
1580 # TODO: might be useful to have a configuration flag for max-length file uploads
1582 out = """<script language="javascript" type="text/javascript">
1583 var win = window.top.window;
1584 win.jQuery(win).trigger(%s, %s);
1587 args = [len(data), ufile.filename,
1588 ufile.content_type, base64.b64encode(data)]
1589 except Exception, e:
1590 args = [False, e.message]
1591 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1593 @openerpweb.httprequest
1594 def upload_attachment(self, req, callback, model, id, ufile):
1595 context = req.session.eval_context(req.context)
1596 Model = req.session.model('ir.attachment')
1598 out = """<script language="javascript" type="text/javascript">
1599 var win = window.top.window;
1600 win.jQuery(win).trigger(%s, %s);
1602 attachment_id = Model.create({
1603 'name': ufile.filename,
1604 'datas': base64.encodestring(ufile.read()),
1605 'datas_fname': ufile.filename,
1610 'filename': ufile.filename,
1613 except Exception, e:
1614 args = { 'error': e.message }
1615 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1617 class Action(openerpweb.Controller):
1618 _cp_path = "/web/action"
1620 @openerpweb.jsonrequest
1621 def load(self, req, action_id, do_not_eval=False):
1622 Actions = req.session.model('ir.actions.actions')
1624 context = req.session.eval_context(req.context)
1627 action_id = int(action_id)
1630 module, xmlid = action_id.split('.', 1)
1631 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1632 assert model.startswith('ir.actions.')
1634 action_id = 0 # force failed read
1636 base_action = Actions.read([action_id], ['type'], context)
1639 action_type = base_action[0]['type']
1640 if action_type == 'ir.actions.report.xml':
1641 ctx.update({'bin_size': True})
1643 action = req.session.model(action_type).read([action_id], False, ctx)
1645 value = clean_action(req, action[0], do_not_eval)
1648 @openerpweb.jsonrequest
1649 def run(self, req, action_id):
1650 return_action = req.session.model('ir.actions.server').run(
1651 [action_id], req.session.eval_context(req.context))
1653 return clean_action(req, return_action)
1658 _cp_path = "/web/export"
1660 @openerpweb.jsonrequest
1661 def formats(self, req):
1662 """ Returns all valid export formats
1664 :returns: for each export format, a pair of identifier and printable name
1665 :rtype: [(str, str)]
1669 for path, controller in openerpweb.controllers_path.iteritems()
1670 if path.startswith(self._cp_path)
1671 if hasattr(controller, 'fmt')
1672 ], key=operator.itemgetter("label"))
1674 def fields_get(self, req, model):
1675 Model = req.session.model(model)
1676 fields = Model.fields_get(False, req.session.eval_context(req.context))
1679 @openerpweb.jsonrequest
1680 def get_fields(self, req, model, prefix='', parent_name= '',
1681 import_compat=True, parent_field_type=None,
1684 if import_compat and parent_field_type == "many2one":
1687 fields = self.fields_get(req, model)
1690 fields.pop('id', None)
1692 fields['.id'] = fields.pop('id', {'string': 'ID'})
1694 fields_sequence = sorted(fields.iteritems(),
1695 key=lambda field: field[1].get('string', ''))
1698 for field_name, field in fields_sequence:
1700 if exclude and field_name in exclude:
1702 if field.get('readonly'):
1703 # If none of the field's states unsets readonly, skip the field
1704 if all(dict(attrs).get('readonly', True)
1705 for attrs in field.get('states', {}).values()):
1708 id = prefix + (prefix and '/'or '') + field_name
1709 name = parent_name + (parent_name and '/' or '') + field['string']
1710 record = {'id': id, 'string': name,
1711 'value': id, 'children': False,
1712 'field_type': field.get('type'),
1713 'required': field.get('required'),
1714 'relation_field': field.get('relation_field')}
1715 records.append(record)
1717 if len(name.split('/')) < 3 and 'relation' in field:
1718 ref = field.pop('relation')
1719 record['value'] += '/id'
1720 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1722 if not import_compat or field['type'] == 'one2many':
1723 # m2m field in import_compat is childless
1724 record['children'] = True
1728 @openerpweb.jsonrequest
1729 def namelist(self,req, model, export_id):
1730 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1731 export = req.session.model("ir.exports").read([export_id])[0]
1732 export_fields_list = req.session.model("ir.exports.line").read(
1733 export['export_fields'])
1735 fields_data = self.fields_info(
1736 req, model, map(operator.itemgetter('name'), export_fields_list))
1739 {'name': field['name'], 'label': fields_data[field['name']]}
1740 for field in export_fields_list
1743 def fields_info(self, req, model, export_fields):
1745 fields = self.fields_get(req, model)
1747 # To make fields retrieval more efficient, fetch all sub-fields of a
1748 # given field at the same time. Because the order in the export list is
1749 # arbitrary, this requires ordering all sub-fields of a given field
1750 # together so they can be fetched at the same time
1752 # Works the following way:
1753 # * sort the list of fields to export, the default sorting order will
1754 # put the field itself (if present, for xmlid) and all of its
1755 # sub-fields right after it
1756 # * then, group on: the first field of the path (which is the same for
1757 # a field and for its subfields and the length of splitting on the
1758 # first '/', which basically means grouping the field on one side and
1759 # all of the subfields on the other. This way, we have the field (for
1760 # the xmlid) with length 1, and all of the subfields with the same
1761 # base but a length "flag" of 2
1762 # * if we have a normal field (length 1), just add it to the info
1763 # mapping (with its string) as-is
1764 # * otherwise, recursively call fields_info via graft_subfields.
1765 # all graft_subfields does is take the result of fields_info (on the
1766 # field's model) and prepend the current base (current field), which
1767 # rebuilds the whole sub-tree for the field
1769 # result: because we're not fetching the fields_get for half the
1770 # database models, fetching a namelist with a dozen fields (including
1771 # relational data) falls from ~6s to ~300ms (on the leads model).
1772 # export lists with no sub-fields (e.g. import_compatible lists with
1773 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1774 # there's a single fields_get to execute)
1775 for (base, length), subfields in itertools.groupby(
1776 sorted(export_fields),
1777 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1778 subfields = list(subfields)
1780 # subfields is a seq of $base/*rest, and not loaded yet
1781 info.update(self.graft_subfields(
1782 req, fields[base]['relation'], base, fields[base]['string'],
1786 info[base] = fields[base]['string']
1790 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1791 export_fields = [field.split('/', 1)[1] for field in fields]
1793 (prefix + '/' + k, prefix_string + '/' + v)
1794 for k, v in self.fields_info(req, model, export_fields).iteritems())
1796 #noinspection PyPropertyDefinition
1798 def content_type(self):
1799 """ Provides the format's content type """
1800 raise NotImplementedError()
1802 def filename(self, base):
1803 """ Creates a valid filename for the format (with extension) from the
1804 provided base name (exension-less)
1806 raise NotImplementedError()
1808 def from_data(self, fields, rows):
1809 """ Conversion method from OpenERP's export data to whatever the
1810 current export class outputs
1812 :params list fields: a list of fields to export
1813 :params list rows: a list of records to export
1817 raise NotImplementedError()
1819 @openerpweb.httprequest
1820 def index(self, req, data, token):
1821 model, fields, ids, domain, import_compat = \
1822 operator.itemgetter('model', 'fields', 'ids', 'domain',
1824 simplejson.loads(data))
1826 context = req.session.eval_context(req.context)
1827 Model = req.session.model(model)
1828 ids = ids or Model.search(domain, 0, False, False, context)
1830 field_names = map(operator.itemgetter('name'), fields)
1831 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1834 columns_headers = field_names
1836 columns_headers = [val['label'].strip() for val in fields]
1839 return req.make_response(self.from_data(columns_headers, import_data),
1840 headers=[('Content-Disposition',
1841 content_disposition(self.filename(model), req)),
1842 ('Content-Type', self.content_type)],
1843 cookies={'fileToken': int(token)})
1845 class CSVExport(Export):
1846 _cp_path = '/web/export/csv'
1847 fmt = {'tag': 'csv', 'label': 'CSV'}
1850 def content_type(self):
1851 return 'text/csv;charset=utf8'
1853 def filename(self, base):
1854 return base + '.csv'
1856 def from_data(self, fields, rows):
1858 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1860 writer.writerow([name.encode('utf-8') for name in fields])
1865 if isinstance(d, basestring):
1866 d = d.replace('\n',' ').replace('\t',' ')
1868 d = d.encode('utf-8')
1869 except UnicodeError:
1871 if d is False: d = None
1873 writer.writerow(row)
1880 class ExcelExport(Export):
1881 _cp_path = '/web/export/xls'
1885 'error': None if xlwt else "XLWT required"
1889 def content_type(self):
1890 return 'application/vnd.ms-excel'
1892 def filename(self, base):
1893 return base + '.xls'
1895 def from_data(self, fields, rows):
1896 workbook = xlwt.Workbook()
1897 worksheet = workbook.add_sheet('Sheet 1')
1899 for i, fieldname in enumerate(fields):
1900 worksheet.write(0, i, fieldname)
1901 worksheet.col(i).width = 8000 # around 220 pixels
1903 style = xlwt.easyxf('align: wrap yes')
1905 for row_index, row in enumerate(rows):
1906 for cell_index, cell_value in enumerate(row):
1907 if isinstance(cell_value, basestring):
1908 cell_value = re.sub("\r", " ", cell_value)
1909 if cell_value is False: cell_value = None
1910 worksheet.write(row_index + 1, cell_index, cell_value, style)
1919 class Reports(View):
1920 _cp_path = "/web/report"
1921 POLLING_DELAY = 0.25
1923 'doc': 'application/vnd.ms-word',
1924 'html': 'text/html',
1925 'odt': 'application/vnd.oasis.opendocument.text',
1926 'pdf': 'application/pdf',
1927 'sxw': 'application/vnd.sun.xml.writer',
1928 'xls': 'application/vnd.ms-excel',
1931 @openerpweb.httprequest
1932 def index(self, req, action, token):
1933 action = simplejson.loads(action)
1935 report_srv = req.session.proxy("report")
1936 context = req.session.eval_context(
1937 nonliterals.CompoundContext(
1938 req.context or {}, action[ "context"]))
1941 report_ids = context["active_ids"]
1942 if 'report_type' in action:
1943 report_data['report_type'] = action['report_type']
1944 if 'datas' in action:
1945 if 'ids' in action['datas']:
1946 report_ids = action['datas'].pop('ids')
1947 report_data.update(action['datas'])
1949 report_id = report_srv.report(
1950 req.session._db, req.session._uid, req.session._password,
1951 action["report_name"], report_ids,
1952 report_data, context)
1954 report_struct = None
1956 report_struct = report_srv.report_get(
1957 req.session._db, req.session._uid, req.session._password, report_id)
1958 if report_struct["state"]:
1961 time.sleep(self.POLLING_DELAY)
1963 report = base64.b64decode(report_struct['result'])
1964 if report_struct.get('code') == 'zlib':
1965 report = zlib.decompress(report)
1966 report_mimetype = self.TYPES_MAPPING.get(
1967 report_struct['format'], 'octet-stream')
1968 file_name = action.get('name', 'report')
1969 if 'name' not in action:
1970 reports = req.session.model('ir.actions.report.xml')
1971 res_id = reports.search([('report_name', '=', action['report_name']),],
1972 0, False, False, context)
1974 file_name = reports.read(res_id[0], ['name'], context)['name']
1976 file_name = action['report_name']
1977 file_name = '%s.%s' % (file_name, report_struct['format'])
1979 return req.make_response(report,
1981 ('Content-Disposition', content_disposition(file_name, req)),
1982 ('Content-Type', report_mimetype),
1983 ('Content-Length', len(report))],
1984 cookies={'fileToken': int(token)})
1986 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: