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, context))
371 for id, name, action in actions]
373 def clean_action(req, action, context, do_not_eval=False):
374 action.setdefault('flags', {})
376 context = context or {}
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):
748 return req.session.proxy('common').version()['openerp']
750 class Proxy(openerpweb.Controller):
751 _cp_path = '/web/proxy'
753 @openerpweb.jsonrequest
754 def load(self, req, path):
755 """ Proxies an HTTP request through a JSON request.
757 It is strongly recommended to not request binary files through this,
758 as the result will be a binary data blob as well.
760 :param req: OpenERP request
761 :param path: actual request path
762 :return: file content
764 from werkzeug.test import Client
765 from werkzeug.wrappers import BaseResponse
767 return Client(req.httprequest.app, BaseResponse).get(path).data
769 class Database(openerpweb.Controller):
770 _cp_path = "/web/database"
772 @openerpweb.jsonrequest
773 def get_list(self, req):
776 @openerpweb.jsonrequest
777 def create(self, req, fields):
778 params = dict(map(operator.itemgetter('name', 'value'), fields))
779 return req.session.proxy("db").create_database(
780 params['super_admin_pwd'],
782 bool(params.get('demo_data')),
784 params['create_admin_pwd'])
786 @openerpweb.jsonrequest
787 def duplicate(self, req, fields):
788 params = dict(map(operator.itemgetter('name', 'value'), fields))
789 return req.session.proxy("db").duplicate_database(
790 params['super_admin_pwd'],
791 params['db_original_name'],
794 @openerpweb.jsonrequest
795 def duplicate(self, req, fields):
796 params = dict(map(operator.itemgetter('name', 'value'), fields))
798 params['super_admin_pwd'],
799 params['db_original_name'],
803 return req.session.proxy("db").duplicate_database(*duplicate_attrs)
805 @openerpweb.jsonrequest
806 def drop(self, req, fields):
807 password, db = operator.itemgetter(
808 'drop_pwd', 'drop_db')(
809 dict(map(operator.itemgetter('name', 'value'), fields)))
812 return req.session.proxy("db").drop(password, db)
813 except xmlrpclib.Fault, e:
814 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
815 return {'error': e.faultCode, 'title': 'Drop Database'}
816 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
818 @openerpweb.httprequest
819 def backup(self, req, backup_db, backup_pwd, token):
821 db_dump = base64.b64decode(
822 req.session.proxy("db").dump(backup_pwd, backup_db))
823 filename = "%(db)s_%(timestamp)s.dump" % {
825 'timestamp': datetime.datetime.utcnow().strftime(
826 "%Y-%m-%d_%H-%M-%SZ")
828 return req.make_response(db_dump,
829 [('Content-Type', 'application/octet-stream; charset=binary'),
830 ('Content-Disposition', content_disposition(filename, req))],
831 {'fileToken': int(token)}
833 except xmlrpclib.Fault, e:
834 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
836 @openerpweb.httprequest
837 def restore(self, req, db_file, restore_pwd, new_db):
839 data = base64.b64encode(db_file.read())
840 req.session.proxy("db").restore(restore_pwd, new_db, data)
842 except xmlrpclib.Fault, e:
843 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
844 raise Exception("AccessDenied")
846 @openerpweb.jsonrequest
847 def change_password(self, req, fields):
848 old_password, new_password = operator.itemgetter(
849 'old_pwd', 'new_pwd')(
850 dict(map(operator.itemgetter('name', 'value'), fields)))
852 return req.session.proxy("db").change_admin_password(old_password, new_password)
853 except xmlrpclib.Fault, e:
854 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
855 return {'error': e.faultCode, 'title': 'Change Password'}
856 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
858 class Session(openerpweb.Controller):
859 _cp_path = "/web/session"
861 def session_info(self, req):
862 req.session.ensure_valid()
864 "session_id": req.session_id,
865 "uid": req.session._uid,
866 "context": req.session.get_context() if req.session._uid else {},
867 "db": req.session._db,
868 "login": req.session._login,
871 @openerpweb.jsonrequest
872 def get_session_info(self, req):
873 return self.session_info(req)
875 @openerpweb.jsonrequest
876 def authenticate(self, req, db, login, password, base_location=None):
877 wsgienv = req.httprequest.environ
879 base_location=base_location,
880 HTTP_HOST=wsgienv['HTTP_HOST'],
881 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
883 req.session.authenticate(db, login, password, env)
885 return self.session_info(req)
887 @openerpweb.jsonrequest
888 def change_password (self,req,fields):
889 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
890 dict(map(operator.itemgetter('name', 'value'), fields)))
891 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
892 return {'error':'All passwords have to be filled.','title': 'Change Password'}
893 if new_password != confirm_password:
894 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
896 if req.session.model('res.users').change_password(
897 old_password, new_password):
898 return {'new_password':new_password}
900 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
901 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
903 @openerpweb.jsonrequest
904 def sc_list(self, req):
905 return req.session.model('ir.ui.view_sc').get_sc(
906 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
908 @openerpweb.jsonrequest
909 def get_lang_list(self, req):
911 return req.session.proxy("db").list_lang() or []
913 return {"error": e, "title": "Languages"}
915 @openerpweb.jsonrequest
916 def modules(self, req):
917 # return all installed modules. Web client is smart enough to not load a module twice
918 return module_installed(req)
920 @openerpweb.jsonrequest
921 def eval_domain_and_context(self, req, contexts, domains,
923 """ Evaluates sequences of domains and contexts, composing them into
924 a single context, domain or group_by sequence.
926 :param list contexts: list of contexts to merge together. Contexts are
927 evaluated in sequence, all previous contexts
928 are part of their own evaluation context
929 (starting at the session context).
930 :param list domains: list of domains to merge together. Domains are
931 evaluated in sequence and appended to one another
932 (implicit AND), their evaluation domain is the
933 result of merging all contexts.
934 :param list group_by_seq: list of domains (which may be in a different
935 order than the ``contexts`` parameter),
936 evaluated in sequence, their ``'group_by'``
937 key is extracted if they have one.
942 the global context created by merging all of
946 the concatenation of all domains
949 a list of fields to group by, potentially empty (in which case
950 no group by should be performed)
952 context, domain = eval_context_and_domain(req.session,
953 nonliterals.CompoundContext(*(contexts or [])),
954 nonliterals.CompoundDomain(*(domains or [])))
956 group_by_sequence = []
957 for candidate in (group_by_seq or []):
958 ctx = req.session.eval_context(candidate, context)
959 group_by = ctx.get('group_by')
962 elif isinstance(group_by, basestring):
963 group_by_sequence.append(group_by)
965 group_by_sequence.extend(group_by)
970 'group_by': group_by_sequence
973 @openerpweb.jsonrequest
974 def save_session_action(self, req, the_action):
976 This method store an action object in the session object and returns an integer
977 identifying that action. The method get_session_action() can be used to get
980 :param the_action: The action to save in the session.
981 :type the_action: anything
982 :return: A key identifying the saved action.
985 saved_actions = req.httpsession.get('saved_actions')
986 if not saved_actions:
987 saved_actions = {"next":0, "actions":{}}
988 req.httpsession['saved_actions'] = saved_actions
989 # we don't allow more than 10 stored actions
990 if len(saved_actions["actions"]) >= 10:
991 del saved_actions["actions"][min(saved_actions["actions"])]
992 key = saved_actions["next"]
993 saved_actions["actions"][key] = the_action
994 saved_actions["next"] = key + 1
997 @openerpweb.jsonrequest
998 def get_session_action(self, req, key):
1000 Gets back a previously saved action. This method can return None if the action
1001 was saved since too much time (this case should be handled in a smart way).
1003 :param key: The key given by save_session_action()
1005 :return: The saved action or None.
1008 saved_actions = req.httpsession.get('saved_actions')
1009 if not saved_actions:
1011 return saved_actions["actions"].get(key)
1013 @openerpweb.jsonrequest
1014 def check(self, req):
1015 req.session.assert_valid()
1018 @openerpweb.jsonrequest
1019 def destroy(self, req):
1020 req.session._suicide = True
1022 class Menu(openerpweb.Controller):
1023 _cp_path = "/web/menu"
1025 @openerpweb.jsonrequest
1026 def load(self, req):
1027 return {'data': self.do_load(req)}
1029 @openerpweb.jsonrequest
1030 def get_user_roots(self, req):
1031 return self.do_get_user_roots(req)
1033 def do_get_user_roots(self, req):
1034 """ Return all root menu ids visible for the session user.
1036 :param req: A request object, with an OpenERP session attribute
1037 :type req: < session -> OpenERPSession >
1038 :return: the root menu ids
1042 context = s.eval_context(req.context)
1043 Menus = s.model('ir.ui.menu')
1044 # If a menu action is defined use its domain to get the root menu items
1045 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1047 menu_domain = [('parent_id', '=', False)]
1049 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1051 menu_domain = ast.literal_eval(domain_string)
1053 return Menus.search(menu_domain, 0, False, False, context)
1055 def do_load(self, req):
1056 """ Loads all menu items (all applications and their sub-menus).
1058 :param req: A request object, with an OpenERP session attribute
1059 :type req: < session -> OpenERPSession >
1060 :return: the menu root
1061 :rtype: dict('children': menu_nodes)
1063 context = req.session.eval_context(req.context)
1064 Menus = req.session.model('ir.ui.menu')
1066 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1067 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1069 # menus are loaded fully unlike a regular tree view, cause there are a
1070 # limited number of items (752 when all 6.1 addons are installed)
1071 menu_ids = Menus.search([], 0, False, False, context)
1072 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1073 # adds roots at the end of the sequence, so that they will overwrite
1074 # equivalent menu items from full menu read when put into id:item
1075 # mapping, resulting in children being correctly set on the roots.
1076 menu_items.extend(menu_roots)
1078 # make a tree using parent_id
1079 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1080 for menu_item in menu_items:
1081 if menu_item['parent_id']:
1082 parent = menu_item['parent_id'][0]
1085 if parent in menu_items_map:
1086 menu_items_map[parent].setdefault(
1087 'children', []).append(menu_item)
1089 # sort by sequence a tree using parent_id
1090 for menu_item in menu_items:
1091 menu_item.setdefault('children', []).sort(
1092 key=operator.itemgetter('sequence'))
1096 @openerpweb.jsonrequest
1097 def action(self, req, menu_id):
1098 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1099 [('ir.ui.menu', menu_id)], False)
1100 return {"action": actions}
1102 class DataSet(openerpweb.Controller):
1103 _cp_path = "/web/dataset"
1105 @openerpweb.jsonrequest
1106 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1107 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1108 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1110 """ Performs a search() followed by a read() (if needed) using the
1111 provided search criteria
1113 :param req: a JSON-RPC request object
1114 :type req: openerpweb.JsonRequest
1115 :param str model: the name of the model to search on
1116 :param fields: a list of the fields to return in the result records
1118 :param int offset: from which index should the results start being returned
1119 :param int limit: the maximum number of records to return
1120 :param list domain: the search domain for the query
1121 :param list sort: sorting directives
1122 :returns: A structure (dict) with two keys: ids (all the ids matching
1123 the (domain, context) pair) and records (paginated records
1124 matching fields selection set)
1127 Model = req.session.model(model)
1129 context, domain = eval_context_and_domain(
1130 req.session, req.context, domain)
1132 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1133 if limit and len(ids) == limit:
1134 length = Model.search_count(domain, context)
1136 length = len(ids) + (offset or 0)
1137 if fields and fields == ['id']:
1138 # shortcut read if we only want the ids
1141 'records': [{'id': id} for id in ids]
1144 records = Model.read(ids, fields or False, context)
1145 records.sort(key=lambda obj: ids.index(obj['id']))
1151 @openerpweb.jsonrequest
1152 def load(self, req, model, id, fields):
1153 m = req.session.model(model)
1155 r = m.read([id], False, req.session.eval_context(req.context))
1158 return {'value': value}
1160 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1161 has_domain = domain_id is not None and domain_id < len(args)
1162 has_context = context_id is not None and context_id < len(args)
1164 domain = args[domain_id] if has_domain else []
1165 context = args[context_id] if has_context else {}
1166 c, d = eval_context_and_domain(req.session, context, domain)
1170 args[context_id] = c
1172 return self._call_kw(req, model, method, args, {})
1174 def _call_kw(self, req, model, method, args, kwargs):
1175 for i in xrange(len(args)):
1176 if isinstance(args[i], nonliterals.BaseContext):
1177 args[i] = req.session.eval_context(args[i])
1178 elif isinstance(args[i], nonliterals.BaseDomain):
1179 args[i] = req.session.eval_domain(args[i])
1180 for k in kwargs.keys():
1181 if isinstance(kwargs[k], nonliterals.BaseContext):
1182 kwargs[k] = req.session.eval_context(kwargs[k])
1183 elif isinstance(kwargs[k], nonliterals.BaseDomain):
1184 kwargs[k] = req.session.eval_domain(kwargs[k])
1186 # Temporary implements future display_name special field for model#read()
1187 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1188 if 'display_name' in args[1]:
1189 names = req.session.model(model).name_get(args[0], **kwargs)
1190 args[1].remove('display_name')
1191 r = getattr(req.session.model(model), method)(*args, **kwargs)
1192 for i in range(len(r)):
1193 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1196 return getattr(req.session.model(model), method)(*args, **kwargs)
1198 @openerpweb.jsonrequest
1199 def onchange(self, req, model, method, args, context_id=None):
1200 """ Support method for handling onchange calls: behaves much like call
1201 with the following differences:
1203 * Does not take a domain_id
1204 * Is aware of the return value's structure, and will parse the domains
1205 if needed in order to return either parsed literal domains (in JSON)
1206 or non-literal domain instances, allowing those domains to be used
1210 :type req: web.common.http.JsonRequest
1211 :param str model: object type on which to call the method
1212 :param str method: name of the onchange handler method
1213 :param list args: arguments to call the onchange handler with
1214 :param int context_id: index of the context object in the list of
1216 :return: result of the onchange call with all domains parsed
1218 result = self.call_common(req, model, method, args, context_id=context_id)
1219 if not result or 'domain' not in result:
1222 result['domain'] = dict(
1223 (k, parse_domain(v, req.session))
1224 for k, v in result['domain'].iteritems())
1228 @openerpweb.jsonrequest
1229 def call(self, req, model, method, args, domain_id=None, context_id=None):
1230 return self.call_common(req, model, method, args, domain_id, context_id)
1232 @openerpweb.jsonrequest
1233 def call_kw(self, req, model, method, args, kwargs):
1234 return self._call_kw(req, model, method, args, kwargs)
1236 @openerpweb.jsonrequest
1237 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1238 context = req.session.eval_context(req.context)
1239 action = self.call_common(req, model, method, args, domain_id, context_id)
1240 if isinstance(action, dict) and action.get('type') != '':
1241 return clean_action(req, action, context)
1244 @openerpweb.jsonrequest
1245 def exec_workflow(self, req, model, id, signal):
1246 return req.session.exec_workflow(model, id, signal)
1248 @openerpweb.jsonrequest
1249 def resequence(self, req, model, ids, field='sequence', offset=0):
1250 """ Re-sequences a number of records in the model, by their ids
1252 The re-sequencing starts at the first model of ``ids``, the sequence
1253 number is incremented by one after each record and starts at ``offset``
1255 :param ids: identifiers of the records to resequence, in the new sequence order
1257 :param str field: field used for sequence specification, defaults to
1259 :param int offset: sequence number for first record in ``ids``, allows
1260 starting the resequencing from an arbitrary number,
1263 m = req.session.model(model)
1264 if not m.fields_get([field]):
1266 # python 2.6 has no start parameter
1267 for i, id in enumerate(ids):
1268 m.write(id, { field: i + offset })
1271 class View(openerpweb.Controller):
1272 _cp_path = "/web/view"
1274 def fields_view_get(self, req, model, view_id, view_type,
1275 transform=True, toolbar=False, submenu=False):
1276 Model = req.session.model(model)
1277 context = req.session.eval_context(req.context)
1278 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1279 # todo fme?: check that we should pass the evaluated context here
1280 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1281 if toolbar and transform:
1282 self.process_toolbar(req, fvg['toolbar'])
1285 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1286 # depending on how it feels, xmlrpclib.ServerProxy can translate
1287 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1288 # enjoy unicode strings which can not be trivially converted to
1289 # strings, and it blows up during parsing.
1291 # So ensure we fix this retardation by converting view xml back to
1293 if isinstance(fvg['arch'], unicode):
1294 arch = fvg['arch'].encode('utf-8')
1297 fvg['arch_string'] = arch
1300 evaluation_context = session.evaluation_context(context or {})
1301 xml = self.transform_view(arch, session, evaluation_context)
1303 xml = ElementTree.fromstring(arch)
1304 fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
1306 if 'id' in fvg['fields']:
1307 # Special case for id's
1308 id_field = fvg['fields']['id']
1309 id_field['original_type'] = id_field['type']
1310 id_field['type'] = 'id'
1312 for field in fvg['fields'].itervalues():
1313 if field.get('views'):
1314 for view in field["views"].itervalues():
1315 self.process_view(session, view, None, transform)
1316 if field.get('domain'):
1317 field["domain"] = parse_domain(field["domain"], session)
1318 if field.get('context'):
1319 field["context"] = parse_context(field["context"], session)
1321 def process_toolbar(self, req, toolbar):
1323 The toolbar is a mapping of section_key: [action_descriptor]
1325 We need to clean all those actions in order to ensure correct
1328 for actions in toolbar.itervalues():
1329 for action in actions:
1330 if 'context' in action:
1331 action['context'] = parse_context(
1332 action['context'], req.session)
1333 if 'domain' in action:
1334 action['domain'] = parse_domain(
1335 action['domain'], req.session)
1337 @openerpweb.jsonrequest
1338 def add_custom(self, req, view_id, arch):
1339 CustomView = req.session.model('ir.ui.view.custom')
1341 'user_id': req.session._uid,
1344 }, req.session.eval_context(req.context))
1345 return {'result': True}
1347 @openerpweb.jsonrequest
1348 def undo_custom(self, req, view_id, reset=False):
1349 CustomView = req.session.model('ir.ui.view.custom')
1350 context = req.session.eval_context(req.context)
1351 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1352 0, False, False, context)
1355 CustomView.unlink(vcustom, context)
1357 CustomView.unlink([vcustom[0]], context)
1358 return {'result': True}
1359 return {'result': False}
1361 def transform_view(self, view_string, session, context=None):
1362 # transform nodes on the fly via iterparse, instead of
1363 # doing it statically on the parsing result
1364 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1366 for event, elem in parser:
1367 if event == "start":
1370 self.parse_domains_and_contexts(elem, session)
1373 def parse_domains_and_contexts(self, elem, session):
1374 """ Converts domains and contexts from the view into Python objects,
1375 either literals if they can be parsed by literal_eval or a special
1376 placeholder object if the domain or context refers to free variables.
1378 :param elem: the current node being parsed
1379 :type param: xml.etree.ElementTree.Element
1380 :param session: OpenERP session object, used to store and retrieve
1382 :type session: openerpweb.openerpweb.OpenERPSession
1384 for el in ['domain', 'filter_domain']:
1385 domain = elem.get(el, '').strip()
1387 elem.set(el, parse_domain(domain, session))
1388 elem.set(el + '_string', domain)
1389 for el in ['context', 'default_get']:
1390 context_string = elem.get(el, '').strip()
1392 elem.set(el, parse_context(context_string, session))
1393 elem.set(el + '_string', context_string)
1395 @openerpweb.jsonrequest
1396 def load(self, req, model, view_id, view_type, toolbar=False):
1397 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1399 class TreeView(View):
1400 _cp_path = "/web/treeview"
1402 @openerpweb.jsonrequest
1403 def action(self, req, model, id):
1404 return load_actions_from_ir_values(
1405 req,'action', 'tree_but_open',[(model, id)],
1408 class SearchView(View):
1409 _cp_path = "/web/searchview"
1411 @openerpweb.jsonrequest
1412 def load(self, req, model, view_id):
1413 fields_view = self.fields_view_get(req, model, view_id, 'search')
1414 return {'fields_view': fields_view}
1416 @openerpweb.jsonrequest
1417 def fields_get(self, req, model):
1418 Model = req.session.model(model)
1419 fields = Model.fields_get(False, req.session.eval_context(req.context))
1420 for field in fields.values():
1421 # shouldn't convert the views too?
1422 if field.get('domain'):
1423 field["domain"] = parse_domain(field["domain"], req.session)
1424 if field.get('context'):
1425 field["context"] = parse_context(field["context"], req.session)
1426 return {'fields': fields}
1428 @openerpweb.jsonrequest
1429 def get_filters(self, req, model):
1430 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1431 Model = req.session.model("ir.filters")
1432 filters = Model.get_filters(model)
1433 for filter in filters:
1435 parsed_context = parse_context(filter["context"], req.session)
1436 filter["context"] = (parsed_context
1437 if not isinstance(parsed_context, nonliterals.BaseContext)
1438 else req.session.eval_context(parsed_context))
1440 parsed_domain = parse_domain(filter["domain"], req.session)
1441 filter["domain"] = (parsed_domain
1442 if not isinstance(parsed_domain, nonliterals.BaseDomain)
1443 else req.session.eval_domain(parsed_domain))
1445 logger.exception("Failed to parse custom filter %s in %s",
1446 filter['name'], model)
1447 filter['disabled'] = True
1448 del filter['context']
1449 del filter['domain']
1452 class Binary(openerpweb.Controller):
1453 _cp_path = "/web/binary"
1455 @openerpweb.httprequest
1456 def image(self, req, model, id, field, **kw):
1457 last_update = '__last_update'
1458 Model = req.session.model(model)
1459 context = req.session.eval_context(req.context)
1460 headers = [('Content-Type', 'image/png')]
1461 etag = req.httprequest.headers.get('If-None-Match')
1462 hashed_session = hashlib.md5(req.session_id).hexdigest()
1463 id = None if not id else simplejson.loads(id)
1464 if type(id) is list:
1467 if not id and hashed_session == etag:
1468 return werkzeug.wrappers.Response(status=304)
1470 date = Model.read([id], [last_update], context)[0].get(last_update)
1471 if hashlib.md5(date).hexdigest() == etag:
1472 return werkzeug.wrappers.Response(status=304)
1474 retag = hashed_session
1477 res = Model.default_get([field], context).get(field)
1480 res = Model.read([id], [last_update, field], context)[0]
1481 retag = hashlib.md5(res.get(last_update)).hexdigest()
1482 image_base64 = res.get(field)
1484 if kw.get('resize'):
1485 resize = kw.get('resize').split(',');
1486 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1487 width = int(resize[0])
1488 height = int(resize[1])
1489 # resize maximum 500*500
1490 if width > 500: width = 500
1491 if height > 500: height = 500
1492 image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1494 image_data = base64.b64decode(image_base64)
1496 except (TypeError, xmlrpclib.Fault):
1497 image_data = self.placeholder(req)
1498 headers.append(('ETag', retag))
1499 headers.append(('Content-Length', len(image_data)))
1501 ncache = int(kw.get('cache'))
1502 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1505 return req.make_response(image_data, headers)
1506 def placeholder(self, req):
1507 addons_path = openerpweb.addons_manifest['web']['addons_path']
1508 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1510 @openerpweb.httprequest
1511 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1512 """ Download link for files stored as binary fields.
1514 If the ``id`` parameter is omitted, fetches the default value for the
1515 binary field (via ``default_get``), otherwise fetches the field for
1516 that precise record.
1518 :param req: OpenERP request
1519 :type req: :class:`web.common.http.HttpRequest`
1520 :param str model: name of the model to fetch the binary from
1521 :param str field: binary field
1522 :param str id: id of the record from which to fetch the binary
1523 :param str filename_field: field holding the file's name, if any
1524 :returns: :class:`werkzeug.wrappers.Response`
1526 Model = req.session.model(model)
1527 context = req.session.eval_context(req.context)
1530 fields.append(filename_field)
1532 res = Model.read([int(id)], fields, context)[0]
1534 res = Model.default_get(fields, context)
1535 filecontent = base64.b64decode(res.get(field, ''))
1537 return req.not_found()
1539 filename = '%s_%s' % (model.replace('.', '_'), id)
1541 filename = res.get(filename_field, '') or filename
1542 return req.make_response(filecontent,
1543 [('Content-Type', 'application/octet-stream'),
1544 ('Content-Disposition', content_disposition(filename, req))])
1546 @openerpweb.httprequest
1547 def saveas_ajax(self, req, data, token):
1548 jdata = simplejson.loads(data)
1549 model = jdata['model']
1550 field = jdata['field']
1551 id = jdata.get('id', None)
1552 filename_field = jdata.get('filename_field', None)
1553 context = jdata.get('context', dict())
1555 context = req.session.eval_context(context)
1556 Model = req.session.model(model)
1559 fields.append(filename_field)
1561 res = Model.read([int(id)], fields, context)[0]
1563 res = Model.default_get(fields, context)
1564 filecontent = base64.b64decode(res.get(field, ''))
1566 raise ValueError("No content found for field '%s' on '%s:%s'" %
1569 filename = '%s_%s' % (model.replace('.', '_'), id)
1571 filename = res.get(filename_field, '') or filename
1572 return req.make_response(filecontent,
1573 headers=[('Content-Type', 'application/octet-stream'),
1574 ('Content-Disposition', content_disposition(filename, req))],
1575 cookies={'fileToken': int(token)})
1577 @openerpweb.httprequest
1578 def upload(self, req, callback, ufile):
1579 # TODO: might be useful to have a configuration flag for max-length file uploads
1581 out = """<script language="javascript" type="text/javascript">
1582 var win = window.top.window;
1583 win.jQuery(win).trigger(%s, %s);
1586 args = [len(data), ufile.filename,
1587 ufile.content_type, base64.b64encode(data)]
1588 except Exception, e:
1589 args = [False, e.message]
1590 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1592 @openerpweb.httprequest
1593 def upload_attachment(self, req, callback, model, id, ufile):
1594 context = req.session.eval_context(req.context)
1595 Model = req.session.model('ir.attachment')
1597 out = """<script language="javascript" type="text/javascript">
1598 var win = window.top.window;
1599 win.jQuery(win).trigger(%s, %s);
1601 attachment_id = Model.create({
1602 'name': ufile.filename,
1603 'datas': base64.encodestring(ufile.read()),
1604 'datas_fname': ufile.filename,
1609 'filename': ufile.filename,
1612 except Exception, e:
1613 args = { 'error': e.message }
1614 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1616 class Action(openerpweb.Controller):
1617 _cp_path = "/web/action"
1619 @openerpweb.jsonrequest
1620 def load(self, req, action_id, do_not_eval=False, eval_context=None):
1621 Actions = req.session.model('ir.actions.actions')
1623 context = req.session.eval_context(req.context)
1624 eval_context = req.session.eval_context(nonliterals.CompoundContext(context, eval_context or {}))
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], eval_context, do_not_eval)
1648 @openerpweb.jsonrequest
1649 def run(self, req, action_id):
1650 context = req.session.eval_context(req.context)
1651 return_action = req.session.model('ir.actions.server').run(
1652 [action_id], req.session.eval_context(req.context))
1654 return clean_action(req, return_action, context)
1659 _cp_path = "/web/export"
1661 @openerpweb.jsonrequest
1662 def formats(self, req):
1663 """ Returns all valid export formats
1665 :returns: for each export format, a pair of identifier and printable name
1666 :rtype: [(str, str)]
1670 for path, controller in openerpweb.controllers_path.iteritems()
1671 if path.startswith(self._cp_path)
1672 if hasattr(controller, 'fmt')
1673 ], key=operator.itemgetter("label"))
1675 def fields_get(self, req, model):
1676 Model = req.session.model(model)
1677 fields = Model.fields_get(False, req.session.eval_context(req.context))
1680 @openerpweb.jsonrequest
1681 def get_fields(self, req, model, prefix='', parent_name= '',
1682 import_compat=True, parent_field_type=None,
1685 if import_compat and parent_field_type == "many2one":
1688 fields = self.fields_get(req, model)
1691 fields.pop('id', None)
1693 fields['.id'] = fields.pop('id', {'string': 'ID'})
1695 fields_sequence = sorted(fields.iteritems(),
1696 key=lambda field: field[1].get('string', ''))
1699 for field_name, field in fields_sequence:
1701 if exclude and field_name in exclude:
1703 if field.get('readonly'):
1704 # If none of the field's states unsets readonly, skip the field
1705 if all(dict(attrs).get('readonly', True)
1706 for attrs in field.get('states', {}).values()):
1709 id = prefix + (prefix and '/'or '') + field_name
1710 name = parent_name + (parent_name and '/' or '') + field['string']
1711 record = {'id': id, 'string': name,
1712 'value': id, 'children': False,
1713 'field_type': field.get('type'),
1714 'required': field.get('required'),
1715 'relation_field': field.get('relation_field')}
1716 records.append(record)
1718 if len(name.split('/')) < 3 and 'relation' in field:
1719 ref = field.pop('relation')
1720 record['value'] += '/id'
1721 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1723 if not import_compat or field['type'] == 'one2many':
1724 # m2m field in import_compat is childless
1725 record['children'] = True
1729 @openerpweb.jsonrequest
1730 def namelist(self,req, model, export_id):
1731 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1732 export = req.session.model("ir.exports").read([export_id])[0]
1733 export_fields_list = req.session.model("ir.exports.line").read(
1734 export['export_fields'])
1736 fields_data = self.fields_info(
1737 req, model, map(operator.itemgetter('name'), export_fields_list))
1740 {'name': field['name'], 'label': fields_data[field['name']]}
1741 for field in export_fields_list
1744 def fields_info(self, req, model, export_fields):
1746 fields = self.fields_get(req, model)
1748 # To make fields retrieval more efficient, fetch all sub-fields of a
1749 # given field at the same time. Because the order in the export list is
1750 # arbitrary, this requires ordering all sub-fields of a given field
1751 # together so they can be fetched at the same time
1753 # Works the following way:
1754 # * sort the list of fields to export, the default sorting order will
1755 # put the field itself (if present, for xmlid) and all of its
1756 # sub-fields right after it
1757 # * then, group on: the first field of the path (which is the same for
1758 # a field and for its subfields and the length of splitting on the
1759 # first '/', which basically means grouping the field on one side and
1760 # all of the subfields on the other. This way, we have the field (for
1761 # the xmlid) with length 1, and all of the subfields with the same
1762 # base but a length "flag" of 2
1763 # * if we have a normal field (length 1), just add it to the info
1764 # mapping (with its string) as-is
1765 # * otherwise, recursively call fields_info via graft_subfields.
1766 # all graft_subfields does is take the result of fields_info (on the
1767 # field's model) and prepend the current base (current field), which
1768 # rebuilds the whole sub-tree for the field
1770 # result: because we're not fetching the fields_get for half the
1771 # database models, fetching a namelist with a dozen fields (including
1772 # relational data) falls from ~6s to ~300ms (on the leads model).
1773 # export lists with no sub-fields (e.g. import_compatible lists with
1774 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1775 # there's a single fields_get to execute)
1776 for (base, length), subfields in itertools.groupby(
1777 sorted(export_fields),
1778 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1779 subfields = list(subfields)
1781 # subfields is a seq of $base/*rest, and not loaded yet
1782 info.update(self.graft_subfields(
1783 req, fields[base]['relation'], base, fields[base]['string'],
1787 info[base] = fields[base]['string']
1791 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1792 export_fields = [field.split('/', 1)[1] for field in fields]
1794 (prefix + '/' + k, prefix_string + '/' + v)
1795 for k, v in self.fields_info(req, model, export_fields).iteritems())
1797 #noinspection PyPropertyDefinition
1799 def content_type(self):
1800 """ Provides the format's content type """
1801 raise NotImplementedError()
1803 def filename(self, base):
1804 """ Creates a valid filename for the format (with extension) from the
1805 provided base name (exension-less)
1807 raise NotImplementedError()
1809 def from_data(self, fields, rows):
1810 """ Conversion method from OpenERP's export data to whatever the
1811 current export class outputs
1813 :params list fields: a list of fields to export
1814 :params list rows: a list of records to export
1818 raise NotImplementedError()
1820 @openerpweb.httprequest
1821 def index(self, req, data, token):
1822 model, fields, ids, domain, import_compat = \
1823 operator.itemgetter('model', 'fields', 'ids', 'domain',
1825 simplejson.loads(data))
1827 context = req.session.eval_context(req.context)
1828 Model = req.session.model(model)
1829 ids = ids or Model.search(domain, 0, False, False, context)
1831 field_names = map(operator.itemgetter('name'), fields)
1832 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1835 columns_headers = field_names
1837 columns_headers = [val['label'].strip() for val in fields]
1840 return req.make_response(self.from_data(columns_headers, import_data),
1841 headers=[('Content-Disposition',
1842 content_disposition(self.filename(model), req)),
1843 ('Content-Type', self.content_type)],
1844 cookies={'fileToken': int(token)})
1846 class CSVExport(Export):
1847 _cp_path = '/web/export/csv'
1848 fmt = {'tag': 'csv', 'label': 'CSV'}
1851 def content_type(self):
1852 return 'text/csv;charset=utf8'
1854 def filename(self, base):
1855 return base + '.csv'
1857 def from_data(self, fields, rows):
1859 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1861 writer.writerow([name.encode('utf-8') for name in fields])
1866 if isinstance(d, basestring):
1867 d = d.replace('\n',' ').replace('\t',' ')
1869 d = d.encode('utf-8')
1870 except UnicodeError:
1872 if d is False: d = None
1874 writer.writerow(row)
1881 class ExcelExport(Export):
1882 _cp_path = '/web/export/xls'
1886 'error': None if xlwt else "XLWT required"
1890 def content_type(self):
1891 return 'application/vnd.ms-excel'
1893 def filename(self, base):
1894 return base + '.xls'
1896 def from_data(self, fields, rows):
1897 workbook = xlwt.Workbook()
1898 worksheet = workbook.add_sheet('Sheet 1')
1900 for i, fieldname in enumerate(fields):
1901 worksheet.write(0, i, fieldname)
1902 worksheet.col(i).width = 8000 # around 220 pixels
1904 style = xlwt.easyxf('align: wrap yes')
1906 for row_index, row in enumerate(rows):
1907 for cell_index, cell_value in enumerate(row):
1908 if isinstance(cell_value, basestring):
1909 cell_value = re.sub("\r", " ", cell_value)
1910 if cell_value is False: cell_value = None
1911 worksheet.write(row_index + 1, cell_index, cell_value, style)
1920 class Reports(View):
1921 _cp_path = "/web/report"
1922 POLLING_DELAY = 0.25
1924 'doc': 'application/vnd.ms-word',
1925 'html': 'text/html',
1926 'odt': 'application/vnd.oasis.opendocument.text',
1927 'pdf': 'application/pdf',
1928 'sxw': 'application/vnd.sun.xml.writer',
1929 'xls': 'application/vnd.ms-excel',
1932 @openerpweb.httprequest
1933 def index(self, req, action, token):
1934 action = simplejson.loads(action)
1936 report_srv = req.session.proxy("report")
1937 context = req.session.eval_context(
1938 nonliterals.CompoundContext(
1939 req.context or {}, action[ "context"]))
1942 report_ids = context["active_ids"]
1943 if 'report_type' in action:
1944 report_data['report_type'] = action['report_type']
1945 if 'datas' in action:
1946 if 'ids' in action['datas']:
1947 report_ids = action['datas'].pop('ids')
1948 report_data.update(action['datas'])
1950 report_id = report_srv.report(
1951 req.session._db, req.session._uid, req.session._password,
1952 action["report_name"], report_ids,
1953 report_data, context)
1955 report_struct = None
1957 report_struct = report_srv.report_get(
1958 req.session._db, req.session._uid, req.session._password, report_id)
1959 if report_struct["state"]:
1962 time.sleep(self.POLLING_DELAY)
1964 report = base64.b64decode(report_struct['result'])
1965 if report_struct.get('code') == 'zlib':
1966 report = zlib.decompress(report)
1967 report_mimetype = self.TYPES_MAPPING.get(
1968 report_struct['format'], 'octet-stream')
1969 file_name = action.get('name', 'report')
1970 if 'name' not in action:
1971 reports = req.session.model('ir.actions.report.xml')
1972 res_id = reports.search([('report_name', '=', action['report_name']),],
1973 0, False, False, context)
1975 file_name = reports.read(res_id[0], ['name'], context)['name']
1977 file_name = action['report_name']
1978 file_name = '%s.%s' % (file_name, report_struct['format'])
1980 return req.make_response(report,
1982 ('Content-Disposition', content_disposition(file_name, req)),
1983 ('Content-Type', report_mimetype),
1984 ('Content-Length', len(report))],
1985 cookies={'fileToken': int(token)})
1987 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: