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 openerpweb = common.http
35 #----------------------------------------------------------
37 #----------------------------------------------------------
40 """ Minify js with a clever regex.
41 Taken from http://opensource.perlig.de/rjsmin
42 Apache License, Version 2.0 """
44 """ Substitution callback """
45 groups = match.groups()
51 (groups[4] and '\n') or
52 (groups[5] and ' ') or
53 (groups[6] and ' ') or
54 (groups[7] and ' ') or
59 r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
60 r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
61 r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
62 r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
63 r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
64 r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
65 r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
66 r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
67 r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
68 r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
69 r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
70 r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
71 r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
72 r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
73 r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
74 r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
75 r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
76 r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
77 r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
78 r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
79 r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
84 # Validated by diff -u of sass2scss against:
85 # sass-convert -F sass -T scss openerp.sass openerp.scss
88 reComment = re.compile(r'//.*$')
89 reIndent = re.compile(r'^\s+')
90 reIgnore = re.compile(r'^\s*(//.*)?$')
91 reFixes = { re.compile(r'\(\((.*)\)\)') : r'(\1)', }
94 for l in src.split('\n'):
96 if reIgnore.search(l): continue
97 l = reComment.sub('', l)
99 indent = reIndent.match(l)
100 level = indent.end() if indent else 0
103 prevBlocks[lastLevel] = block
105 block[-1] = (block[-1], newBlock)
107 elif level<lastLevel:
108 block = prevBlocks[level]
112 for ereg, repl in reFixes.items():
113 l = ereg.sub(repl if type(repl)==str else repl(), l)
116 def write(sass, level=-1):
119 if type(sass)==tuple:
121 out += indent+sass[0]+" {\n"
123 out += write(e, level+1)
125 out = out.rstrip(" \n")
130 out += indent+sass+";\n"
136 proxy = req.session.proxy("db")
138 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
140 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
141 dbs = [i for i in dbs if re.match(r, i)]
144 def module_topological_sort(modules):
145 """ Return a list of module names sorted so that their dependencies of the
146 modules are listed before the module itself
148 modules is a dict of {module_name: dependencies}
150 :param modules: modules to sort
155 dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
156 # incoming edge: dependency on other module (if a depends on b, a has an
157 # incoming edge from b, aka there's an edge from b to a)
158 # outgoing edge: other module depending on this one
160 # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
161 #L ← Empty list that will contain the sorted nodes
163 #S ← Set of all nodes with no outgoing edges (modules on which no other
165 S = set(module for module in modules if module not in dependencies)
168 #function visit(node n)
170 #if n has not been visited yet then
174 #change: n not web module, can not be resolved, ignore
175 if n not in modules: return
176 #for each node m with an edge from m to n do (dependencies of n)
182 #for each node n in S do
188 def module_installed(req):
189 # Candidates module the current heuristic is the /static dir
190 loadable = openerpweb.addons_manifest.keys()
193 # Retrieve database installed modules
194 # TODO The following code should move to ir.module.module.list_installed_modules()
195 Modules = req.session.model('ir.module.module')
196 domain = [('state','=','installed'), ('name','in', loadable)]
197 for module in Modules.search_read(domain, ['name', 'dependencies_id']):
198 modules[module['name']] = []
199 deps = module.get('dependencies_id')
201 deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
202 dependencies = [i['name'] for i in deps_read]
203 modules[module['name']] = dependencies
205 sorted_modules = module_topological_sort(modules)
206 return sorted_modules
208 def module_installed_bypass_session(dbname):
209 loadable = openerpweb.addons_manifest.keys()
212 import openerp.modules.registry
213 registry = openerp.modules.registry.RegistryManager.get(dbname)
214 with registry.cursor() as cr:
215 m = registry.get('ir.module.module')
216 # TODO The following code should move to ir.module.module.list_installed_modules()
217 domain = [('state','=','installed'), ('name','in', loadable)]
218 ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
219 for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
220 modules[module['name']] = []
221 deps = module.get('dependencies_id')
223 deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
224 dependencies = [i['name'] for i in deps_read]
225 modules[module['name']] = dependencies
228 sorted_modules = module_topological_sort(modules)
229 return sorted_modules
231 def module_boot(req):
232 server_wide_modules = openerp.conf.server_wide_modules or ['web']
233 return [m for m in server_wide_modules if m in openerpweb.addons_manifest]
234 # TODO the following will be enabled once we separate the module code and translation loading
237 for i in server_wide_modules:
238 if i in openerpweb.addons_manifest:
240 # if only one db load every module at boot
244 except xmlrpclib.Fault:
245 # ignore access denied
248 dbside = module_installed_bypass_session(dbs[0])
249 dbside = [i for i in dbside if i not in serverside]
250 addons = serverside + dbside
253 def concat_xml(file_list):
254 """Concatenate xml files
256 :param list(str) file_list: list of files to check
257 :returns: (concatenation_result, checksum)
260 checksum = hashlib.new('sha1')
262 return '', checksum.hexdigest()
265 for fname in file_list:
266 with open(fname, 'rb') as fp:
268 checksum.update(contents)
270 xml = ElementTree.parse(fp).getroot()
273 root = ElementTree.Element(xml.tag)
274 #elif root.tag != xml.tag:
275 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
277 for child in xml.getchildren():
279 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
281 def concat_files(file_list, reader=None, intersperse=""):
282 """ Concatenates contents of all provided files
284 :param list(str) file_list: list of files to check
285 :param function reader: reading procedure for each file
286 :param str intersperse: string to intersperse between file contents
287 :returns: (concatenation_result, checksum)
290 checksum = hashlib.new('sha1')
292 return '', checksum.hexdigest()
296 with open(f, 'rb') as fp:
300 for fname in file_list:
301 contents = reader(fname)
302 checksum.update(contents)
303 files_content.append(contents)
305 files_concat = intersperse.join(files_content)
306 return files_concat, checksum.hexdigest()
308 def concat_js(file_list):
309 content, checksum = concat_files(file_list, intersperse=';')
310 content = rjsmin(content)
311 return content, checksum
313 def manifest_glob(req, addons, key):
315 addons = module_boot(req)
317 addons = addons.split(',')
320 manifest = openerpweb.addons_manifest.get(addon, None)
323 # ensure does not ends with /
324 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
325 globlist = manifest.get(key, [])
326 for pattern in globlist:
327 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
328 r.append((path, path[len(addons_path):]))
331 def manifest_list(req, mods, extension):
333 path = '/web/webclient/' + extension
335 path += '?mods=' + mods
337 files = manifest_glob(req, mods, extension)
338 i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
339 req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
341 return [wp for _fp, wp in files]
343 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
345 def get_last_modified(files):
346 """ Returns the modification time of the most recently modified
349 :param list(str) files: names of files to check
350 :return: most recent modification time amongst the fileset
351 :rtype: datetime.datetime
355 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
357 return datetime.datetime(1970, 1, 1)
359 def make_conditional(req, response, last_modified=None, etag=None):
360 """ Makes the provided response conditional based upon the request,
361 and mandates revalidation from clients
363 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
364 setting ``last_modified`` and ``etag`` correctly on the response object
366 :param req: OpenERP request
367 :type req: web.common.http.WebRequest
368 :param response: Werkzeug response
369 :type response: werkzeug.wrappers.Response
370 :param datetime.datetime last_modified: last modification date of the response content
371 :param str etag: some sort of checksum of the content (deep etag)
372 :return: the response object provided
373 :rtype: werkzeug.wrappers.Response
375 response.cache_control.must_revalidate = True
376 response.cache_control.max_age = 0
378 response.last_modified = last_modified
380 response.set_etag(etag)
381 return response.make_conditional(req.httprequest)
383 def login_and_redirect(req, db, login, key, redirect_url='/'):
384 req.session.authenticate(db, login, key, {})
385 return set_cookie_and_redirect(req, redirect_url)
387 def set_cookie_and_redirect(req, redirect_url):
388 redirect = werkzeug.utils.redirect(redirect_url, 303)
389 redirect.autocorrect_location_header = False
390 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
391 redirect.set_cookie('instance0|session_id', cookie_val)
394 def eval_context_and_domain(session, context, domain=None):
395 e_context = session.eval_context(context)
396 # should we give the evaluated context as an evaluation context to the domain?
397 e_domain = session.eval_domain(domain or [])
399 return e_context, e_domain
401 def load_actions_from_ir_values(req, key, key2, models, meta):
402 context = req.session.eval_context(req.context)
403 Values = req.session.model('ir.values')
404 actions = Values.get(key, key2, models, meta, context)
406 return [(id, name, clean_action(req, action))
407 for id, name, action in actions]
409 def clean_action(req, action, do_not_eval=False):
410 action.setdefault('flags', {})
412 context = req.session.eval_context(req.context)
413 eval_ctx = req.session.evaluation_context(context)
416 # values come from the server, we can just eval them
417 if action.get('context') and isinstance(action.get('context'), basestring):
418 action['context'] = eval( action['context'], eval_ctx ) or {}
420 if action.get('domain') and isinstance(action.get('domain'), basestring):
421 action['domain'] = eval( action['domain'], eval_ctx ) or []
423 if 'context' in action:
424 action['context'] = parse_context(action['context'], req.session)
425 if 'domain' in action:
426 action['domain'] = parse_domain(action['domain'], req.session)
428 action_type = action.setdefault('type', 'ir.actions.act_window_close')
429 if action_type == 'ir.actions.act_window':
430 return fix_view_modes(action)
433 # I think generate_views,fix_view_modes should go into js ActionManager
434 def generate_views(action):
436 While the server generates a sequence called "views" computing dependencies
437 between a bunch of stuff for views coming directly from the database
438 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
439 to return custom view dictionaries generated on the fly.
441 In that case, there is no ``views`` key available on the action.
443 Since the web client relies on ``action['views']``, generate it here from
444 ``view_mode`` and ``view_id``.
446 Currently handles two different cases:
448 * no view_id, multiple view_mode
449 * single view_id, single view_mode
451 :param dict action: action descriptor dictionary to generate a views key for
453 view_id = action.get('view_id') or False
454 if isinstance(view_id, (list, tuple)):
457 # providing at least one view mode is a requirement, not an option
458 view_modes = action['view_mode'].split(',')
460 if len(view_modes) > 1:
462 raise ValueError('Non-db action dictionaries should provide '
463 'either multiple view modes or a single view '
464 'mode and an optional view id.\n\n Got view '
465 'modes %r and view id %r for action %r' % (
466 view_modes, view_id, action))
467 action['views'] = [(False, mode) for mode in view_modes]
469 action['views'] = [(view_id, view_modes[0])]
471 def fix_view_modes(action):
472 """ For historical reasons, OpenERP has weird dealings in relation to
473 view_mode and the view_type attribute (on window actions):
475 * one of the view modes is ``tree``, which stands for both list views
477 * the choice is made by checking ``view_type``, which is either
478 ``form`` for a list view or ``tree`` for an actual tree view
480 This methods simply folds the view_type into view_mode by adding a
481 new view mode ``list`` which is the result of the ``tree`` view_mode
482 in conjunction with the ``form`` view_type.
484 TODO: this should go into the doc, some kind of "peculiarities" section
486 :param dict action: an action descriptor
487 :returns: nothing, the action is modified in place
489 if not action.get('views'):
490 generate_views(action)
492 if action.pop('view_type', 'form') != 'form':
495 if 'view_mode' in action:
496 action['view_mode'] = ','.join(
497 mode if mode != 'tree' else 'list'
498 for mode in action['view_mode'].split(','))
500 [id, mode if mode != 'tree' else 'list']
501 for id, mode in action['views']
506 def parse_domain(domain, session):
507 """ Parses an arbitrary string containing a domain, transforms it
508 to either a literal domain or a :class:`common.nonliterals.Domain`
510 :param domain: the domain to parse, if the domain is not a string it
511 is assumed to be a literal domain and is returned as-is
512 :param session: Current OpenERP session
513 :type session: openerpweb.OpenERPSession
515 if not isinstance(domain, basestring):
518 return ast.literal_eval(domain)
521 return common.nonliterals.Domain(session, domain)
523 def parse_context(context, session):
524 """ Parses an arbitrary string containing a context, transforms it
525 to either a literal context or a :class:`common.nonliterals.Context`
527 :param context: the context to parse, if the context is not a string it
528 is assumed to be a literal domain and is returned as-is
529 :param session: Current OpenERP session
530 :type session: openerpweb.OpenERPSession
532 if not isinstance(context, basestring):
535 return ast.literal_eval(context)
537 return common.nonliterals.Context(session, context)
540 def _local_web_translations(trans_file):
543 with open(trans_file) as t_file:
544 po = babel.messages.pofile.read_po(t_file)
548 if x.id and x.string and "openerp-web" in x.auto_comments:
549 messages.append({'id': x.id, 'string': x.string})
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 type="text/javascript"
577 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, **kw):
603 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, None, 'js'))
604 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, None, 'css'))
606 r = html_template % {
609 'modules': simplejson.dumps(module_boot(req)),
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, mods, 'css')
625 @openerpweb.jsonrequest
626 def jslist(self, req, mods=None):
627 return manifest_list(req, mods, 'js')
629 @openerpweb.jsonrequest
630 def qweblist(self, req, mods=None):
631 return manifest_list(req, mods, 'qweb')
633 @openerpweb.httprequest
634 def css(self, req, mods=None):
635 files = list(manifest_glob(req, mods, 'css'))
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 # convert FS path into web path
652 web_dir = '/'.join(os.path.dirname(path).split(os.path.sep))
656 r"""@import \1%s/""" % (web_dir,),
662 r"""url(\1%s/""" % (web_dir,),
665 return data.encode('utf-8')
667 content, checksum = concat_files((f[0] for f in files), reader)
669 return make_conditional(
670 req, req.make_response(content, [('Content-Type', 'text/css')]),
671 last_modified, checksum)
673 @openerpweb.httprequest
674 def js(self, req, mods=None):
675 files = [f[0] for f in manifest_glob(req, mods, 'js')]
676 last_modified = get_last_modified(files)
677 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
678 return werkzeug.wrappers.Response(status=304)
680 content, checksum = concat_js(files)
682 return make_conditional(
683 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
684 last_modified, checksum)
686 @openerpweb.httprequest
687 def qweb(self, req, mods=None):
688 files = [f[0] for f in manifest_glob(req, mods, 'qweb')]
689 last_modified = get_last_modified(files)
690 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
691 return werkzeug.wrappers.Response(status=304)
693 content, checksum = concat_xml(files)
695 return make_conditional(
696 req, req.make_response(content, [('Content-Type', 'text/xml')]),
697 last_modified, checksum)
699 @openerpweb.jsonrequest
700 def bootstrap_translations(self, req, mods):
701 """ Load local translations from *.po files, as a temporary solution
702 until we have established a valid session. This is meant only
703 for translating the login page and db management chrome, using
704 the browser's language. """
705 lang = req.httprequest.accept_languages.best or 'en'
706 # For performance reasons we only load a single translation, so for
707 # sub-languages (that should only be partially translated) we load the
708 # main language PO instead - that should be enough for the login screen.
709 if '-' in lang: # RFC2616 uses '-' separators for sublanguages
710 lang = lang.split('-')[0]
712 translations_per_module = {}
713 for addon_name in mods:
714 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
715 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
716 if not os.path.exists(f_name):
718 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
720 return {"modules": translations_per_module,
721 "lang_parameters": None}
723 @openerpweb.jsonrequest
724 def translations(self, req, mods, lang):
725 res_lang = req.session.model('res.lang')
726 ids = res_lang.search([("code", "=", lang)])
729 lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
730 "grouping", "decimal_point", "thousands_sep"])
732 # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
733 # done server-side when the language is loaded, so we only need to load the user's lang.
734 ir_translation = req.session.model('ir.translation')
735 translations_per_module = {}
736 messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
737 ('comments','like','openerp-web'),('value','!=',False),
739 ['module','src','value','lang'], order='module')
740 for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
741 translations_per_module.setdefault(mod,{'messages':[]})
742 translations_per_module[mod]['messages'].extend({'id': m['src'],
743 'string': m['value']} \
745 return {"modules": translations_per_module,
746 "lang_parameters": lang_params}
748 @openerpweb.jsonrequest
749 def version_info(self, req):
751 "version": openerp.release.version
754 class Proxy(openerpweb.Controller):
755 _cp_path = '/web/proxy'
757 @openerpweb.jsonrequest
758 def load(self, req, path):
759 """ Proxies an HTTP request through a JSON request.
761 It is strongly recommended to not request binary files through this,
762 as the result will be a binary data blob as well.
764 :param req: OpenERP request
765 :param path: actual request path
766 :return: file content
768 from werkzeug.test import Client
769 from werkzeug.wrappers import BaseResponse
771 return Client(req.httprequest.app, BaseResponse).get(path).data
773 class Database(openerpweb.Controller):
774 _cp_path = "/web/database"
776 @openerpweb.jsonrequest
777 def get_list(self, req):
779 return {"db_list": dbs}
781 @openerpweb.jsonrequest
782 def create(self, req, fields):
783 params = dict(map(operator.itemgetter('name', 'value'), fields))
785 params['super_admin_pwd'],
787 bool(params.get('demo_data')),
789 params['create_admin_pwd']
792 return req.session.proxy("db").create_database(*create_attrs)
794 @openerpweb.jsonrequest
795 def drop(self, req, fields):
796 password, db = operator.itemgetter(
797 'drop_pwd', 'drop_db')(
798 dict(map(operator.itemgetter('name', 'value'), fields)))
801 return req.session.proxy("db").drop(password, db)
802 except xmlrpclib.Fault, e:
803 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
804 return {'error': e.faultCode, 'title': 'Drop Database'}
805 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
807 @openerpweb.httprequest
808 def backup(self, req, backup_db, backup_pwd, token):
810 db_dump = base64.b64decode(
811 req.session.proxy("db").dump(backup_pwd, backup_db))
812 filename = "%(db)s_%(timestamp)s.dump" % {
814 'timestamp': datetime.datetime.utcnow().strftime(
815 "%Y-%m-%d_%H-%M-%SZ")
817 return req.make_response(db_dump,
818 [('Content-Type', 'application/octet-stream; charset=binary'),
819 ('Content-Disposition', 'attachment; filename="' + filename + '"')],
820 {'fileToken': int(token)}
822 except xmlrpclib.Fault, e:
823 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
825 @openerpweb.httprequest
826 def restore(self, req, db_file, restore_pwd, new_db):
828 data = base64.b64encode(db_file.read())
829 req.session.proxy("db").restore(restore_pwd, new_db, data)
831 except xmlrpclib.Fault, e:
832 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
833 raise Exception("AccessDenied")
835 @openerpweb.jsonrequest
836 def change_password(self, req, fields):
837 old_password, new_password = operator.itemgetter(
838 'old_pwd', 'new_pwd')(
839 dict(map(operator.itemgetter('name', 'value'), fields)))
841 return req.session.proxy("db").change_admin_password(old_password, new_password)
842 except xmlrpclib.Fault, e:
843 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
844 return {'error': e.faultCode, 'title': 'Change Password'}
845 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
847 class Session(openerpweb.Controller):
848 _cp_path = "/web/session"
850 def session_info(self, req):
851 req.session.ensure_valid()
853 "session_id": req.session_id,
854 "uid": req.session._uid,
855 "context": req.session.get_context() if req.session._uid else {},
856 "db": req.session._db,
857 "login": req.session._login,
860 @openerpweb.jsonrequest
861 def get_session_info(self, req):
862 return self.session_info(req)
864 @openerpweb.jsonrequest
865 def authenticate(self, req, db, login, password, base_location=None):
866 wsgienv = req.httprequest.environ
868 base_location=base_location,
869 HTTP_HOST=wsgienv['HTTP_HOST'],
870 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
872 req.session.authenticate(db, login, password, env)
874 return self.session_info(req)
876 @openerpweb.jsonrequest
877 def change_password (self,req,fields):
878 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
879 dict(map(operator.itemgetter('name', 'value'), fields)))
880 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
881 return {'error':'All passwords have to be filled.','title': 'Change Password'}
882 if new_password != confirm_password:
883 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
885 if req.session.model('res.users').change_password(
886 old_password, new_password):
887 return {'new_password':new_password}
889 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
890 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
892 @openerpweb.jsonrequest
893 def sc_list(self, req):
894 return req.session.model('ir.ui.view_sc').get_sc(
895 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
897 @openerpweb.jsonrequest
898 def get_lang_list(self, req):
901 'lang_list': (req.session.proxy("db").list_lang() or []),
905 return {"error": e, "title": "Languages"}
907 @openerpweb.jsonrequest
908 def modules(self, req):
909 # return all installed modules. Web client is smart enough to not load a module twice
910 return module_installed(req)
912 @openerpweb.jsonrequest
913 def eval_domain_and_context(self, req, contexts, domains,
915 """ Evaluates sequences of domains and contexts, composing them into
916 a single context, domain or group_by sequence.
918 :param list contexts: list of contexts to merge together. Contexts are
919 evaluated in sequence, all previous contexts
920 are part of their own evaluation context
921 (starting at the session context).
922 :param list domains: list of domains to merge together. Domains are
923 evaluated in sequence and appended to one another
924 (implicit AND), their evaluation domain is the
925 result of merging all contexts.
926 :param list group_by_seq: list of domains (which may be in a different
927 order than the ``contexts`` parameter),
928 evaluated in sequence, their ``'group_by'``
929 key is extracted if they have one.
934 the global context created by merging all of
938 the concatenation of all domains
941 a list of fields to group by, potentially empty (in which case
942 no group by should be performed)
944 context, domain = eval_context_and_domain(req.session,
945 common.nonliterals.CompoundContext(*(contexts or [])),
946 common.nonliterals.CompoundDomain(*(domains or [])))
948 group_by_sequence = []
949 for candidate in (group_by_seq or []):
950 ctx = req.session.eval_context(candidate, context)
951 group_by = ctx.get('group_by')
954 elif isinstance(group_by, basestring):
955 group_by_sequence.append(group_by)
957 group_by_sequence.extend(group_by)
962 'group_by': group_by_sequence
965 @openerpweb.jsonrequest
966 def save_session_action(self, req, the_action):
968 This method store an action object in the session object and returns an integer
969 identifying that action. The method get_session_action() can be used to get
972 :param the_action: The action to save in the session.
973 :type the_action: anything
974 :return: A key identifying the saved action.
977 saved_actions = req.httpsession.get('saved_actions')
978 if not saved_actions:
979 saved_actions = {"next":0, "actions":{}}
980 req.httpsession['saved_actions'] = saved_actions
981 # we don't allow more than 10 stored actions
982 if len(saved_actions["actions"]) >= 10:
983 del saved_actions["actions"][min(saved_actions["actions"])]
984 key = saved_actions["next"]
985 saved_actions["actions"][key] = the_action
986 saved_actions["next"] = key + 1
989 @openerpweb.jsonrequest
990 def get_session_action(self, req, key):
992 Gets back a previously saved action. This method can return None if the action
993 was saved since too much time (this case should be handled in a smart way).
995 :param key: The key given by save_session_action()
997 :return: The saved action or None.
1000 saved_actions = req.httpsession.get('saved_actions')
1001 if not saved_actions:
1003 return saved_actions["actions"].get(key)
1005 @openerpweb.jsonrequest
1006 def check(self, req):
1007 req.session.assert_valid()
1010 @openerpweb.jsonrequest
1011 def destroy(self, req):
1012 req.session._suicide = True
1014 class Menu(openerpweb.Controller):
1015 _cp_path = "/web/menu"
1017 @openerpweb.jsonrequest
1018 def load(self, req):
1019 return {'data': self.do_load(req)}
1021 @openerpweb.jsonrequest
1022 def get_user_roots(self, req):
1023 return self.do_get_user_roots(req)
1025 def do_get_user_roots(self, req):
1026 """ Return all root menu ids visible for the session user.
1028 :param req: A request object, with an OpenERP session attribute
1029 :type req: < session -> OpenERPSession >
1030 :return: the root menu ids
1034 context = s.eval_context(req.context)
1035 Menus = s.model('ir.ui.menu')
1036 # If a menu action is defined use its domain to get the root menu items
1037 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1039 menu_domain = [('parent_id', '=', False)]
1041 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1043 menu_domain = ast.literal_eval(domain_string)
1045 return Menus.search(menu_domain, 0, False, False, context)
1047 def do_load(self, req):
1048 """ Loads all menu items (all applications and their sub-menus).
1050 :param req: A request object, with an OpenERP session attribute
1051 :type req: < session -> OpenERPSession >
1052 :return: the menu root
1053 :rtype: dict('children': menu_nodes)
1055 context = req.session.eval_context(req.context)
1056 Menus = req.session.model('ir.ui.menu')
1058 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1059 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1061 # menus are loaded fully unlike a regular tree view, cause there are a
1062 # limited number of items (752 when all 6.1 addons are installed)
1063 menu_ids = Menus.search([], 0, False, False, context)
1064 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1065 # adds roots at the end of the sequence, so that they will overwrite
1066 # equivalent menu items from full menu read when put into id:item
1067 # mapping, resulting in children being correctly set on the roots.
1068 menu_items.extend(menu_roots)
1070 # make a tree using parent_id
1071 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1072 for menu_item in menu_items:
1073 if menu_item['parent_id']:
1074 parent = menu_item['parent_id'][0]
1077 if parent in menu_items_map:
1078 menu_items_map[parent].setdefault(
1079 'children', []).append(menu_item)
1081 # sort by sequence a tree using parent_id
1082 for menu_item in menu_items:
1083 menu_item.setdefault('children', []).sort(
1084 key=operator.itemgetter('sequence'))
1088 @openerpweb.jsonrequest
1089 def action(self, req, menu_id):
1090 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1091 [('ir.ui.menu', menu_id)], False)
1092 return {"action": actions}
1094 class DataSet(openerpweb.Controller):
1095 _cp_path = "/web/dataset"
1097 @openerpweb.jsonrequest
1098 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1099 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1100 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1102 """ Performs a search() followed by a read() (if needed) using the
1103 provided search criteria
1105 :param req: a JSON-RPC request object
1106 :type req: openerpweb.JsonRequest
1107 :param str model: the name of the model to search on
1108 :param fields: a list of the fields to return in the result records
1110 :param int offset: from which index should the results start being returned
1111 :param int limit: the maximum number of records to return
1112 :param list domain: the search domain for the query
1113 :param list sort: sorting directives
1114 :returns: A structure (dict) with two keys: ids (all the ids matching
1115 the (domain, context) pair) and records (paginated records
1116 matching fields selection set)
1119 Model = req.session.model(model)
1121 context, domain = eval_context_and_domain(
1122 req.session, req.context, domain)
1124 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1125 if limit and len(ids) == limit:
1126 length = Model.search_count(domain, context)
1128 length = len(ids) + (offset or 0)
1129 if fields and fields == ['id']:
1130 # shortcut read if we only want the ids
1133 'records': [{'id': id} for id in ids]
1136 records = Model.read(ids, fields or False, context)
1137 records.sort(key=lambda obj: ids.index(obj['id']))
1143 @openerpweb.jsonrequest
1144 def load(self, req, model, id, fields):
1145 m = req.session.model(model)
1147 r = m.read([id], False, req.session.eval_context(req.context))
1150 return {'value': value}
1152 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1153 has_domain = domain_id is not None and domain_id < len(args)
1154 has_context = context_id is not None and context_id < len(args)
1156 domain = args[domain_id] if has_domain else []
1157 context = args[context_id] if has_context else {}
1158 c, d = eval_context_and_domain(req.session, context, domain)
1162 args[context_id] = c
1164 return self._call_kw(req, model, method, args, {})
1166 def _call_kw(self, req, model, method, args, kwargs):
1167 for i in xrange(len(args)):
1168 if isinstance(args[i], common.nonliterals.BaseContext):
1169 args[i] = req.session.eval_context(args[i])
1170 elif isinstance(args[i], common.nonliterals.BaseDomain):
1171 args[i] = req.session.eval_domain(args[i])
1172 for k in kwargs.keys():
1173 if isinstance(kwargs[k], common.nonliterals.BaseContext):
1174 kwargs[k] = req.session.eval_context(kwargs[k])
1175 elif isinstance(kwargs[k], common.nonliterals.BaseDomain):
1176 kwargs[k] = req.session.eval_domain(kwargs[k])
1178 # Temporary implements future display_name special field for model#read()
1179 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1180 if 'display_name' in args[1]:
1181 names = req.session.model(model).name_get(args[0], **kwargs)
1182 args[1].remove('display_name')
1183 r = getattr(req.session.model(model), method)(*args, **kwargs)
1184 for i in range(len(r)):
1185 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1188 return getattr(req.session.model(model), method)(*args, **kwargs)
1190 @openerpweb.jsonrequest
1191 def onchange(self, req, model, method, args, context_id=None):
1192 """ Support method for handling onchange calls: behaves much like call
1193 with the following differences:
1195 * Does not take a domain_id
1196 * Is aware of the return value's structure, and will parse the domains
1197 if needed in order to return either parsed literal domains (in JSON)
1198 or non-literal domain instances, allowing those domains to be used
1202 :type req: web.common.http.JsonRequest
1203 :param str model: object type on which to call the method
1204 :param str method: name of the onchange handler method
1205 :param list args: arguments to call the onchange handler with
1206 :param int context_id: index of the context object in the list of
1208 :return: result of the onchange call with all domains parsed
1210 result = self.call_common(req, model, method, args, context_id=context_id)
1211 if not result or 'domain' not in result:
1214 result['domain'] = dict(
1215 (k, parse_domain(v, req.session))
1216 for k, v in result['domain'].iteritems())
1220 @openerpweb.jsonrequest
1221 def call(self, req, model, method, args, domain_id=None, context_id=None):
1222 return self.call_common(req, model, method, args, domain_id, context_id)
1224 @openerpweb.jsonrequest
1225 def call_kw(self, req, model, method, args, kwargs):
1226 return self._call_kw(req, model, method, args, kwargs)
1228 @openerpweb.jsonrequest
1229 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1230 action = self.call_common(req, model, method, args, domain_id, context_id)
1231 if isinstance(action, dict) and action.get('type') != '':
1232 return clean_action(req, action)
1235 @openerpweb.jsonrequest
1236 def exec_workflow(self, req, model, id, signal):
1237 return req.session.exec_workflow(model, id, signal)
1239 @openerpweb.jsonrequest
1240 def resequence(self, req, model, ids, field='sequence', offset=0):
1241 """ Re-sequences a number of records in the model, by their ids
1243 The re-sequencing starts at the first model of ``ids``, the sequence
1244 number is incremented by one after each record and starts at ``offset``
1246 :param ids: identifiers of the records to resequence, in the new sequence order
1248 :param str field: field used for sequence specification, defaults to
1250 :param int offset: sequence number for first record in ``ids``, allows
1251 starting the resequencing from an arbitrary number,
1254 m = req.session.model(model)
1255 if not m.fields_get([field]):
1257 # python 2.6 has no start parameter
1258 for i, id in enumerate(ids):
1259 m.write(id, { field: i + offset })
1262 class DataGroup(openerpweb.Controller):
1263 _cp_path = "/web/group"
1264 @openerpweb.jsonrequest
1265 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1266 Model = req.session.model(model)
1267 context, domain = eval_context_and_domain(req.session, req.context, domain)
1269 return Model.read_group(
1270 domain or [], fields, group_by_fields, 0, False,
1271 dict(context, group_by=group_by_fields), sort or False)
1273 class View(openerpweb.Controller):
1274 _cp_path = "/web/view"
1276 def fields_view_get(self, req, model, view_id, view_type,
1277 transform=True, toolbar=False, submenu=False):
1278 Model = req.session.model(model)
1279 context = req.session.eval_context(req.context)
1280 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1281 # todo fme?: check that we should pass the evaluated context here
1282 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1283 if toolbar and transform:
1284 self.process_toolbar(req, fvg['toolbar'])
1287 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1288 # depending on how it feels, xmlrpclib.ServerProxy can translate
1289 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1290 # enjoy unicode strings which can not be trivially converted to
1291 # strings, and it blows up during parsing.
1293 # So ensure we fix this retardation by converting view xml back to
1295 if isinstance(fvg['arch'], unicode):
1296 arch = fvg['arch'].encode('utf-8')
1299 fvg['arch_string'] = arch
1302 evaluation_context = session.evaluation_context(context or {})
1303 xml = self.transform_view(arch, session, evaluation_context)
1305 xml = ElementTree.fromstring(arch)
1306 fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
1308 if 'id' in fvg['fields']:
1309 # Special case for id's
1310 id_field = fvg['fields']['id']
1311 id_field['original_type'] = id_field['type']
1312 id_field['type'] = 'id'
1314 for field in fvg['fields'].itervalues():
1315 if field.get('views'):
1316 for view in field["views"].itervalues():
1317 self.process_view(session, view, None, transform)
1318 if field.get('domain'):
1319 field["domain"] = parse_domain(field["domain"], session)
1320 if field.get('context'):
1321 field["context"] = parse_context(field["context"], session)
1323 def process_toolbar(self, req, toolbar):
1325 The toolbar is a mapping of section_key: [action_descriptor]
1327 We need to clean all those actions in order to ensure correct
1330 for actions in toolbar.itervalues():
1331 for action in actions:
1332 if 'context' in action:
1333 action['context'] = parse_context(
1334 action['context'], req.session)
1335 if 'domain' in action:
1336 action['domain'] = parse_domain(
1337 action['domain'], req.session)
1339 @openerpweb.jsonrequest
1340 def add_custom(self, req, view_id, arch):
1341 CustomView = req.session.model('ir.ui.view.custom')
1343 'user_id': req.session._uid,
1346 }, req.session.eval_context(req.context))
1347 return {'result': True}
1349 @openerpweb.jsonrequest
1350 def undo_custom(self, req, view_id, reset=False):
1351 CustomView = req.session.model('ir.ui.view.custom')
1352 context = req.session.eval_context(req.context)
1353 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1354 0, False, False, context)
1357 CustomView.unlink(vcustom, context)
1359 CustomView.unlink([vcustom[0]], context)
1360 return {'result': True}
1361 return {'result': False}
1363 def transform_view(self, view_string, session, context=None):
1364 # transform nodes on the fly via iterparse, instead of
1365 # doing it statically on the parsing result
1366 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1368 for event, elem in parser:
1369 if event == "start":
1372 self.parse_domains_and_contexts(elem, session)
1375 def parse_domains_and_contexts(self, elem, session):
1376 """ Converts domains and contexts from the view into Python objects,
1377 either literals if they can be parsed by literal_eval or a special
1378 placeholder object if the domain or context refers to free variables.
1380 :param elem: the current node being parsed
1381 :type param: xml.etree.ElementTree.Element
1382 :param session: OpenERP session object, used to store and retrieve
1384 :type session: openerpweb.openerpweb.OpenERPSession
1386 for el in ['domain', 'filter_domain']:
1387 domain = elem.get(el, '').strip()
1389 elem.set(el, parse_domain(domain, session))
1390 elem.set(el + '_string', domain)
1391 for el in ['context', 'default_get']:
1392 context_string = elem.get(el, '').strip()
1394 elem.set(el, parse_context(context_string, session))
1395 elem.set(el + '_string', context_string)
1397 @openerpweb.jsonrequest
1398 def load(self, req, model, view_id, view_type, toolbar=False):
1399 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1401 class TreeView(View):
1402 _cp_path = "/web/treeview"
1404 @openerpweb.jsonrequest
1405 def action(self, req, model, id):
1406 return load_actions_from_ir_values(
1407 req,'action', 'tree_but_open',[(model, id)],
1410 class SearchView(View):
1411 _cp_path = "/web/searchview"
1413 @openerpweb.jsonrequest
1414 def load(self, req, model, view_id):
1415 fields_view = self.fields_view_get(req, model, view_id, 'search')
1416 return {'fields_view': fields_view}
1418 @openerpweb.jsonrequest
1419 def fields_get(self, req, model):
1420 Model = req.session.model(model)
1421 fields = Model.fields_get(False, req.session.eval_context(req.context))
1422 for field in fields.values():
1423 # shouldn't convert the views too?
1424 if field.get('domain'):
1425 field["domain"] = parse_domain(field["domain"], req.session)
1426 if field.get('context'):
1427 field["context"] = parse_context(field["context"], req.session)
1428 return {'fields': fields}
1430 @openerpweb.jsonrequest
1431 def get_filters(self, req, model):
1432 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1433 Model = req.session.model("ir.filters")
1434 filters = Model.get_filters(model)
1435 for filter in filters:
1437 parsed_context = parse_context(filter["context"], req.session)
1438 filter["context"] = (parsed_context
1439 if not isinstance(parsed_context, common.nonliterals.BaseContext)
1440 else req.session.eval_context(parsed_context))
1442 parsed_domain = parse_domain(filter["domain"], req.session)
1443 filter["domain"] = (parsed_domain
1444 if not isinstance(parsed_domain, common.nonliterals.BaseDomain)
1445 else req.session.eval_domain(parsed_domain))
1447 logger.exception("Failed to parse custom filter %s in %s",
1448 filter['name'], model)
1449 filter['disabled'] = True
1450 del filter['context']
1451 del filter['domain']
1454 class Binary(openerpweb.Controller):
1455 _cp_path = "/web/binary"
1457 @openerpweb.httprequest
1458 def image(self, req, model, id, field, **kw):
1459 last_update = '__last_update'
1460 Model = req.session.model(model)
1461 context = req.session.eval_context(req.context)
1462 headers = [('Content-Type', 'image/png')]
1463 etag = req.httprequest.headers.get('If-None-Match')
1464 hashed_session = hashlib.md5(req.session_id).hexdigest()
1465 id = None if not id else simplejson.loads(id)
1466 if type(id) is list:
1469 if not id and hashed_session == etag:
1470 return werkzeug.wrappers.Response(status=304)
1472 date = Model.read([id], [last_update], context)[0].get(last_update)
1473 if hashlib.md5(date).hexdigest() == etag:
1474 return werkzeug.wrappers.Response(status=304)
1476 retag = hashed_session
1479 res = Model.default_get([field], context).get(field)
1480 image_data = base64.b64decode(res)
1482 res = Model.read([id], [last_update, field], context)[0]
1483 retag = hashlib.md5(res.get(last_update)).hexdigest()
1484 image_data = base64.b64decode(res.get(field))
1485 except (TypeError, xmlrpclib.Fault):
1486 image_data = self.placeholder(req)
1487 headers.append(('ETag', retag))
1488 headers.append(('Content-Length', len(image_data)))
1490 ncache = int(kw.get('cache'))
1491 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1494 return req.make_response(image_data, headers)
1495 def placeholder(self, req):
1496 addons_path = openerpweb.addons_manifest['web']['addons_path']
1497 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1498 def content_disposition(self, filename, req):
1499 filename = filename.encode('utf8')
1500 escaped = urllib2.quote(filename)
1501 browser = req.httprequest.user_agent.browser
1502 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
1503 if browser == 'msie' and version < 9:
1504 return "attachment; filename=%s" % escaped
1505 elif browser == 'safari':
1506 return "attachment; filename=%s" % filename
1508 return "attachment; filename*=UTF-8''%s" % escaped
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', self.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', self.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):
1621 Actions = req.session.model('ir.actions.actions')
1623 context = req.session.eval_context(req.context)
1626 action_id = int(action_id)
1629 module, xmlid = action_id.split('.', 1)
1630 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1631 assert model.startswith('ir.actions.')
1633 action_id = 0 # force failed read
1635 base_action = Actions.read([action_id], ['type'], context)
1638 action_type = base_action[0]['type']
1639 if action_type == 'ir.actions.report.xml':
1640 ctx.update({'bin_size': True})
1642 action = req.session.model(action_type).read([action_id], False, ctx)
1644 value = clean_action(req, action[0], do_not_eval)
1647 @openerpweb.jsonrequest
1648 def run(self, req, action_id):
1649 return_action = req.session.model('ir.actions.server').run(
1650 [action_id], req.session.eval_context(req.context))
1652 return clean_action(req, return_action)
1657 _cp_path = "/web/export"
1659 @openerpweb.jsonrequest
1660 def formats(self, req):
1661 """ Returns all valid export formats
1663 :returns: for each export format, a pair of identifier and printable name
1664 :rtype: [(str, str)]
1668 for path, controller in openerpweb.controllers_path.iteritems()
1669 if path.startswith(self._cp_path)
1670 if hasattr(controller, 'fmt')
1671 ], key=operator.itemgetter("label"))
1673 def fields_get(self, req, model):
1674 Model = req.session.model(model)
1675 fields = Model.fields_get(False, req.session.eval_context(req.context))
1678 @openerpweb.jsonrequest
1679 def get_fields(self, req, model, prefix='', parent_name= '',
1680 import_compat=True, parent_field_type=None,
1683 if import_compat and parent_field_type == "many2one":
1686 fields = self.fields_get(req, model)
1689 fields.pop('id', None)
1691 fields['.id'] = fields.pop('id', {'string': 'ID'})
1693 fields_sequence = sorted(fields.iteritems(),
1694 key=lambda field: field[1].get('string', ''))
1697 for field_name, field in fields_sequence:
1699 if exclude and field_name in exclude:
1701 if field.get('readonly'):
1702 # If none of the field's states unsets readonly, skip the field
1703 if all(dict(attrs).get('readonly', True)
1704 for attrs in field.get('states', {}).values()):
1707 id = prefix + (prefix and '/'or '') + field_name
1708 name = parent_name + (parent_name and '/' or '') + field['string']
1709 record = {'id': id, 'string': name,
1710 'value': id, 'children': False,
1711 'field_type': field.get('type'),
1712 'required': field.get('required'),
1713 'relation_field': field.get('relation_field')}
1714 records.append(record)
1716 if len(name.split('/')) < 3 and 'relation' in field:
1717 ref = field.pop('relation')
1718 record['value'] += '/id'
1719 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1721 if not import_compat or field['type'] == 'one2many':
1722 # m2m field in import_compat is childless
1723 record['children'] = True
1727 @openerpweb.jsonrequest
1728 def namelist(self,req, model, export_id):
1729 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1730 export = req.session.model("ir.exports").read([export_id])[0]
1731 export_fields_list = req.session.model("ir.exports.line").read(
1732 export['export_fields'])
1734 fields_data = self.fields_info(
1735 req, model, map(operator.itemgetter('name'), export_fields_list))
1738 {'name': field['name'], 'label': fields_data[field['name']]}
1739 for field in export_fields_list
1742 def fields_info(self, req, model, export_fields):
1744 fields = self.fields_get(req, model)
1746 # To make fields retrieval more efficient, fetch all sub-fields of a
1747 # given field at the same time. Because the order in the export list is
1748 # arbitrary, this requires ordering all sub-fields of a given field
1749 # together so they can be fetched at the same time
1751 # Works the following way:
1752 # * sort the list of fields to export, the default sorting order will
1753 # put the field itself (if present, for xmlid) and all of its
1754 # sub-fields right after it
1755 # * then, group on: the first field of the path (which is the same for
1756 # a field and for its subfields and the length of splitting on the
1757 # first '/', which basically means grouping the field on one side and
1758 # all of the subfields on the other. This way, we have the field (for
1759 # the xmlid) with length 1, and all of the subfields with the same
1760 # base but a length "flag" of 2
1761 # * if we have a normal field (length 1), just add it to the info
1762 # mapping (with its string) as-is
1763 # * otherwise, recursively call fields_info via graft_subfields.
1764 # all graft_subfields does is take the result of fields_info (on the
1765 # field's model) and prepend the current base (current field), which
1766 # rebuilds the whole sub-tree for the field
1768 # result: because we're not fetching the fields_get for half the
1769 # database models, fetching a namelist with a dozen fields (including
1770 # relational data) falls from ~6s to ~300ms (on the leads model).
1771 # export lists with no sub-fields (e.g. import_compatible lists with
1772 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1773 # there's a single fields_get to execute)
1774 for (base, length), subfields in itertools.groupby(
1775 sorted(export_fields),
1776 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1777 subfields = list(subfields)
1779 # subfields is a seq of $base/*rest, and not loaded yet
1780 info.update(self.graft_subfields(
1781 req, fields[base]['relation'], base, fields[base]['string'],
1785 info[base] = fields[base]['string']
1789 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1790 export_fields = [field.split('/', 1)[1] for field in fields]
1792 (prefix + '/' + k, prefix_string + '/' + v)
1793 for k, v in self.fields_info(req, model, export_fields).iteritems())
1795 #noinspection PyPropertyDefinition
1797 def content_type(self):
1798 """ Provides the format's content type """
1799 raise NotImplementedError()
1801 def filename(self, base):
1802 """ Creates a valid filename for the format (with extension) from the
1803 provided base name (exension-less)
1805 raise NotImplementedError()
1807 def from_data(self, fields, rows):
1808 """ Conversion method from OpenERP's export data to whatever the
1809 current export class outputs
1811 :params list fields: a list of fields to export
1812 :params list rows: a list of records to export
1816 raise NotImplementedError()
1818 @openerpweb.httprequest
1819 def index(self, req, data, token):
1820 model, fields, ids, domain, import_compat = \
1821 operator.itemgetter('model', 'fields', 'ids', 'domain',
1823 simplejson.loads(data))
1825 context = req.session.eval_context(req.context)
1826 Model = req.session.model(model)
1827 ids = ids or Model.search(domain, 0, False, False, context)
1829 field_names = map(operator.itemgetter('name'), fields)
1830 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1833 columns_headers = field_names
1835 columns_headers = [val['label'].strip() for val in fields]
1838 return req.make_response(self.from_data(columns_headers, import_data),
1839 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1840 ('Content-Type', self.content_type)],
1841 cookies={'fileToken': int(token)})
1843 class CSVExport(Export):
1844 _cp_path = '/web/export/csv'
1845 fmt = {'tag': 'csv', 'label': 'CSV'}
1848 def content_type(self):
1849 return 'text/csv;charset=utf8'
1851 def filename(self, base):
1852 return base + '.csv'
1854 def from_data(self, fields, rows):
1856 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1858 writer.writerow([name.encode('utf-8') for name in fields])
1863 if isinstance(d, basestring):
1864 d = d.replace('\n',' ').replace('\t',' ')
1866 d = d.encode('utf-8')
1867 except UnicodeError:
1869 if d is False: d = None
1871 writer.writerow(row)
1878 class ExcelExport(Export):
1879 _cp_path = '/web/export/xls'
1883 'error': None if xlwt else "XLWT required"
1887 def content_type(self):
1888 return 'application/vnd.ms-excel'
1890 def filename(self, base):
1891 return base + '.xls'
1893 def from_data(self, fields, rows):
1894 workbook = xlwt.Workbook()
1895 worksheet = workbook.add_sheet('Sheet 1')
1897 for i, fieldname in enumerate(fields):
1898 worksheet.write(0, i, fieldname)
1899 worksheet.col(i).width = 8000 # around 220 pixels
1901 style = xlwt.easyxf('align: wrap yes')
1903 for row_index, row in enumerate(rows):
1904 for cell_index, cell_value in enumerate(row):
1905 if isinstance(cell_value, basestring):
1906 cell_value = re.sub("\r", " ", cell_value)
1907 if cell_value is False: cell_value = None
1908 worksheet.write(row_index + 1, cell_index, cell_value, style)
1917 class Reports(View):
1918 _cp_path = "/web/report"
1919 POLLING_DELAY = 0.25
1921 'doc': 'application/vnd.ms-word',
1922 'html': 'text/html',
1923 'odt': 'application/vnd.oasis.opendocument.text',
1924 'pdf': 'application/pdf',
1925 'sxw': 'application/vnd.sun.xml.writer',
1926 'xls': 'application/vnd.ms-excel',
1929 @openerpweb.httprequest
1930 def index(self, req, action, token):
1931 action = simplejson.loads(action)
1933 report_srv = req.session.proxy("report")
1934 context = req.session.eval_context(
1935 common.nonliterals.CompoundContext(
1936 req.context or {}, action[ "context"]))
1939 report_ids = context["active_ids"]
1940 if 'report_type' in action:
1941 report_data['report_type'] = action['report_type']
1942 if 'datas' in action:
1943 if 'ids' in action['datas']:
1944 report_ids = action['datas'].pop('ids')
1945 report_data.update(action['datas'])
1947 report_id = report_srv.report(
1948 req.session._db, req.session._uid, req.session._password,
1949 action["report_name"], report_ids,
1950 report_data, context)
1952 report_struct = None
1954 report_struct = report_srv.report_get(
1955 req.session._db, req.session._uid, req.session._password, report_id)
1956 if report_struct["state"]:
1959 time.sleep(self.POLLING_DELAY)
1961 report = base64.b64decode(report_struct['result'])
1962 if report_struct.get('code') == 'zlib':
1963 report = zlib.decompress(report)
1964 report_mimetype = self.TYPES_MAPPING.get(
1965 report_struct['format'], 'octet-stream')
1966 file_name = action.get('name', 'report')
1967 if 'name' not in action:
1968 reports = req.session.model('ir.actions.report.xml')
1969 res_id = reports.search([('report_name', '=', action['report_name']),],
1970 0, False, False, context)
1972 file_name = reports.read(res_id[0], ['name'], context)['name']
1974 file_name = action['report_name']
1976 return req.make_response(report,
1978 # maybe we should take of what characters can appear in a file name?
1979 ('Content-Disposition', 'attachment; filename="%s.%s"' % (file_name, report_struct['format'])),
1980 ('Content-Type', report_mimetype),
1981 ('Content-Length', len(report))],
1982 cookies={'fileToken': int(token)})
1985 _cp_path = "/web/import"
1987 def fields_get(self, req, model):
1988 Model = req.session.model(model)
1989 fields = Model.fields_get(False, req.session.eval_context(req.context))
1992 @openerpweb.httprequest
1993 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1995 data = list(csv.reader(
1996 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1997 except csv.Error, e:
1999 return '<script>window.top.%s(%s);</script>' % (
2000 jsonp, simplejson.dumps({'error': {
2001 'message': 'Error parsing CSV file: %s' % e,
2002 # decodes each byte to a unicode character, which may or
2003 # may not be printable, but decoding will succeed.
2004 # Otherwise simplejson will try to decode the `str` using
2005 # utf-8, which is very likely to blow up on characters out
2006 # of the ascii range (in range [128, 256))
2007 'preview': csvfile.read(200).decode('iso-8859-1')}}))
2010 return '<script>window.top.%s(%s);</script>' % (
2011 jsonp, simplejson.dumps(
2012 {'records': data[:10]}, encoding=csvcode))
2013 except UnicodeDecodeError:
2014 return '<script>window.top.%s(%s);</script>' % (
2015 jsonp, simplejson.dumps({
2016 'message': u"Failed to decode CSV file using encoding %s, "
2017 u"try switching to a different encoding" % csvcode
2020 @openerpweb.httprequest
2021 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
2023 modle_obj = req.session.model(model)
2024 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
2025 simplejson.loads(meta))
2028 if not (csvdel and len(csvdel) == 1):
2029 error = u"The CSV delimiter must be a single character"
2031 if not indices and fields:
2032 error = u"You must select at least one field to import"
2035 return '<script>window.top.%s(%s);</script>' % (
2036 jsonp, simplejson.dumps({'error': {'message': error}}))
2038 # skip ignored records (@skip parameter)
2039 # then skip empty lines (not valid csv)
2040 # nb: should these operations be reverted?
2041 rows_to_import = itertools.ifilter(
2044 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
2047 # if only one index, itemgetter will return an atom rather than a tuple
2048 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
2049 else: mapper = operator.itemgetter(*indices)
2054 # decode each data row
2056 [record.decode(csvcode) for record in row]
2057 for row in itertools.imap(mapper, rows_to_import)
2058 # don't insert completely empty rows (can happen due to fields
2059 # filtering in case of e.g. o2m content rows)
2062 except UnicodeDecodeError:
2063 error = u"Failed to decode CSV file using encoding %s" % csvcode
2064 except csv.Error, e:
2065 error = u"Could not process CSV file: %s" % e
2067 # If the file contains nothing,
2069 error = u"File to import is empty"
2071 return '<script>window.top.%s(%s);</script>' % (
2072 jsonp, simplejson.dumps({'error': {'message': error}}))
2075 (code, record, message, _nope) = modle_obj.import_data(
2076 fields, data, 'init', '', False,
2077 req.session.eval_context(req.context))
2078 except xmlrpclib.Fault, e:
2079 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
2080 return '<script>window.top.%s(%s);</script>' % (
2081 jsonp, simplejson.dumps({'error':error}))
2084 return '<script>window.top.%s(%s);</script>' % (
2085 jsonp, simplejson.dumps({'success':True}))
2087 msg = u"Error during import: %s\n\nTrying to import record %r" % (
2089 return '<script>window.top.%s(%s);</script>' % (
2090 jsonp, simplejson.dumps({'error': {'message':msg}}))
2092 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: