1 # -*- coding: utf-8 -*-
19 from xml.etree import ElementTree
20 from cStringIO import StringIO
22 import babel.messages.pofile
24 import werkzeug.wrappers
33 from .. import nonliterals
36 #----------------------------------------------------------
38 #----------------------------------------------------------
41 """ Minify js with a clever regex.
42 Taken from http://opensource.perlig.de/rjsmin
43 Apache License, Version 2.0 """
45 """ Substitution callback """
46 groups = match.groups()
52 (groups[4] and '\n') or
53 (groups[5] and ' ') or
54 (groups[6] and ' ') or
55 (groups[7] and ' ') or
60 r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
61 r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
62 r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
63 r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
64 r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
65 r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
66 r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
67 r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
68 r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
69 r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
70 r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
71 r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
72 r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
73 r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
74 r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
75 r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
76 r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
77 r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
78 r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
79 r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
80 r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
86 proxy = req.session.proxy("db")
88 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
90 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
91 dbs = [i for i in dbs if re.match(r, i)]
95 # if only one db is listed returns it else return False
100 except xmlrpclib.Fault:
101 # ignore access denied
105 def module_topological_sort(modules):
106 """ Return a list of module names sorted so that their dependencies of the
107 modules are listed before the module itself
109 modules is a dict of {module_name: dependencies}
111 :param modules: modules to sort
116 dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
117 # incoming edge: dependency on other module (if a depends on b, a has an
118 # incoming edge from b, aka there's an edge from b to a)
119 # outgoing edge: other module depending on this one
121 # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
122 #L ← Empty list that will contain the sorted nodes
124 #S ← Set of all nodes with no outgoing edges (modules on which no other
126 S = set(module for module in modules if module not in dependencies)
129 #function visit(node n)
131 #if n has not been visited yet then
135 #change: n not web module, can not be resolved, ignore
136 if n not in modules: return
137 #for each node m with an edge from m to n do (dependencies of n)
143 #for each node n in S do
149 def module_installed(req):
150 # Candidates module the current heuristic is the /static dir
151 loadable = openerpweb.addons_manifest.keys()
154 # Retrieve database installed modules
155 # TODO The following code should move to ir.module.module.list_installed_modules()
156 Modules = req.session.model('ir.module.module')
157 domain = [('state','=','installed'), ('name','in', loadable)]
158 for module in Modules.search_read(domain, ['name', 'dependencies_id']):
159 modules[module['name']] = []
160 deps = module.get('dependencies_id')
162 deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
163 dependencies = [i['name'] for i in deps_read]
164 modules[module['name']] = dependencies
166 sorted_modules = module_topological_sort(modules)
167 return sorted_modules
169 def module_installed_bypass_session(dbname):
170 loadable = openerpweb.addons_manifest.keys()
173 import openerp.modules.registry
174 registry = openerp.modules.registry.RegistryManager.get(dbname)
175 with registry.cursor() as cr:
176 m = registry.get('ir.module.module')
177 # TODO The following code should move to ir.module.module.list_installed_modules()
178 domain = [('state','=','installed'), ('name','in', loadable)]
179 ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
180 for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
181 modules[module['name']] = []
182 deps = module.get('dependencies_id')
184 deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
185 dependencies = [i['name'] for i in deps_read]
186 modules[module['name']] = dependencies
189 sorted_modules = module_topological_sort(modules)
190 return sorted_modules
192 def module_boot(req):
193 server_wide_modules = openerp.conf.server_wide_modules or ['web']
196 for i in server_wide_modules:
197 if i in openerpweb.addons_manifest:
199 monodb = db_monodb(req)
201 dbside = module_installed_bypass_session(monodb)
202 dbside = [i for i in dbside if i not in serverside]
203 addons = serverside + dbside
206 def concat_xml(file_list):
207 """Concatenate xml files
209 :param list(str) file_list: list of files to check
210 :returns: (concatenation_result, checksum)
213 checksum = hashlib.new('sha1')
215 return '', checksum.hexdigest()
218 for fname in file_list:
219 with open(fname, 'rb') as fp:
221 checksum.update(contents)
223 xml = ElementTree.parse(fp).getroot()
226 root = ElementTree.Element(xml.tag)
227 #elif root.tag != xml.tag:
228 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
230 for child in xml.getchildren():
232 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
234 def concat_files(file_list, reader=None, intersperse=""):
235 """ Concatenates contents of all provided files
237 :param list(str) file_list: list of files to check
238 :param function reader: reading procedure for each file
239 :param str intersperse: string to intersperse between file contents
240 :returns: (concatenation_result, checksum)
243 checksum = hashlib.new('sha1')
245 return '', checksum.hexdigest()
249 with open(f, 'rb') as fp:
253 for fname in file_list:
254 contents = reader(fname)
255 checksum.update(contents)
256 files_content.append(contents)
258 files_concat = intersperse.join(files_content)
259 return files_concat, checksum.hexdigest()
263 def concat_js(file_list):
264 content, checksum = concat_files(file_list, intersperse=';')
265 if checksum in concat_js_cache:
266 content = concat_js_cache[checksum]
268 content = rjsmin(content)
269 concat_js_cache[checksum] = content
270 return content, checksum
273 """convert FS path into web path"""
274 return '/'.join(path.split(os.path.sep))
276 def manifest_glob(req, addons, key):
278 addons = module_boot(req)
280 addons = addons.split(',')
283 manifest = openerpweb.addons_manifest.get(addon, None)
286 # ensure does not ends with /
287 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
288 globlist = manifest.get(key, [])
289 for pattern in globlist:
290 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
291 r.append((path, fs2web(path[len(addons_path):])))
294 def manifest_list(req, mods, extension):
296 path = '/web/webclient/' + extension
298 path += '?mods=' + mods
300 files = manifest_glob(req, mods, extension)
301 i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
302 req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
304 return [wp for _fp, wp in files]
306 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
308 def get_last_modified(files):
309 """ Returns the modification time of the most recently modified
312 :param list(str) files: names of files to check
313 :return: most recent modification time amongst the fileset
314 :rtype: datetime.datetime
318 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
320 return datetime.datetime(1970, 1, 1)
322 def make_conditional(req, response, last_modified=None, etag=None):
323 """ Makes the provided response conditional based upon the request,
324 and mandates revalidation from clients
326 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
327 setting ``last_modified`` and ``etag`` correctly on the response object
329 :param req: OpenERP request
330 :type req: web.common.http.WebRequest
331 :param response: Werkzeug response
332 :type response: werkzeug.wrappers.Response
333 :param datetime.datetime last_modified: last modification date of the response content
334 :param str etag: some sort of checksum of the content (deep etag)
335 :return: the response object provided
336 :rtype: werkzeug.wrappers.Response
338 response.cache_control.must_revalidate = True
339 response.cache_control.max_age = 0
341 response.last_modified = last_modified
343 response.set_etag(etag)
344 return response.make_conditional(req.httprequest)
346 def login_and_redirect(req, db, login, key, redirect_url='/'):
347 req.session.authenticate(db, login, key, {})
348 return set_cookie_and_redirect(req, redirect_url)
350 def set_cookie_and_redirect(req, redirect_url):
351 redirect = werkzeug.utils.redirect(redirect_url, 303)
352 redirect.autocorrect_location_header = False
353 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
354 redirect.set_cookie('instance0|session_id', cookie_val)
357 def eval_context_and_domain(session, context, domain=None):
358 e_context = session.eval_context(context)
359 # should we give the evaluated context as an evaluation context to the domain?
360 e_domain = session.eval_domain(domain or [])
362 return e_context, e_domain
364 def load_actions_from_ir_values(req, key, key2, models, meta):
365 context = req.session.eval_context(req.context)
366 Values = req.session.model('ir.values')
367 actions = Values.get(key, key2, models, meta, context)
369 return [(id, name, clean_action(req, action))
370 for id, name, action in actions]
372 def clean_action(req, action, do_not_eval=False):
373 action.setdefault('flags', {})
375 context = req.session.eval_context(req.context)
376 eval_ctx = req.session.evaluation_context(context)
379 # values come from the server, we can just eval them
380 if action.get('context') and isinstance(action.get('context'), basestring):
381 action['context'] = eval( action['context'], eval_ctx ) or {}
383 if action.get('domain') and isinstance(action.get('domain'), basestring):
384 action['domain'] = eval( action['domain'], eval_ctx ) or []
386 if 'context' in action:
387 action['context'] = parse_context(action['context'], req.session)
388 if 'domain' in action:
389 action['domain'] = parse_domain(action['domain'], req.session)
391 action_type = action.setdefault('type', 'ir.actions.act_window_close')
392 if action_type == 'ir.actions.act_window':
393 return fix_view_modes(action)
396 # I think generate_views,fix_view_modes should go into js ActionManager
397 def generate_views(action):
399 While the server generates a sequence called "views" computing dependencies
400 between a bunch of stuff for views coming directly from the database
401 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
402 to return custom view dictionaries generated on the fly.
404 In that case, there is no ``views`` key available on the action.
406 Since the web client relies on ``action['views']``, generate it here from
407 ``view_mode`` and ``view_id``.
409 Currently handles two different cases:
411 * no view_id, multiple view_mode
412 * single view_id, single view_mode
414 :param dict action: action descriptor dictionary to generate a views key for
416 view_id = action.get('view_id') or False
417 if isinstance(view_id, (list, tuple)):
420 # providing at least one view mode is a requirement, not an option
421 view_modes = action['view_mode'].split(',')
423 if len(view_modes) > 1:
425 raise ValueError('Non-db action dictionaries should provide '
426 'either multiple view modes or a single view '
427 'mode and an optional view id.\n\n Got view '
428 'modes %r and view id %r for action %r' % (
429 view_modes, view_id, action))
430 action['views'] = [(False, mode) for mode in view_modes]
432 action['views'] = [(view_id, view_modes[0])]
434 def fix_view_modes(action):
435 """ For historical reasons, OpenERP has weird dealings in relation to
436 view_mode and the view_type attribute (on window actions):
438 * one of the view modes is ``tree``, which stands for both list views
440 * the choice is made by checking ``view_type``, which is either
441 ``form`` for a list view or ``tree`` for an actual tree view
443 This methods simply folds the view_type into view_mode by adding a
444 new view mode ``list`` which is the result of the ``tree`` view_mode
445 in conjunction with the ``form`` view_type.
447 TODO: this should go into the doc, some kind of "peculiarities" section
449 :param dict action: an action descriptor
450 :returns: nothing, the action is modified in place
452 if not action.get('views'):
453 generate_views(action)
455 if action.pop('view_type', 'form') != 'form':
458 if 'view_mode' in action:
459 action['view_mode'] = ','.join(
460 mode if mode != 'tree' else 'list'
461 for mode in action['view_mode'].split(','))
463 [id, mode if mode != 'tree' else 'list']
464 for id, mode in action['views']
469 def parse_domain(domain, session):
470 """ Parses an arbitrary string containing a domain, transforms it
471 to either a literal domain or a :class:`nonliterals.Domain`
473 :param domain: the domain to parse, if the domain is not a string it
474 is assumed to be a literal domain and is returned as-is
475 :param session: Current OpenERP session
476 :type session: openerpweb.OpenERPSession
478 if not isinstance(domain, basestring):
481 return ast.literal_eval(domain)
484 return nonliterals.Domain(session, domain)
486 def parse_context(context, session):
487 """ Parses an arbitrary string containing a context, transforms it
488 to either a literal context or a :class:`nonliterals.Context`
490 :param context: the context to parse, if the context is not a string it
491 is assumed to be a literal domain and is returned as-is
492 :param session: Current OpenERP session
493 :type session: openerpweb.OpenERPSession
495 if not isinstance(context, basestring):
498 return ast.literal_eval(context)
500 return nonliterals.Context(session, context)
502 def _local_web_translations(trans_file):
505 with open(trans_file) as t_file:
506 po = babel.messages.pofile.read_po(t_file)
510 if x.id and x.string and "openerp-web" in x.auto_comments:
511 messages.append({'id': x.id, 'string': x.string})
514 def xml2json_from_elementtree(el, preserve_whitespaces=False):
516 Simple and straightforward XML-to-JSON converter in Python
518 http://code.google.com/p/xml2json-direct/
522 ns, name = el.tag.rsplit("}", 1)
524 res["namespace"] = ns[1:]
528 for k, v in el.items():
531 if el.text and (preserve_whitespaces or el.text.strip() != ''):
534 kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
535 if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
536 kids.append(kid.tail)
537 res["children"] = kids
540 def content_disposition(filename, req):
541 filename = filename.encode('utf8')
542 escaped = urllib2.quote(filename)
543 browser = req.httprequest.user_agent.browser
544 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
545 if browser == 'msie' and version < 9:
546 return "attachment; filename=%s" % escaped
547 elif browser == 'safari':
548 return "attachment; filename=%s" % filename
550 return "attachment; filename*=UTF-8''%s" % escaped
553 #----------------------------------------------------------
554 # OpenERP Web web Controllers
555 #----------------------------------------------------------
557 html_template = """<!DOCTYPE html>
558 <html style="height: 100%%">
560 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
561 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
562 <title>OpenERP</title>
563 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
564 <link rel="stylesheet" href="/web/static/src/css/full.css" />
567 <script type="text/javascript">
569 var s = new openerp.init(%(modules)s);
576 <script src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
578 var test = function() {
583 if (window.localStorage && false) {
584 if (! localStorage.getItem("hasShownGFramePopup")) {
586 localStorage.setItem("hasShownGFramePopup", true);
597 class Home(openerpweb.Controller):
600 @openerpweb.httprequest
601 def index(self, req, s_action=None, **kw):
602 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, None, 'js'))
603 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, None, 'css'))
605 r = html_template % {
608 'modules': simplejson.dumps(module_boot(req)),
609 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
613 @openerpweb.httprequest
614 def login(self, req, db, login, key):
615 return login_and_redirect(req, db, login, key)
617 class WebClient(openerpweb.Controller):
618 _cp_path = "/web/webclient"
620 @openerpweb.jsonrequest
621 def csslist(self, req, mods=None):
622 return manifest_list(req, mods, 'css')
624 @openerpweb.jsonrequest
625 def jslist(self, req, mods=None):
626 return manifest_list(req, mods, 'js')
628 @openerpweb.jsonrequest
629 def qweblist(self, req, mods=None):
630 return manifest_list(req, mods, 'qweb')
632 @openerpweb.httprequest
633 def css(self, req, mods=None):
634 files = list(manifest_glob(req, mods, 'css'))
635 last_modified = get_last_modified(f[0] for f in files)
636 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
637 return werkzeug.wrappers.Response(status=304)
639 file_map = dict(files)
641 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
642 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
645 """read the a css file and absolutify all relative uris"""
646 with open(f, 'rb') as fp:
647 data = fp.read().decode('utf-8')
650 web_dir = os.path.dirname(path)
654 r"""@import \1%s/""" % (web_dir,),
660 r"""url(\1%s/""" % (web_dir,),
663 return data.encode('utf-8')
665 content, checksum = concat_files((f[0] for f in files), reader)
667 return make_conditional(
668 req, req.make_response(content, [('Content-Type', 'text/css')]),
669 last_modified, checksum)
671 @openerpweb.httprequest
672 def js(self, req, mods=None):
673 files = [f[0] for f in manifest_glob(req, mods, 'js')]
674 last_modified = get_last_modified(files)
675 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
676 return werkzeug.wrappers.Response(status=304)
678 content, checksum = concat_js(files)
680 return make_conditional(
681 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
682 last_modified, checksum)
684 @openerpweb.httprequest
685 def qweb(self, req, mods=None):
686 files = [f[0] for f in manifest_glob(req, mods, 'qweb')]
687 last_modified = get_last_modified(files)
688 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
689 return werkzeug.wrappers.Response(status=304)
691 content, checksum = concat_xml(files)
693 return make_conditional(
694 req, req.make_response(content, [('Content-Type', 'text/xml')]),
695 last_modified, checksum)
697 @openerpweb.jsonrequest
698 def bootstrap_translations(self, req, mods):
699 """ Load local translations from *.po files, as a temporary solution
700 until we have established a valid session. This is meant only
701 for translating the login page and db management chrome, using
702 the browser's language. """
703 # For performance reasons we only load a single translation, so for
704 # sub-languages (that should only be partially translated) we load the
705 # main language PO instead - that should be enough for the login screen.
706 lang = req.lang.split('_')[0]
708 translations_per_module = {}
709 for addon_name in mods:
710 if openerpweb.addons_manifest[addon_name].get('bootstrap'):
711 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
712 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
713 if not os.path.exists(f_name):
715 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
717 return {"modules": translations_per_module,
718 "lang_parameters": None}
720 @openerpweb.jsonrequest
721 def translations(self, req, mods, lang):
722 res_lang = req.session.model('res.lang')
723 ids = res_lang.search([("code", "=", lang)])
726 lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
727 "grouping", "decimal_point", "thousands_sep"])
729 # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
730 # done server-side when the language is loaded, so we only need to load the user's lang.
731 ir_translation = req.session.model('ir.translation')
732 translations_per_module = {}
733 messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
734 ('comments','like','openerp-web'),('value','!=',False),
736 ['module','src','value','lang'], order='module')
737 for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
738 translations_per_module.setdefault(mod,{'messages':[]})
739 translations_per_module[mod]['messages'].extend({'id': m['src'],
740 'string': m['value']} \
742 return {"modules": translations_per_module,
743 "lang_parameters": lang_params}
745 @openerpweb.jsonrequest
746 def version_info(self, req):
747 return req.session.proxy('common').version()['openerp']
749 class Proxy(openerpweb.Controller):
750 _cp_path = '/web/proxy'
752 @openerpweb.jsonrequest
753 def load(self, req, path):
754 """ Proxies an HTTP request through a JSON request.
756 It is strongly recommended to not request binary files through this,
757 as the result will be a binary data blob as well.
759 :param req: OpenERP request
760 :param path: actual request path
761 :return: file content
763 from werkzeug.test import Client
764 from werkzeug.wrappers import BaseResponse
766 return Client(req.httprequest.app, BaseResponse).get(path).data
768 class Database(openerpweb.Controller):
769 _cp_path = "/web/database"
771 @openerpweb.jsonrequest
772 def get_list(self, req):
775 @openerpweb.jsonrequest
776 def create(self, req, fields):
777 params = dict(map(operator.itemgetter('name', 'value'), fields))
778 return req.session.proxy("db").create_database(
779 params['super_admin_pwd'],
781 bool(params.get('demo_data')),
783 params['create_admin_pwd'])
785 @openerpweb.jsonrequest
786 def duplicate(self, req, fields):
787 params = dict(map(operator.itemgetter('name', 'value'), fields))
788 return req.session.proxy("db").duplicate_database(
789 params['super_admin_pwd'],
790 params['db_original_name'],
793 @openerpweb.jsonrequest
794 def duplicate(self, req, fields):
795 params = dict(map(operator.itemgetter('name', 'value'), fields))
797 params['super_admin_pwd'],
798 params['db_original_name'],
802 return req.session.proxy("db").duplicate_database(*duplicate_attrs)
804 @openerpweb.jsonrequest
805 def drop(self, req, fields):
806 password, db = operator.itemgetter(
807 'drop_pwd', 'drop_db')(
808 dict(map(operator.itemgetter('name', 'value'), fields)))
811 return req.session.proxy("db").drop(password, db)
812 except xmlrpclib.Fault, e:
813 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
814 return {'error': e.faultCode, 'title': 'Drop Database'}
815 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
817 @openerpweb.httprequest
818 def backup(self, req, backup_db, backup_pwd, token):
820 db_dump = base64.b64decode(
821 req.session.proxy("db").dump(backup_pwd, backup_db))
822 filename = "%(db)s_%(timestamp)s.dump" % {
824 'timestamp': datetime.datetime.utcnow().strftime(
825 "%Y-%m-%d_%H-%M-%SZ")
827 return req.make_response(db_dump,
828 [('Content-Type', 'application/octet-stream; charset=binary'),
829 ('Content-Disposition', content_disposition(filename, req))],
830 {'fileToken': int(token)}
832 except xmlrpclib.Fault, e:
833 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
835 @openerpweb.httprequest
836 def restore(self, req, db_file, restore_pwd, new_db):
838 data = base64.b64encode(db_file.read())
839 req.session.proxy("db").restore(restore_pwd, new_db, data)
841 except xmlrpclib.Fault, e:
842 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
843 raise Exception("AccessDenied")
845 @openerpweb.jsonrequest
846 def change_password(self, req, fields):
847 old_password, new_password = operator.itemgetter(
848 'old_pwd', 'new_pwd')(
849 dict(map(operator.itemgetter('name', 'value'), fields)))
851 return req.session.proxy("db").change_admin_password(old_password, new_password)
852 except xmlrpclib.Fault, e:
853 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
854 return {'error': e.faultCode, 'title': 'Change Password'}
855 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
857 class Session(openerpweb.Controller):
858 _cp_path = "/web/session"
860 def session_info(self, req):
861 req.session.ensure_valid()
863 "session_id": req.session_id,
864 "uid": req.session._uid,
865 "context": req.session.get_context() if req.session._uid else {},
866 "db": req.session._db,
867 "login": req.session._login,
870 @openerpweb.jsonrequest
871 def get_session_info(self, req):
872 return self.session_info(req)
874 @openerpweb.jsonrequest
875 def authenticate(self, req, db, login, password, base_location=None):
876 wsgienv = req.httprequest.environ
878 base_location=base_location,
879 HTTP_HOST=wsgienv['HTTP_HOST'],
880 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
882 req.session.authenticate(db, login, password, env)
884 return self.session_info(req)
886 @openerpweb.jsonrequest
887 def change_password (self,req,fields):
888 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
889 dict(map(operator.itemgetter('name', 'value'), fields)))
890 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
891 return {'error':'All passwords have to be filled.','title': 'Change Password'}
892 if new_password != confirm_password:
893 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
895 if req.session.model('res.users').change_password(
896 old_password, new_password):
897 return {'new_password':new_password}
899 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
900 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
902 @openerpweb.jsonrequest
903 def sc_list(self, req):
904 return req.session.model('ir.ui.view_sc').get_sc(
905 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
907 @openerpweb.jsonrequest
908 def get_lang_list(self, req):
910 return req.session.proxy("db").list_lang() or []
912 return {"error": e, "title": "Languages"}
914 @openerpweb.jsonrequest
915 def modules(self, req):
916 # return all installed modules. Web client is smart enough to not load a module twice
917 return module_installed(req)
919 @openerpweb.jsonrequest
920 def eval_domain_and_context(self, req, contexts, domains,
922 """ Evaluates sequences of domains and contexts, composing them into
923 a single context, domain or group_by sequence.
925 :param list contexts: list of contexts to merge together. Contexts are
926 evaluated in sequence, all previous contexts
927 are part of their own evaluation context
928 (starting at the session context).
929 :param list domains: list of domains to merge together. Domains are
930 evaluated in sequence and appended to one another
931 (implicit AND), their evaluation domain is the
932 result of merging all contexts.
933 :param list group_by_seq: list of domains (which may be in a different
934 order than the ``contexts`` parameter),
935 evaluated in sequence, their ``'group_by'``
936 key is extracted if they have one.
941 the global context created by merging all of
945 the concatenation of all domains
948 a list of fields to group by, potentially empty (in which case
949 no group by should be performed)
951 context, domain = eval_context_and_domain(req.session,
952 nonliterals.CompoundContext(*(contexts or [])),
953 nonliterals.CompoundDomain(*(domains or [])))
955 group_by_sequence = []
956 for candidate in (group_by_seq or []):
957 ctx = req.session.eval_context(candidate, context)
958 group_by = ctx.get('group_by')
961 elif isinstance(group_by, basestring):
962 group_by_sequence.append(group_by)
964 group_by_sequence.extend(group_by)
969 'group_by': group_by_sequence
972 @openerpweb.jsonrequest
973 def save_session_action(self, req, the_action):
975 This method store an action object in the session object and returns an integer
976 identifying that action. The method get_session_action() can be used to get
979 :param the_action: The action to save in the session.
980 :type the_action: anything
981 :return: A key identifying the saved action.
984 saved_actions = req.httpsession.get('saved_actions')
985 if not saved_actions:
986 saved_actions = {"next":0, "actions":{}}
987 req.httpsession['saved_actions'] = saved_actions
988 # we don't allow more than 10 stored actions
989 if len(saved_actions["actions"]) >= 10:
990 del saved_actions["actions"][min(saved_actions["actions"])]
991 key = saved_actions["next"]
992 saved_actions["actions"][key] = the_action
993 saved_actions["next"] = key + 1
996 @openerpweb.jsonrequest
997 def get_session_action(self, req, key):
999 Gets back a previously saved action. This method can return None if the action
1000 was saved since too much time (this case should be handled in a smart way).
1002 :param key: The key given by save_session_action()
1004 :return: The saved action or None.
1007 saved_actions = req.httpsession.get('saved_actions')
1008 if not saved_actions:
1010 return saved_actions["actions"].get(key)
1012 @openerpweb.jsonrequest
1013 def check(self, req):
1014 req.session.assert_valid()
1017 @openerpweb.jsonrequest
1018 def destroy(self, req):
1019 req.session._suicide = True
1021 class Menu(openerpweb.Controller):
1022 _cp_path = "/web/menu"
1024 @openerpweb.jsonrequest
1025 def load(self, req):
1026 return {'data': self.do_load(req)}
1028 @openerpweb.jsonrequest
1029 def get_user_roots(self, req):
1030 return self.do_get_user_roots(req)
1032 def do_get_user_roots(self, req):
1033 """ Return all root menu ids visible for the session user.
1035 :param req: A request object, with an OpenERP session attribute
1036 :type req: < session -> OpenERPSession >
1037 :return: the root menu ids
1041 context = s.eval_context(req.context)
1042 Menus = s.model('ir.ui.menu')
1043 # If a menu action is defined use its domain to get the root menu items
1044 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1046 menu_domain = [('parent_id', '=', False)]
1048 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1050 menu_domain = ast.literal_eval(domain_string)
1052 return Menus.search(menu_domain, 0, False, False, context)
1054 def do_load(self, req):
1055 """ Loads all menu items (all applications and their sub-menus).
1057 :param req: A request object, with an OpenERP session attribute
1058 :type req: < session -> OpenERPSession >
1059 :return: the menu root
1060 :rtype: dict('children': menu_nodes)
1062 context = req.session.eval_context(req.context)
1063 Menus = req.session.model('ir.ui.menu')
1065 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1066 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1068 # menus are loaded fully unlike a regular tree view, cause there are a
1069 # limited number of items (752 when all 6.1 addons are installed)
1070 menu_ids = Menus.search([], 0, False, False, context)
1071 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1072 # adds roots at the end of the sequence, so that they will overwrite
1073 # equivalent menu items from full menu read when put into id:item
1074 # mapping, resulting in children being correctly set on the roots.
1075 menu_items.extend(menu_roots)
1077 # make a tree using parent_id
1078 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1079 for menu_item in menu_items:
1080 if menu_item['parent_id']:
1081 parent = menu_item['parent_id'][0]
1084 if parent in menu_items_map:
1085 menu_items_map[parent].setdefault(
1086 'children', []).append(menu_item)
1088 # sort by sequence a tree using parent_id
1089 for menu_item in menu_items:
1090 menu_item.setdefault('children', []).sort(
1091 key=operator.itemgetter('sequence'))
1095 @openerpweb.jsonrequest
1096 def action(self, req, menu_id):
1097 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1098 [('ir.ui.menu', menu_id)], False)
1099 return {"action": actions}
1101 class DataSet(openerpweb.Controller):
1102 _cp_path = "/web/dataset"
1104 @openerpweb.jsonrequest
1105 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1106 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1107 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1109 """ Performs a search() followed by a read() (if needed) using the
1110 provided search criteria
1112 :param req: a JSON-RPC request object
1113 :type req: openerpweb.JsonRequest
1114 :param str model: the name of the model to search on
1115 :param fields: a list of the fields to return in the result records
1117 :param int offset: from which index should the results start being returned
1118 :param int limit: the maximum number of records to return
1119 :param list domain: the search domain for the query
1120 :param list sort: sorting directives
1121 :returns: A structure (dict) with two keys: ids (all the ids matching
1122 the (domain, context) pair) and records (paginated records
1123 matching fields selection set)
1126 Model = req.session.model(model)
1128 context, domain = eval_context_and_domain(
1129 req.session, req.context, domain)
1131 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1132 if limit and len(ids) == limit:
1133 length = Model.search_count(domain, context)
1135 length = len(ids) + (offset or 0)
1136 if fields and fields == ['id']:
1137 # shortcut read if we only want the ids
1140 'records': [{'id': id} for id in ids]
1143 records = Model.read(ids, fields or False, context)
1144 records.sort(key=lambda obj: ids.index(obj['id']))
1150 @openerpweb.jsonrequest
1151 def load(self, req, model, id, fields):
1152 m = req.session.model(model)
1154 r = m.read([id], False, req.session.eval_context(req.context))
1157 return {'value': value}
1159 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1160 has_domain = domain_id is not None and domain_id < len(args)
1161 has_context = context_id is not None and context_id < len(args)
1163 domain = args[domain_id] if has_domain else []
1164 context = args[context_id] if has_context else {}
1165 c, d = eval_context_and_domain(req.session, context, domain)
1169 args[context_id] = c
1171 return self._call_kw(req, model, method, args, {})
1173 def _call_kw(self, req, model, method, args, kwargs):
1174 for i in xrange(len(args)):
1175 if isinstance(args[i], nonliterals.BaseContext):
1176 args[i] = req.session.eval_context(args[i])
1177 elif isinstance(args[i], nonliterals.BaseDomain):
1178 args[i] = req.session.eval_domain(args[i])
1179 for k in kwargs.keys():
1180 if isinstance(kwargs[k], nonliterals.BaseContext):
1181 kwargs[k] = req.session.eval_context(kwargs[k])
1182 elif isinstance(kwargs[k], nonliterals.BaseDomain):
1183 kwargs[k] = req.session.eval_domain(kwargs[k])
1185 # Temporary implements future display_name special field for model#read()
1186 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1187 if 'display_name' in args[1]:
1188 names = req.session.model(model).name_get(args[0], **kwargs)
1189 args[1].remove('display_name')
1190 r = getattr(req.session.model(model), method)(*args, **kwargs)
1191 for i in range(len(r)):
1192 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1195 return getattr(req.session.model(model), method)(*args, **kwargs)
1197 @openerpweb.jsonrequest
1198 def onchange(self, req, model, method, args, context_id=None):
1199 """ Support method for handling onchange calls: behaves much like call
1200 with the following differences:
1202 * Does not take a domain_id
1203 * Is aware of the return value's structure, and will parse the domains
1204 if needed in order to return either parsed literal domains (in JSON)
1205 or non-literal domain instances, allowing those domains to be used
1209 :type req: web.common.http.JsonRequest
1210 :param str model: object type on which to call the method
1211 :param str method: name of the onchange handler method
1212 :param list args: arguments to call the onchange handler with
1213 :param int context_id: index of the context object in the list of
1215 :return: result of the onchange call with all domains parsed
1217 result = self.call_common(req, model, method, args, context_id=context_id)
1218 if not result or 'domain' not in result:
1221 result['domain'] = dict(
1222 (k, parse_domain(v, req.session))
1223 for k, v in result['domain'].iteritems())
1227 @openerpweb.jsonrequest
1228 def call(self, req, model, method, args, domain_id=None, context_id=None):
1229 return self.call_common(req, model, method, args, domain_id, context_id)
1231 @openerpweb.jsonrequest
1232 def call_kw(self, req, model, method, args, kwargs):
1233 return self._call_kw(req, model, method, args, kwargs)
1235 @openerpweb.jsonrequest
1236 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1237 action = self.call_common(req, model, method, args, domain_id, context_id)
1238 if isinstance(action, dict) and action.get('type') != '':
1239 return clean_action(req, action)
1242 @openerpweb.jsonrequest
1243 def exec_workflow(self, req, model, id, signal):
1244 return req.session.exec_workflow(model, id, signal)
1246 @openerpweb.jsonrequest
1247 def resequence(self, req, model, ids, field='sequence', offset=0):
1248 """ Re-sequences a number of records in the model, by their ids
1250 The re-sequencing starts at the first model of ``ids``, the sequence
1251 number is incremented by one after each record and starts at ``offset``
1253 :param ids: identifiers of the records to resequence, in the new sequence order
1255 :param str field: field used for sequence specification, defaults to
1257 :param int offset: sequence number for first record in ``ids``, allows
1258 starting the resequencing from an arbitrary number,
1261 m = req.session.model(model)
1262 if not m.fields_get([field]):
1264 # python 2.6 has no start parameter
1265 for i, id in enumerate(ids):
1266 m.write(id, { field: i + offset })
1269 class View(openerpweb.Controller):
1270 _cp_path = "/web/view"
1272 def fields_view_get(self, req, model, view_id, view_type,
1273 transform=True, toolbar=False, submenu=False):
1274 Model = req.session.model(model)
1275 context = req.session.eval_context(req.context)
1276 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1277 # todo fme?: check that we should pass the evaluated context here
1278 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1279 if toolbar and transform:
1280 self.process_toolbar(req, fvg['toolbar'])
1283 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1284 # depending on how it feels, xmlrpclib.ServerProxy can translate
1285 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1286 # enjoy unicode strings which can not be trivially converted to
1287 # strings, and it blows up during parsing.
1289 # So ensure we fix this retardation by converting view xml back to
1291 if isinstance(fvg['arch'], unicode):
1292 arch = fvg['arch'].encode('utf-8')
1295 fvg['arch_string'] = arch
1298 evaluation_context = session.evaluation_context(context or {})
1299 xml = self.transform_view(arch, session, evaluation_context)
1301 xml = ElementTree.fromstring(arch)
1302 fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
1304 if 'id' in fvg['fields']:
1305 # Special case for id's
1306 id_field = fvg['fields']['id']
1307 id_field['original_type'] = id_field['type']
1308 id_field['type'] = 'id'
1310 for field in fvg['fields'].itervalues():
1311 if field.get('views'):
1312 for view in field["views"].itervalues():
1313 self.process_view(session, view, None, transform)
1314 if field.get('domain'):
1315 field["domain"] = parse_domain(field["domain"], session)
1316 if field.get('context'):
1317 field["context"] = parse_context(field["context"], session)
1319 def process_toolbar(self, req, toolbar):
1321 The toolbar is a mapping of section_key: [action_descriptor]
1323 We need to clean all those actions in order to ensure correct
1326 for actions in toolbar.itervalues():
1327 for action in actions:
1328 if 'context' in action:
1329 action['context'] = parse_context(
1330 action['context'], req.session)
1331 if 'domain' in action:
1332 action['domain'] = parse_domain(
1333 action['domain'], req.session)
1335 @openerpweb.jsonrequest
1336 def add_custom(self, req, view_id, arch):
1337 CustomView = req.session.model('ir.ui.view.custom')
1339 'user_id': req.session._uid,
1342 }, req.session.eval_context(req.context))
1343 return {'result': True}
1345 @openerpweb.jsonrequest
1346 def undo_custom(self, req, view_id, reset=False):
1347 CustomView = req.session.model('ir.ui.view.custom')
1348 context = req.session.eval_context(req.context)
1349 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1350 0, False, False, context)
1353 CustomView.unlink(vcustom, context)
1355 CustomView.unlink([vcustom[0]], context)
1356 return {'result': True}
1357 return {'result': False}
1359 def transform_view(self, view_string, session, context=None):
1360 # transform nodes on the fly via iterparse, instead of
1361 # doing it statically on the parsing result
1362 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1364 for event, elem in parser:
1365 if event == "start":
1368 self.parse_domains_and_contexts(elem, session)
1371 def parse_domains_and_contexts(self, elem, session):
1372 """ Converts domains and contexts from the view into Python objects,
1373 either literals if they can be parsed by literal_eval or a special
1374 placeholder object if the domain or context refers to free variables.
1376 :param elem: the current node being parsed
1377 :type param: xml.etree.ElementTree.Element
1378 :param session: OpenERP session object, used to store and retrieve
1380 :type session: openerpweb.openerpweb.OpenERPSession
1382 for el in ['domain', 'filter_domain']:
1383 domain = elem.get(el, '').strip()
1385 elem.set(el, parse_domain(domain, session))
1386 elem.set(el + '_string', domain)
1387 for el in ['context', 'default_get']:
1388 context_string = elem.get(el, '').strip()
1390 elem.set(el, parse_context(context_string, session))
1391 elem.set(el + '_string', context_string)
1393 @openerpweb.jsonrequest
1394 def load(self, req, model, view_id, view_type, toolbar=False):
1395 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1397 class TreeView(View):
1398 _cp_path = "/web/treeview"
1400 @openerpweb.jsonrequest
1401 def action(self, req, model, id):
1402 return load_actions_from_ir_values(
1403 req,'action', 'tree_but_open',[(model, id)],
1406 class SearchView(View):
1407 _cp_path = "/web/searchview"
1409 @openerpweb.jsonrequest
1410 def load(self, req, model, view_id):
1411 fields_view = self.fields_view_get(req, model, view_id, 'search')
1412 return {'fields_view': fields_view}
1414 @openerpweb.jsonrequest
1415 def fields_get(self, req, model):
1416 Model = req.session.model(model)
1417 fields = Model.fields_get(False, req.session.eval_context(req.context))
1418 for field in fields.values():
1419 # shouldn't convert the views too?
1420 if field.get('domain'):
1421 field["domain"] = parse_domain(field["domain"], req.session)
1422 if field.get('context'):
1423 field["context"] = parse_context(field["context"], req.session)
1424 return {'fields': fields}
1426 @openerpweb.jsonrequest
1427 def get_filters(self, req, model):
1428 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1429 Model = req.session.model("ir.filters")
1430 filters = Model.get_filters(model)
1431 for filter in filters:
1433 parsed_context = parse_context(filter["context"], req.session)
1434 filter["context"] = (parsed_context
1435 if not isinstance(parsed_context, nonliterals.BaseContext)
1436 else req.session.eval_context(parsed_context))
1438 parsed_domain = parse_domain(filter["domain"], req.session)
1439 filter["domain"] = (parsed_domain
1440 if not isinstance(parsed_domain, nonliterals.BaseDomain)
1441 else req.session.eval_domain(parsed_domain))
1443 logger.exception("Failed to parse custom filter %s in %s",
1444 filter['name'], model)
1445 filter['disabled'] = True
1446 del filter['context']
1447 del filter['domain']
1450 class Binary(openerpweb.Controller):
1451 _cp_path = "/web/binary"
1453 @openerpweb.httprequest
1454 def image(self, req, model, id, field, **kw):
1455 last_update = '__last_update'
1456 Model = req.session.model(model)
1457 context = req.session.eval_context(req.context)
1458 headers = [('Content-Type', 'image/png')]
1459 etag = req.httprequest.headers.get('If-None-Match')
1460 hashed_session = hashlib.md5(req.session_id).hexdigest()
1461 id = None if not id else simplejson.loads(id)
1462 if type(id) is list:
1465 if not id and hashed_session == etag:
1466 return werkzeug.wrappers.Response(status=304)
1468 date = Model.read([id], [last_update], context)[0].get(last_update)
1469 if hashlib.md5(date).hexdigest() == etag:
1470 return werkzeug.wrappers.Response(status=304)
1472 retag = hashed_session
1475 res = Model.default_get([field], context).get(field)
1478 res = Model.read([id], [last_update, field], context)[0]
1479 retag = hashlib.md5(res.get(last_update)).hexdigest()
1480 image_base64 = res.get(field)
1482 if kw.get('resize'):
1483 resize = kw.get('resize').split(',');
1484 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1485 width = int(resize[0])
1486 height = int(resize[1])
1487 # resize maximum 500*500
1488 if width > 500: width = 500
1489 if height > 500: height = 500
1490 image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1492 image_data = base64.b64decode(image_base64)
1494 except (TypeError, xmlrpclib.Fault):
1495 image_data = self.placeholder(req)
1496 headers.append(('ETag', retag))
1497 headers.append(('Content-Length', len(image_data)))
1499 ncache = int(kw.get('cache'))
1500 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1503 return req.make_response(image_data, headers)
1504 def placeholder(self, req):
1505 addons_path = openerpweb.addons_manifest['web']['addons_path']
1506 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1508 @openerpweb.httprequest
1509 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1510 """ Download link for files stored as binary fields.
1512 If the ``id`` parameter is omitted, fetches the default value for the
1513 binary field (via ``default_get``), otherwise fetches the field for
1514 that precise record.
1516 :param req: OpenERP request
1517 :type req: :class:`web.common.http.HttpRequest`
1518 :param str model: name of the model to fetch the binary from
1519 :param str field: binary field
1520 :param str id: id of the record from which to fetch the binary
1521 :param str filename_field: field holding the file's name, if any
1522 :returns: :class:`werkzeug.wrappers.Response`
1524 Model = req.session.model(model)
1525 context = req.session.eval_context(req.context)
1528 fields.append(filename_field)
1530 res = Model.read([int(id)], fields, context)[0]
1532 res = Model.default_get(fields, context)
1533 filecontent = base64.b64decode(res.get(field, ''))
1535 return req.not_found()
1537 filename = '%s_%s' % (model.replace('.', '_'), id)
1539 filename = res.get(filename_field, '') or filename
1540 return req.make_response(filecontent,
1541 [('Content-Type', 'application/octet-stream'),
1542 ('Content-Disposition', content_disposition(filename, req))])
1544 @openerpweb.httprequest
1545 def saveas_ajax(self, req, data, token):
1546 jdata = simplejson.loads(data)
1547 model = jdata['model']
1548 field = jdata['field']
1549 id = jdata.get('id', None)
1550 filename_field = jdata.get('filename_field', None)
1551 context = jdata.get('context', dict())
1553 context = req.session.eval_context(context)
1554 Model = req.session.model(model)
1557 fields.append(filename_field)
1559 res = Model.read([int(id)], fields, context)[0]
1561 res = Model.default_get(fields, context)
1562 filecontent = base64.b64decode(res.get(field, ''))
1564 raise ValueError("No content found for field '%s' on '%s:%s'" %
1567 filename = '%s_%s' % (model.replace('.', '_'), id)
1569 filename = res.get(filename_field, '') or filename
1570 return req.make_response(filecontent,
1571 headers=[('Content-Type', 'application/octet-stream'),
1572 ('Content-Disposition', content_disposition(filename, req))],
1573 cookies={'fileToken': int(token)})
1575 @openerpweb.httprequest
1576 def upload(self, req, callback, ufile):
1577 # TODO: might be useful to have a configuration flag for max-length file uploads
1579 out = """<script language="javascript" type="text/javascript">
1580 var win = window.top.window;
1581 win.jQuery(win).trigger(%s, %s);
1584 args = [len(data), ufile.filename,
1585 ufile.content_type, base64.b64encode(data)]
1586 except Exception, e:
1587 args = [False, e.message]
1588 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1590 @openerpweb.httprequest
1591 def upload_attachment(self, req, callback, model, id, ufile):
1592 context = req.session.eval_context(req.context)
1593 Model = req.session.model('ir.attachment')
1595 out = """<script language="javascript" type="text/javascript">
1596 var win = window.top.window;
1597 win.jQuery(win).trigger(%s, %s);
1599 attachment_id = Model.create({
1600 'name': ufile.filename,
1601 'datas': base64.encodestring(ufile.read()),
1602 'datas_fname': ufile.filename,
1607 'filename': ufile.filename,
1610 except Exception, e:
1611 args = { 'error': e.message }
1612 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1614 class Action(openerpweb.Controller):
1615 _cp_path = "/web/action"
1617 @openerpweb.jsonrequest
1618 def load(self, req, action_id, do_not_eval=False):
1619 Actions = req.session.model('ir.actions.actions')
1621 context = req.session.eval_context(req.context)
1624 action_id = int(action_id)
1627 module, xmlid = action_id.split('.', 1)
1628 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1629 assert model.startswith('ir.actions.')
1631 action_id = 0 # force failed read
1633 base_action = Actions.read([action_id], ['type'], context)
1636 action_type = base_action[0]['type']
1637 if action_type == 'ir.actions.report.xml':
1638 ctx.update({'bin_size': True})
1640 action = req.session.model(action_type).read([action_id], False, ctx)
1642 value = clean_action(req, action[0], do_not_eval)
1645 @openerpweb.jsonrequest
1646 def run(self, req, action_id):
1647 return_action = req.session.model('ir.actions.server').run(
1648 [action_id], req.session.eval_context(req.context))
1650 return clean_action(req, return_action)
1655 _cp_path = "/web/export"
1657 @openerpweb.jsonrequest
1658 def formats(self, req):
1659 """ Returns all valid export formats
1661 :returns: for each export format, a pair of identifier and printable name
1662 :rtype: [(str, str)]
1666 for path, controller in openerpweb.controllers_path.iteritems()
1667 if path.startswith(self._cp_path)
1668 if hasattr(controller, 'fmt')
1669 ], key=operator.itemgetter("label"))
1671 def fields_get(self, req, model):
1672 Model = req.session.model(model)
1673 fields = Model.fields_get(False, req.session.eval_context(req.context))
1676 @openerpweb.jsonrequest
1677 def get_fields(self, req, model, prefix='', parent_name= '',
1678 import_compat=True, parent_field_type=None,
1681 if import_compat and parent_field_type == "many2one":
1684 fields = self.fields_get(req, model)
1687 fields.pop('id', None)
1689 fields['.id'] = fields.pop('id', {'string': 'ID'})
1691 fields_sequence = sorted(fields.iteritems(),
1692 key=lambda field: field[1].get('string', ''))
1695 for field_name, field in fields_sequence:
1697 if exclude and field_name in exclude:
1699 if field.get('readonly'):
1700 # If none of the field's states unsets readonly, skip the field
1701 if all(dict(attrs).get('readonly', True)
1702 for attrs in field.get('states', {}).values()):
1705 id = prefix + (prefix and '/'or '') + field_name
1706 name = parent_name + (parent_name and '/' or '') + field['string']
1707 record = {'id': id, 'string': name,
1708 'value': id, 'children': False,
1709 'field_type': field.get('type'),
1710 'required': field.get('required'),
1711 'relation_field': field.get('relation_field')}
1712 records.append(record)
1714 if len(name.split('/')) < 3 and 'relation' in field:
1715 ref = field.pop('relation')
1716 record['value'] += '/id'
1717 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1719 if not import_compat or field['type'] == 'one2many':
1720 # m2m field in import_compat is childless
1721 record['children'] = True
1725 @openerpweb.jsonrequest
1726 def namelist(self,req, model, export_id):
1727 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1728 export = req.session.model("ir.exports").read([export_id])[0]
1729 export_fields_list = req.session.model("ir.exports.line").read(
1730 export['export_fields'])
1732 fields_data = self.fields_info(
1733 req, model, map(operator.itemgetter('name'), export_fields_list))
1736 {'name': field['name'], 'label': fields_data[field['name']]}
1737 for field in export_fields_list
1740 def fields_info(self, req, model, export_fields):
1742 fields = self.fields_get(req, model)
1744 # To make fields retrieval more efficient, fetch all sub-fields of a
1745 # given field at the same time. Because the order in the export list is
1746 # arbitrary, this requires ordering all sub-fields of a given field
1747 # together so they can be fetched at the same time
1749 # Works the following way:
1750 # * sort the list of fields to export, the default sorting order will
1751 # put the field itself (if present, for xmlid) and all of its
1752 # sub-fields right after it
1753 # * then, group on: the first field of the path (which is the same for
1754 # a field and for its subfields and the length of splitting on the
1755 # first '/', which basically means grouping the field on one side and
1756 # all of the subfields on the other. This way, we have the field (for
1757 # the xmlid) with length 1, and all of the subfields with the same
1758 # base but a length "flag" of 2
1759 # * if we have a normal field (length 1), just add it to the info
1760 # mapping (with its string) as-is
1761 # * otherwise, recursively call fields_info via graft_subfields.
1762 # all graft_subfields does is take the result of fields_info (on the
1763 # field's model) and prepend the current base (current field), which
1764 # rebuilds the whole sub-tree for the field
1766 # result: because we're not fetching the fields_get for half the
1767 # database models, fetching a namelist with a dozen fields (including
1768 # relational data) falls from ~6s to ~300ms (on the leads model).
1769 # export lists with no sub-fields (e.g. import_compatible lists with
1770 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1771 # there's a single fields_get to execute)
1772 for (base, length), subfields in itertools.groupby(
1773 sorted(export_fields),
1774 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1775 subfields = list(subfields)
1777 # subfields is a seq of $base/*rest, and not loaded yet
1778 info.update(self.graft_subfields(
1779 req, fields[base]['relation'], base, fields[base]['string'],
1783 info[base] = fields[base]['string']
1787 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1788 export_fields = [field.split('/', 1)[1] for field in fields]
1790 (prefix + '/' + k, prefix_string + '/' + v)
1791 for k, v in self.fields_info(req, model, export_fields).iteritems())
1793 #noinspection PyPropertyDefinition
1795 def content_type(self):
1796 """ Provides the format's content type """
1797 raise NotImplementedError()
1799 def filename(self, base):
1800 """ Creates a valid filename for the format (with extension) from the
1801 provided base name (exension-less)
1803 raise NotImplementedError()
1805 def from_data(self, fields, rows):
1806 """ Conversion method from OpenERP's export data to whatever the
1807 current export class outputs
1809 :params list fields: a list of fields to export
1810 :params list rows: a list of records to export
1814 raise NotImplementedError()
1816 @openerpweb.httprequest
1817 def index(self, req, data, token):
1818 model, fields, ids, domain, import_compat = \
1819 operator.itemgetter('model', 'fields', 'ids', 'domain',
1821 simplejson.loads(data))
1823 context = req.session.eval_context(req.context)
1824 Model = req.session.model(model)
1825 ids = ids or Model.search(domain, 0, False, False, context)
1827 field_names = map(operator.itemgetter('name'), fields)
1828 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1831 columns_headers = field_names
1833 columns_headers = [val['label'].strip() for val in fields]
1836 return req.make_response(self.from_data(columns_headers, import_data),
1837 headers=[('Content-Disposition',
1838 content_disposition(self.filename(model), req)),
1839 ('Content-Type', self.content_type)],
1840 cookies={'fileToken': int(token)})
1842 class CSVExport(Export):
1843 _cp_path = '/web/export/csv'
1844 fmt = {'tag': 'csv', 'label': 'CSV'}
1847 def content_type(self):
1848 return 'text/csv;charset=utf8'
1850 def filename(self, base):
1851 return base + '.csv'
1853 def from_data(self, fields, rows):
1855 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1857 writer.writerow([name.encode('utf-8') for name in fields])
1862 if isinstance(d, basestring):
1863 d = d.replace('\n',' ').replace('\t',' ')
1865 d = d.encode('utf-8')
1866 except UnicodeError:
1868 if d is False: d = None
1870 writer.writerow(row)
1877 class ExcelExport(Export):
1878 _cp_path = '/web/export/xls'
1882 'error': None if xlwt else "XLWT required"
1886 def content_type(self):
1887 return 'application/vnd.ms-excel'
1889 def filename(self, base):
1890 return base + '.xls'
1892 def from_data(self, fields, rows):
1893 workbook = xlwt.Workbook()
1894 worksheet = workbook.add_sheet('Sheet 1')
1896 for i, fieldname in enumerate(fields):
1897 worksheet.write(0, i, fieldname)
1898 worksheet.col(i).width = 8000 # around 220 pixels
1900 style = xlwt.easyxf('align: wrap yes')
1902 for row_index, row in enumerate(rows):
1903 for cell_index, cell_value in enumerate(row):
1904 if isinstance(cell_value, basestring):
1905 cell_value = re.sub("\r", " ", cell_value)
1906 if cell_value is False: cell_value = None
1907 worksheet.write(row_index + 1, cell_index, cell_value, style)
1916 class Reports(View):
1917 _cp_path = "/web/report"
1918 POLLING_DELAY = 0.25
1920 'doc': 'application/vnd.ms-word',
1921 'html': 'text/html',
1922 'odt': 'application/vnd.oasis.opendocument.text',
1923 'pdf': 'application/pdf',
1924 'sxw': 'application/vnd.sun.xml.writer',
1925 'xls': 'application/vnd.ms-excel',
1928 @openerpweb.httprequest
1929 def index(self, req, action, token):
1930 action = simplejson.loads(action)
1932 report_srv = req.session.proxy("report")
1933 context = req.session.eval_context(
1934 nonliterals.CompoundContext(
1935 req.context or {}, action[ "context"]))
1938 report_ids = context["active_ids"]
1939 if 'report_type' in action:
1940 report_data['report_type'] = action['report_type']
1941 if 'datas' in action:
1942 if 'ids' in action['datas']:
1943 report_ids = action['datas'].pop('ids')
1944 report_data.update(action['datas'])
1946 report_id = report_srv.report(
1947 req.session._db, req.session._uid, req.session._password,
1948 action["report_name"], report_ids,
1949 report_data, context)
1951 report_struct = None
1953 report_struct = report_srv.report_get(
1954 req.session._db, req.session._uid, req.session._password, report_id)
1955 if report_struct["state"]:
1958 time.sleep(self.POLLING_DELAY)
1960 report = base64.b64decode(report_struct['result'])
1961 if report_struct.get('code') == 'zlib':
1962 report = zlib.decompress(report)
1963 report_mimetype = self.TYPES_MAPPING.get(
1964 report_struct['format'], 'octet-stream')
1965 file_name = action.get('name', 'report')
1966 if 'name' not in action:
1967 reports = req.session.model('ir.actions.report.xml')
1968 res_id = reports.search([('report_name', '=', action['report_name']),],
1969 0, False, False, context)
1971 file_name = reports.read(res_id[0], ['name'], context)['name']
1973 file_name = action['report_name']
1974 file_name = '%s.%s' % (file_name, report_struct['format'])
1976 return req.make_response(report,
1978 ('Content-Disposition', content_disposition(file_name, req)),
1979 ('Content-Type', report_mimetype),
1980 ('Content-Length', len(report))],
1981 cookies={'fileToken': int(token)})
1983 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: