1 # -*- coding: utf-8 -*-
19 from xml.etree import ElementTree
20 from cStringIO import StringIO
22 import babel.messages.pofile
24 import werkzeug.wrappers
31 openerpweb = common.http
33 #----------------------------------------------------------
35 #----------------------------------------------------------
38 """ Minify js with a clever regex.
39 Taken from http://opensource.perlig.de/rjsmin
40 Apache License, Version 2.0 """
42 """ Substitution callback """
43 groups = match.groups()
49 (groups[4] and '\n') or
50 (groups[5] and ' ') or
51 (groups[6] and ' ') or
52 (groups[7] and ' ') or
57 r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
58 r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
59 r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
60 r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
61 r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
62 r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
63 r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
64 r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
65 r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
66 r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
67 r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
68 r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
69 r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
70 r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
71 r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
72 r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
73 r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
74 r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
75 r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
76 r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
77 r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
82 # Validated by diff -u of sass2scss against:
83 # sass-convert -F sass -T scss openerp.sass openerp.scss
86 reComment = re.compile(r'//.*$')
87 reIndent = re.compile(r'^\s+')
88 reIgnore = re.compile(r'^\s*(//.*)?$')
89 reFixes = { re.compile(r'\(\((.*)\)\)') : r'(\1)', }
92 for l in src.split('\n'):
94 if reIgnore.search(l): continue
95 l = reComment.sub('', l)
97 indent = reIndent.match(l)
98 level = indent.end() if indent else 0
101 prevBlocks[lastLevel] = block
103 block[-1] = (block[-1], newBlock)
105 elif level<lastLevel:
106 block = prevBlocks[level]
110 for ereg, repl in reFixes.items():
111 l = ereg.sub(repl if type(repl)==str else repl(), l)
114 def write(sass, level=-1):
117 if type(sass)==tuple:
119 out += indent+sass[0]+" {\n"
121 out += write(e, level+1)
123 out = out.rstrip(" \n")
128 out += indent+sass+";\n"
134 proxy = req.session.proxy("db")
136 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
138 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
139 dbs = [i for i in dbs if re.match(r, i)]
142 def module_topological_sort(modules):
143 """ Return a list of module names sorted so that their dependencies of the
144 modules are listed before the module itself
146 modules is a dict of {module_name: dependencies}
148 :param modules: modules to sort
153 dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
154 # incoming edge: dependency on other module (if a depends on b, a has an
155 # incoming edge from b, aka there's an edge from b to a)
156 # outgoing edge: other module depending on this one
158 # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
159 #L ← Empty list that will contain the sorted nodes
161 #S ← Set of all nodes with no outgoing edges (modules on which no other
163 S = set(module for module in modules if module not in dependencies)
166 #function visit(node n)
168 #if n has not been visited yet then
172 #change: n not web module, can not be resolved, ignore
173 if n not in modules: return
174 #for each node m with an edge from m to n do (dependencies of n)
180 #for each node n in S do
186 def module_installed(req):
187 # Candidates module the current heuristic is the /static dir
188 loadable = openerpweb.addons_manifest.keys()
191 # Retrieve database installed modules
192 # TODO The following code should move to ir.module.module.list_installed_modules()
193 Modules = req.session.model('ir.module.module')
194 domain = [('state','=','installed'), ('name','in', loadable)]
195 for module in Modules.search_read(domain, ['name', 'dependencies_id']):
196 modules[module['name']] = []
197 deps = module.get('dependencies_id')
199 deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
200 dependencies = [i['name'] for i in deps_read]
201 modules[module['name']] = dependencies
203 sorted_modules = module_topological_sort(modules)
204 return sorted_modules
206 def module_installed_bypass_session(dbname):
207 loadable = openerpweb.addons_manifest.keys()
210 import openerp.modules.registry
211 registry = openerp.modules.registry.RegistryManager.get(dbname)
212 with registry.cursor() as cr:
213 m = registry.get('ir.module.module')
214 # TODO The following code should move to ir.module.module.list_installed_modules()
215 domain = [('state','=','installed'), ('name','in', loadable)]
216 ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
217 for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
218 modules[module['name']] = []
219 deps = module.get('dependencies_id')
221 deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
222 dependencies = [i['name'] for i in deps_read]
223 modules[module['name']] = dependencies
226 sorted_modules = module_topological_sort(modules)
227 return sorted_modules
229 def module_boot(req):
232 for i in req.config.server_wide_modules:
233 if i in openerpweb.addons_manifest:
235 # if only one db load every module at boot
239 except xmlrpclib.Fault:
240 # ignore access denied
243 dbside = module_installed_bypass_session(dbs[0])
244 dbside = [i for i in dbside if i not in serverside]
245 addons = serverside + dbside
248 def concat_xml(file_list):
249 """Concatenate xml files
251 :param list(str) file_list: list of files to check
252 :returns: (concatenation_result, checksum)
255 checksum = hashlib.new('sha1')
257 return '', checksum.hexdigest()
260 for fname in file_list:
261 with open(fname, 'rb') as fp:
263 checksum.update(contents)
265 xml = ElementTree.parse(fp).getroot()
268 root = ElementTree.Element(xml.tag)
269 #elif root.tag != xml.tag:
270 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
272 for child in xml.getchildren():
274 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
276 def concat_files(file_list, reader=None, intersperse=""):
277 """ Concatenates contents of all provided files
279 :param list(str) file_list: list of files to check
280 :param function reader: reading procedure for each file
281 :param str intersperse: string to intersperse between file contents
282 :returns: (concatenation_result, checksum)
285 checksum = hashlib.new('sha1')
287 return '', checksum.hexdigest()
291 with open(f, 'rb') as fp:
295 for fname in file_list:
296 contents = reader(fname)
297 checksum.update(contents)
298 files_content.append(contents)
300 files_concat = intersperse.join(files_content)
301 return files_concat, checksum.hexdigest()
303 def concat_js(file_list):
304 content, checksum = concat_files(file_list, intersperse=';')
305 content = rjsmin(content)
306 return content, checksum
308 def manifest_glob(req, addons, key):
310 addons = module_boot(req)
312 addons = addons.split(',')
315 manifest = openerpweb.addons_manifest.get(addon, None)
318 # ensure does not ends with /
319 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
320 globlist = manifest.get(key, [])
321 for pattern in globlist:
322 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
323 r.append((path, path[len(addons_path):]))
326 def manifest_list(req, mods, extension):
328 path = '/web/webclient/' + extension
330 path += '?mods=' + mods
332 files = manifest_glob(req, mods, extension)
333 i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
334 req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
336 return [wp for _fp, wp in files]
338 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
340 def get_last_modified(files):
341 """ Returns the modification time of the most recently modified
344 :param list(str) files: names of files to check
345 :return: most recent modification time amongst the fileset
346 :rtype: datetime.datetime
350 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
352 return datetime.datetime(1970, 1, 1)
354 def make_conditional(req, response, last_modified=None, etag=None):
355 """ Makes the provided response conditional based upon the request,
356 and mandates revalidation from clients
358 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
359 setting ``last_modified`` and ``etag`` correctly on the response object
361 :param req: OpenERP request
362 :type req: web.common.http.WebRequest
363 :param response: Werkzeug response
364 :type response: werkzeug.wrappers.Response
365 :param datetime.datetime last_modified: last modification date of the response content
366 :param str etag: some sort of checksum of the content (deep etag)
367 :return: the response object provided
368 :rtype: werkzeug.wrappers.Response
370 response.cache_control.must_revalidate = True
371 response.cache_control.max_age = 0
373 response.last_modified = last_modified
375 response.set_etag(etag)
376 return response.make_conditional(req.httprequest)
378 def login_and_redirect(req, db, login, key, redirect_url='/'):
379 req.session.authenticate(db, login, key, {})
380 return set_cookie_and_redirect(req, redirect_url)
382 def set_cookie_and_redirect(req, redirect_url):
383 redirect = werkzeug.utils.redirect(redirect_url, 303)
384 redirect.autocorrect_location_header = False
385 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
386 redirect.set_cookie('instance0|session_id', cookie_val)
389 def eval_context_and_domain(session, context, domain=None):
390 e_context = session.eval_context(context)
391 # should we give the evaluated context as an evaluation context to the domain?
392 e_domain = session.eval_domain(domain or [])
394 return e_context, e_domain
396 def load_actions_from_ir_values(req, key, key2, models, meta):
397 context = req.session.eval_context(req.context)
398 Values = req.session.model('ir.values')
399 actions = Values.get(key, key2, models, meta, context)
401 return [(id, name, clean_action(req, action))
402 for id, name, action in actions]
404 def clean_action(req, action, do_not_eval=False):
405 action.setdefault('flags', {})
407 context = req.session.eval_context(req.context)
408 eval_ctx = req.session.evaluation_context(context)
411 # values come from the server, we can just eval them
412 if action.get('context') and isinstance(action.get('context'), basestring):
413 action['context'] = eval( action['context'], eval_ctx ) or {}
415 if action.get('domain') and isinstance(action.get('domain'), basestring):
416 action['domain'] = eval( action['domain'], eval_ctx ) or []
418 if 'context' in action:
419 action['context'] = parse_context(action['context'], req.session)
420 if 'domain' in action:
421 action['domain'] = parse_domain(action['domain'], req.session)
423 action_type = action.setdefault('type', 'ir.actions.act_window_close')
424 if action_type == 'ir.actions.act_window':
425 return fix_view_modes(action)
428 # I think generate_views,fix_view_modes should go into js ActionManager
429 def generate_views(action):
431 While the server generates a sequence called "views" computing dependencies
432 between a bunch of stuff for views coming directly from the database
433 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
434 to return custom view dictionaries generated on the fly.
436 In that case, there is no ``views`` key available on the action.
438 Since the web client relies on ``action['views']``, generate it here from
439 ``view_mode`` and ``view_id``.
441 Currently handles two different cases:
443 * no view_id, multiple view_mode
444 * single view_id, single view_mode
446 :param dict action: action descriptor dictionary to generate a views key for
448 view_id = action.get('view_id') or False
449 if isinstance(view_id, (list, tuple)):
452 # providing at least one view mode is a requirement, not an option
453 view_modes = action['view_mode'].split(',')
455 if len(view_modes) > 1:
457 raise ValueError('Non-db action dictionaries should provide '
458 'either multiple view modes or a single view '
459 'mode and an optional view id.\n\n Got view '
460 'modes %r and view id %r for action %r' % (
461 view_modes, view_id, action))
462 action['views'] = [(False, mode) for mode in view_modes]
464 action['views'] = [(view_id, view_modes[0])]
466 def fix_view_modes(action):
467 """ For historical reasons, OpenERP has weird dealings in relation to
468 view_mode and the view_type attribute (on window actions):
470 * one of the view modes is ``tree``, which stands for both list views
472 * the choice is made by checking ``view_type``, which is either
473 ``form`` for a list view or ``tree`` for an actual tree view
475 This methods simply folds the view_type into view_mode by adding a
476 new view mode ``list`` which is the result of the ``tree`` view_mode
477 in conjunction with the ``form`` view_type.
479 TODO: this should go into the doc, some kind of "peculiarities" section
481 :param dict action: an action descriptor
482 :returns: nothing, the action is modified in place
484 if not action.get('views'):
485 generate_views(action)
487 if action.pop('view_type', 'form') != 'form':
490 if 'view_mode' in action:
491 action['view_mode'] = ','.join(
492 mode if mode != 'tree' else 'list'
493 for mode in action['view_mode'].split(','))
495 [id, mode if mode != 'tree' else 'list']
496 for id, mode in action['views']
501 def parse_domain(domain, session):
502 """ Parses an arbitrary string containing a domain, transforms it
503 to either a literal domain or a :class:`common.nonliterals.Domain`
505 :param domain: the domain to parse, if the domain is not a string it
506 is assumed to be a literal domain and is returned as-is
507 :param session: Current OpenERP session
508 :type session: openerpweb.openerpweb.OpenERPSession
510 if not isinstance(domain, basestring):
513 return ast.literal_eval(domain)
516 return common.nonliterals.Domain(session, domain)
518 def parse_context(context, session):
519 """ Parses an arbitrary string containing a context, transforms it
520 to either a literal context or a :class:`common.nonliterals.Context`
522 :param context: the context to parse, if the context is not a string it
523 is assumed to be a literal domain and is returned as-is
524 :param session: Current OpenERP session
525 :type session: openerpweb.openerpweb.OpenERPSession
527 if not isinstance(context, basestring):
530 return ast.literal_eval(context)
532 return common.nonliterals.Context(session, context)
534 #----------------------------------------------------------
535 # OpenERP Web web Controllers
536 #----------------------------------------------------------
538 html_template = """<!DOCTYPE html>
539 <html style="height: 100%%">
541 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
542 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
543 <title>OpenERP</title>
544 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
545 <link rel="stylesheet" href="/web/static/src/css/full.css" />
548 <script type="text/javascript">
550 var s = new openerp.init(%(modules)s);
559 class Home(openerpweb.Controller):
562 @openerpweb.httprequest
563 def index(self, req, s_action=None, **kw):
564 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, None, 'js'))
565 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, None, 'css'))
567 r = html_template % {
570 'modules': simplejson.dumps(module_boot(req)),
571 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
575 @openerpweb.httprequest
576 def login(self, req, db, login, key):
577 return login_and_redirect(req, db, login, key)
579 class WebClient(openerpweb.Controller):
580 _cp_path = "/web/webclient"
582 @openerpweb.jsonrequest
583 def csslist(self, req, mods=None):
584 return manifest_list(req, mods, 'css')
586 @openerpweb.jsonrequest
587 def jslist(self, req, mods=None):
588 return manifest_list(req, mods, 'js')
590 @openerpweb.jsonrequest
591 def qweblist(self, req, mods=None):
592 return manifest_list(req, mods, 'qweb')
594 @openerpweb.httprequest
595 def css(self, req, mods=None):
596 files = list(manifest_glob(req, mods, 'css'))
597 last_modified = get_last_modified(f[0] for f in files)
598 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
599 return werkzeug.wrappers.Response(status=304)
601 file_map = dict(files)
603 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
604 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
607 """read the a css file and absolutify all relative uris"""
608 with open(f, 'rb') as fp:
609 data = fp.read().decode('utf-8')
612 # convert FS path into web path
613 web_dir = '/'.join(os.path.dirname(path).split(os.path.sep))
617 r"""@import \1%s/""" % (web_dir,),
623 r"""url(\1%s/""" % (web_dir,),
626 return data.encode('utf-8')
628 content, checksum = concat_files((f[0] for f in files), reader)
630 return make_conditional(
631 req, req.make_response(content, [('Content-Type', 'text/css')]),
632 last_modified, checksum)
634 @openerpweb.httprequest
635 def js(self, req, mods=None):
636 files = [f[0] for f in manifest_glob(req, mods, 'js')]
637 last_modified = get_last_modified(files)
638 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
639 return werkzeug.wrappers.Response(status=304)
641 content, checksum = concat_js(files)
643 return make_conditional(
644 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
645 last_modified, checksum)
647 @openerpweb.httprequest
648 def qweb(self, req, mods=None):
649 files = [f[0] for f in manifest_glob(req, mods, 'qweb')]
650 last_modified = get_last_modified(files)
651 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
652 return werkzeug.wrappers.Response(status=304)
654 content, checksum = concat_xml(files)
656 return make_conditional(
657 req, req.make_response(content, [('Content-Type', 'text/xml')]),
658 last_modified, checksum)
660 @openerpweb.jsonrequest
661 def translations(self, req, mods, lang):
662 lang_model = req.session.model('res.lang')
663 ids = lang_model.search([("code", "=", lang)])
665 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
666 "grouping", "decimal_point", "thousands_sep"])
674 langs = lang.split(separator)
675 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
678 for addon_name in mods:
679 transl = {"messages":[]}
680 transs[addon_name] = transl
681 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
683 f_name = os.path.join(addons_path, addon_name, "i18n", l + ".po")
684 if not os.path.exists(f_name):
687 with open(f_name) as t_file:
688 po = babel.messages.pofile.read_po(t_file)
692 if x.id and x.string and "openerp-web" in x.auto_comments:
693 transl["messages"].append({'id': x.id, 'string': x.string})
694 return {"modules": transs,
695 "lang_parameters": lang_obj}
697 @openerpweb.jsonrequest
698 def version_info(self, req):
700 "version": common.release.version
703 class Proxy(openerpweb.Controller):
704 _cp_path = '/web/proxy'
706 @openerpweb.jsonrequest
707 def load(self, req, path):
708 """ Proxies an HTTP request through a JSON request.
710 It is strongly recommended to not request binary files through this,
711 as the result will be a binary data blob as well.
713 :param req: OpenERP request
714 :param path: actual request path
715 :return: file content
717 from werkzeug.test import Client
718 from werkzeug.wrappers import BaseResponse
720 return Client(req.httprequest.app, BaseResponse).get(path).data
722 class Database(openerpweb.Controller):
723 _cp_path = "/web/database"
725 @openerpweb.jsonrequest
726 def get_list(self, req):
728 return {"db_list": dbs}
730 @openerpweb.jsonrequest
731 def create(self, req, fields):
732 params = dict(map(operator.itemgetter('name', 'value'), fields))
734 params['super_admin_pwd'],
736 bool(params.get('demo_data')),
738 params['create_admin_pwd']
741 return req.session.proxy("db").create_database(*create_attrs)
743 @openerpweb.jsonrequest
744 def drop(self, req, fields):
745 password, db = operator.itemgetter(
746 'drop_pwd', 'drop_db')(
747 dict(map(operator.itemgetter('name', 'value'), fields)))
750 return req.session.proxy("db").drop(password, db)
751 except xmlrpclib.Fault, e:
752 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
753 return {'error': e.faultCode, 'title': 'Drop Database'}
754 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
756 @openerpweb.httprequest
757 def backup(self, req, backup_db, backup_pwd, token):
759 db_dump = base64.b64decode(
760 req.session.proxy("db").dump(backup_pwd, backup_db))
761 filename = "%(db)s_%(timestamp)s.dump" % {
763 'timestamp': datetime.datetime.utcnow().strftime(
764 "%Y-%m-%d_%H-%M-%SZ")
766 return req.make_response(db_dump,
767 [('Content-Type', 'application/octet-stream; charset=binary'),
768 ('Content-Disposition', 'attachment; filename="' + filename + '"')],
769 {'fileToken': int(token)}
771 except xmlrpclib.Fault, e:
772 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
774 @openerpweb.httprequest
775 def restore(self, req, db_file, restore_pwd, new_db):
777 data = base64.b64encode(db_file.read())
778 req.session.proxy("db").restore(restore_pwd, new_db, data)
780 except xmlrpclib.Fault, e:
781 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
782 raise Exception("AccessDenied")
784 @openerpweb.jsonrequest
785 def change_password(self, req, fields):
786 old_password, new_password = operator.itemgetter(
787 'old_pwd', 'new_pwd')(
788 dict(map(operator.itemgetter('name', 'value'), fields)))
790 return req.session.proxy("db").change_admin_password(old_password, new_password)
791 except xmlrpclib.Fault, e:
792 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
793 return {'error': e.faultCode, 'title': 'Change Password'}
794 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
796 class Session(openerpweb.Controller):
797 _cp_path = "/web/session"
799 def session_info(self, req):
800 req.session.ensure_valid()
802 "session_id": req.session_id,
803 "uid": req.session._uid,
804 "context": req.session.get_context() if req.session._uid else {},
805 "db": req.session._db,
806 "login": req.session._login,
809 @openerpweb.jsonrequest
810 def get_session_info(self, req):
811 return self.session_info(req)
813 @openerpweb.jsonrequest
814 def authenticate(self, req, db, login, password, base_location=None):
815 wsgienv = req.httprequest.environ
816 release = common.release
818 base_location=base_location,
819 HTTP_HOST=wsgienv['HTTP_HOST'],
820 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
821 user_agent="%s / %s" % (release.name, release.version),
823 req.session.authenticate(db, login, password, env)
825 return self.session_info(req)
827 @openerpweb.jsonrequest
828 def change_password (self,req,fields):
829 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
830 dict(map(operator.itemgetter('name', 'value'), fields)))
831 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
832 return {'error':'All passwords have to be filled.','title': 'Change Password'}
833 if new_password != confirm_password:
834 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
836 if req.session.model('res.users').change_password(
837 old_password, new_password):
838 return {'new_password':new_password}
840 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
841 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
843 @openerpweb.jsonrequest
844 def sc_list(self, req):
845 return req.session.model('ir.ui.view_sc').get_sc(
846 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
848 @openerpweb.jsonrequest
849 def get_lang_list(self, req):
852 'lang_list': (req.session.proxy("db").list_lang() or []),
856 return {"error": e, "title": "Languages"}
858 @openerpweb.jsonrequest
859 def modules(self, req):
860 # return all installed modules. Web client is smart enough to not load a module twice
861 return module_installed(req)
863 @openerpweb.jsonrequest
864 def eval_domain_and_context(self, req, contexts, domains,
866 """ Evaluates sequences of domains and contexts, composing them into
867 a single context, domain or group_by sequence.
869 :param list contexts: list of contexts to merge together. Contexts are
870 evaluated in sequence, all previous contexts
871 are part of their own evaluation context
872 (starting at the session context).
873 :param list domains: list of domains to merge together. Domains are
874 evaluated in sequence and appended to one another
875 (implicit AND), their evaluation domain is the
876 result of merging all contexts.
877 :param list group_by_seq: list of domains (which may be in a different
878 order than the ``contexts`` parameter),
879 evaluated in sequence, their ``'group_by'``
880 key is extracted if they have one.
885 the global context created by merging all of
889 the concatenation of all domains
892 a list of fields to group by, potentially empty (in which case
893 no group by should be performed)
895 context, domain = eval_context_and_domain(req.session,
896 common.nonliterals.CompoundContext(*(contexts or [])),
897 common.nonliterals.CompoundDomain(*(domains or [])))
899 group_by_sequence = []
900 for candidate in (group_by_seq or []):
901 ctx = req.session.eval_context(candidate, context)
902 group_by = ctx.get('group_by')
905 elif isinstance(group_by, basestring):
906 group_by_sequence.append(group_by)
908 group_by_sequence.extend(group_by)
913 'group_by': group_by_sequence
916 @openerpweb.jsonrequest
917 def save_session_action(self, req, the_action):
919 This method store an action object in the session object and returns an integer
920 identifying that action. The method get_session_action() can be used to get
923 :param the_action: The action to save in the session.
924 :type the_action: anything
925 :return: A key identifying the saved action.
928 saved_actions = req.httpsession.get('saved_actions')
929 if not saved_actions:
930 saved_actions = {"next":0, "actions":{}}
931 req.httpsession['saved_actions'] = saved_actions
932 # we don't allow more than 10 stored actions
933 if len(saved_actions["actions"]) >= 10:
934 del saved_actions["actions"][min(saved_actions["actions"])]
935 key = saved_actions["next"]
936 saved_actions["actions"][key] = the_action
937 saved_actions["next"] = key + 1
940 @openerpweb.jsonrequest
941 def get_session_action(self, req, key):
943 Gets back a previously saved action. This method can return None if the action
944 was saved since too much time (this case should be handled in a smart way).
946 :param key: The key given by save_session_action()
948 :return: The saved action or None.
951 saved_actions = req.httpsession.get('saved_actions')
952 if not saved_actions:
954 return saved_actions["actions"].get(key)
956 @openerpweb.jsonrequest
957 def check(self, req):
958 req.session.assert_valid()
961 @openerpweb.jsonrequest
962 def destroy(self, req):
963 req.session._suicide = True
965 class Menu(openerpweb.Controller):
966 _cp_path = "/web/menu"
968 @openerpweb.jsonrequest
970 return {'data': self.do_load(req)}
972 @openerpweb.jsonrequest
973 def get_user_roots(self, req):
974 return self.do_get_user_roots(req)
976 def do_get_user_roots(self, req):
977 """ Return all root menu ids visible for the session user.
979 :param req: A request object, with an OpenERP session attribute
980 :type req: < session -> OpenERPSession >
981 :return: the root menu ids
985 context = s.eval_context(req.context)
986 Menus = s.model('ir.ui.menu')
987 # If a menu action is defined use its domain to get the root menu items
988 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
990 menu_domain = [('parent_id', '=', False)]
992 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
994 menu_domain = ast.literal_eval(domain_string)
996 return Menus.search(menu_domain, 0, False, False, context)
998 def do_load(self, req):
999 """ Loads all menu items (all applications and their sub-menus).
1001 :param req: A request object, with an OpenERP session attribute
1002 :type req: < session -> OpenERPSession >
1003 :return: the menu root
1004 :rtype: dict('children': menu_nodes)
1006 context = req.session.eval_context(req.context)
1007 Menus = req.session.model('ir.ui.menu')
1009 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1010 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1012 # menus are loaded fully unlike a regular tree view, cause there are a
1013 # limited number of items (752 when all 6.1 addons are installed)
1014 menu_ids = Menus.search([], 0, False, False, context)
1015 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1016 # adds roots at the end of the sequence, so that they will overwrite
1017 # equivalent menu items from full menu read when put into id:item
1018 # mapping, resulting in children being correctly set on the roots.
1019 menu_items.extend(menu_roots)
1021 # make a tree using parent_id
1022 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1023 for menu_item in menu_items:
1024 if menu_item['parent_id']:
1025 parent = menu_item['parent_id'][0]
1028 if parent in menu_items_map:
1029 menu_items_map[parent].setdefault(
1030 'children', []).append(menu_item)
1032 # sort by sequence a tree using parent_id
1033 for menu_item in menu_items:
1034 menu_item.setdefault('children', []).sort(
1035 key=operator.itemgetter('sequence'))
1039 @openerpweb.jsonrequest
1040 def action(self, req, menu_id):
1041 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1042 [('ir.ui.menu', menu_id)], False)
1043 return {"action": actions}
1045 class DataSet(openerpweb.Controller):
1046 _cp_path = "/web/dataset"
1048 @openerpweb.jsonrequest
1049 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1050 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1051 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1053 """ Performs a search() followed by a read() (if needed) using the
1054 provided search criteria
1056 :param req: a JSON-RPC request object
1057 :type req: openerpweb.JsonRequest
1058 :param str model: the name of the model to search on
1059 :param fields: a list of the fields to return in the result records
1061 :param int offset: from which index should the results start being returned
1062 :param int limit: the maximum number of records to return
1063 :param list domain: the search domain for the query
1064 :param list sort: sorting directives
1065 :returns: A structure (dict) with two keys: ids (all the ids matching
1066 the (domain, context) pair) and records (paginated records
1067 matching fields selection set)
1070 Model = req.session.model(model)
1072 context, domain = eval_context_and_domain(
1073 req.session, req.context, domain)
1075 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1076 if limit and len(ids) == limit:
1077 length = Model.search_count(domain, context)
1079 length = len(ids) + (offset or 0)
1080 if fields and fields == ['id']:
1081 # shortcut read if we only want the ids
1084 'records': [{'id': id} for id in ids]
1087 records = Model.read(ids, fields or False, context)
1088 records.sort(key=lambda obj: ids.index(obj['id']))
1094 @openerpweb.jsonrequest
1095 def load(self, req, model, id, fields):
1096 m = req.session.model(model)
1098 r = m.read([id], False, req.session.eval_context(req.context))
1101 return {'value': value}
1103 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1104 has_domain = domain_id is not None and domain_id < len(args)
1105 has_context = context_id is not None and context_id < len(args)
1107 domain = args[domain_id] if has_domain else []
1108 context = args[context_id] if has_context else {}
1109 c, d = eval_context_and_domain(req.session, context, domain)
1113 args[context_id] = c
1115 return self._call_kw(req, model, method, args, {})
1117 def _call_kw(self, req, model, method, args, kwargs):
1118 for i in xrange(len(args)):
1119 if isinstance(args[i], common.nonliterals.BaseContext):
1120 args[i] = req.session.eval_context(args[i])
1121 elif isinstance(args[i], common.nonliterals.BaseDomain):
1122 args[i] = req.session.eval_domain(args[i])
1123 for k in kwargs.keys():
1124 if isinstance(kwargs[k], common.nonliterals.BaseContext):
1125 kwargs[k] = req.session.eval_context(kwargs[k])
1126 elif isinstance(kwargs[k], common.nonliterals.BaseDomain):
1127 kwargs[k] = req.session.eval_domain(kwargs[k])
1129 # Temporary implements future display_name special field for model#read()
1130 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1131 if 'display_name' in args[1]:
1132 names = req.session.model(model).name_get(args[0], **kwargs)
1133 args[1].remove('display_name')
1134 r = getattr(req.session.model(model), method)(*args, **kwargs)
1135 for i in range(len(r)):
1136 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1139 return getattr(req.session.model(model), method)(*args, **kwargs)
1141 @openerpweb.jsonrequest
1142 def onchange(self, req, model, method, args, context_id=None):
1143 """ Support method for handling onchange calls: behaves much like call
1144 with the following differences:
1146 * Does not take a domain_id
1147 * Is aware of the return value's structure, and will parse the domains
1148 if needed in order to return either parsed literal domains (in JSON)
1149 or non-literal domain instances, allowing those domains to be used
1153 :type req: web.common.http.JsonRequest
1154 :param str model: object type on which to call the method
1155 :param str method: name of the onchange handler method
1156 :param list args: arguments to call the onchange handler with
1157 :param int context_id: index of the context object in the list of
1159 :return: result of the onchange call with all domains parsed
1161 result = self.call_common(req, model, method, args, context_id=context_id)
1162 if not result or 'domain' not in result:
1165 result['domain'] = dict(
1166 (k, parse_domain(v, req.session))
1167 for k, v in result['domain'].iteritems())
1171 @openerpweb.jsonrequest
1172 def call(self, req, model, method, args, domain_id=None, context_id=None):
1173 return self.call_common(req, model, method, args, domain_id, context_id)
1175 @openerpweb.jsonrequest
1176 def call_kw(self, req, model, method, args, kwargs):
1177 return self._call_kw(req, model, method, args, kwargs)
1179 @openerpweb.jsonrequest
1180 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1181 action = self.call_common(req, model, method, args, domain_id, context_id)
1182 if isinstance(action, dict) and action.get('type') != '':
1183 return {'result': clean_action(req, action)}
1184 return {'result': False}
1186 @openerpweb.jsonrequest
1187 def exec_workflow(self, req, model, id, signal):
1188 return req.session.exec_workflow(model, id, signal)
1190 @openerpweb.jsonrequest
1191 def resequence(self, req, model, ids, field='sequence', offset=0):
1192 """ Re-sequences a number of records in the model, by their ids
1194 The re-sequencing starts at the first model of ``ids``, the sequence
1195 number is incremented by one after each record and starts at ``offset``
1197 :param ids: identifiers of the records to resequence, in the new sequence order
1199 :param str field: field used for sequence specification, defaults to
1201 :param int offset: sequence number for first record in ``ids``, allows
1202 starting the resequencing from an arbitrary number,
1205 m = req.session.model(model)
1206 if not m.fields_get([field]):
1208 # python 2.6 has no start parameter
1209 for i, id in enumerate(ids):
1210 m.write(id, { field: i + offset })
1213 class DataGroup(openerpweb.Controller):
1214 _cp_path = "/web/group"
1215 @openerpweb.jsonrequest
1216 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1217 Model = req.session.model(model)
1218 context, domain = eval_context_and_domain(req.session, req.context, domain)
1220 return Model.read_group(
1221 domain or [], fields, group_by_fields, 0, False,
1222 dict(context, group_by=group_by_fields), sort or False)
1224 class View(openerpweb.Controller):
1225 _cp_path = "/web/view"
1227 def fields_view_get(self, req, model, view_id, view_type,
1228 transform=True, toolbar=False, submenu=False):
1229 Model = req.session.model(model)
1230 context = req.session.eval_context(req.context)
1231 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1232 # todo fme?: check that we should pass the evaluated context here
1233 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1234 if toolbar and transform:
1235 self.process_toolbar(req, fvg['toolbar'])
1238 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1239 # depending on how it feels, xmlrpclib.ServerProxy can translate
1240 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1241 # enjoy unicode strings which can not be trivially converted to
1242 # strings, and it blows up during parsing.
1244 # So ensure we fix this retardation by converting view xml back to
1246 if isinstance(fvg['arch'], unicode):
1247 arch = fvg['arch'].encode('utf-8')
1250 fvg['arch_string'] = arch
1253 evaluation_context = session.evaluation_context(context or {})
1254 xml = self.transform_view(arch, session, evaluation_context)
1256 xml = ElementTree.fromstring(arch)
1257 fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
1259 if 'id' in fvg['fields']:
1260 # Special case for id's
1261 id_field = fvg['fields']['id']
1262 id_field['original_type'] = id_field['type']
1263 id_field['type'] = 'id'
1265 for field in fvg['fields'].itervalues():
1266 if field.get('views'):
1267 for view in field["views"].itervalues():
1268 self.process_view(session, view, None, transform)
1269 if field.get('domain'):
1270 field["domain"] = parse_domain(field["domain"], session)
1271 if field.get('context'):
1272 field["context"] = parse_context(field["context"], session)
1274 def process_toolbar(self, req, toolbar):
1276 The toolbar is a mapping of section_key: [action_descriptor]
1278 We need to clean all those actions in order to ensure correct
1281 for actions in toolbar.itervalues():
1282 for action in actions:
1283 if 'context' in action:
1284 action['context'] = parse_context(
1285 action['context'], req.session)
1286 if 'domain' in action:
1287 action['domain'] = parse_domain(
1288 action['domain'], req.session)
1290 @openerpweb.jsonrequest
1291 def add_custom(self, req, view_id, arch):
1292 CustomView = req.session.model('ir.ui.view.custom')
1294 'user_id': req.session._uid,
1297 }, req.session.eval_context(req.context))
1298 return {'result': True}
1300 @openerpweb.jsonrequest
1301 def undo_custom(self, req, view_id, reset=False):
1302 CustomView = req.session.model('ir.ui.view.custom')
1303 context = req.session.eval_context(req.context)
1304 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1305 0, False, False, context)
1308 CustomView.unlink(vcustom, context)
1310 CustomView.unlink([vcustom[0]], context)
1311 return {'result': True}
1312 return {'result': False}
1314 def transform_view(self, view_string, session, context=None):
1315 # transform nodes on the fly via iterparse, instead of
1316 # doing it statically on the parsing result
1317 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1319 for event, elem in parser:
1320 if event == "start":
1323 self.parse_domains_and_contexts(elem, session)
1326 def parse_domains_and_contexts(self, elem, session):
1327 """ Converts domains and contexts from the view into Python objects,
1328 either literals if they can be parsed by literal_eval or a special
1329 placeholder object if the domain or context refers to free variables.
1331 :param elem: the current node being parsed
1332 :type param: xml.etree.ElementTree.Element
1333 :param session: OpenERP session object, used to store and retrieve
1335 :type session: openerpweb.openerpweb.OpenERPSession
1337 for el in ['domain', 'filter_domain']:
1338 domain = elem.get(el, '').strip()
1340 elem.set(el, parse_domain(domain, session))
1341 elem.set(el + '_string', domain)
1342 for el in ['context', 'default_get']:
1343 context_string = elem.get(el, '').strip()
1345 elem.set(el, parse_context(context_string, session))
1346 elem.set(el + '_string', context_string)
1348 @openerpweb.jsonrequest
1349 def load(self, req, model, view_id, view_type, toolbar=False):
1350 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1352 class TreeView(View):
1353 _cp_path = "/web/treeview"
1355 @openerpweb.jsonrequest
1356 def action(self, req, model, id):
1357 return load_actions_from_ir_values(
1358 req,'action', 'tree_but_open',[(model, id)],
1361 class SearchView(View):
1362 _cp_path = "/web/searchview"
1364 @openerpweb.jsonrequest
1365 def load(self, req, model, view_id):
1366 fields_view = self.fields_view_get(req, model, view_id, 'search')
1367 return {'fields_view': fields_view}
1369 @openerpweb.jsonrequest
1370 def fields_get(self, req, model):
1371 Model = req.session.model(model)
1372 fields = Model.fields_get(False, req.session.eval_context(req.context))
1373 for field in fields.values():
1374 # shouldn't convert the views too?
1375 if field.get('domain'):
1376 field["domain"] = parse_domain(field["domain"], req.session)
1377 if field.get('context'):
1378 field["context"] = parse_context(field["context"], req.session)
1379 return {'fields': fields}
1381 @openerpweb.jsonrequest
1382 def get_filters(self, req, model):
1383 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1384 Model = req.session.model("ir.filters")
1385 filters = Model.get_filters(model)
1386 for filter in filters:
1388 parsed_context = parse_context(filter["context"], req.session)
1389 filter["context"] = (parsed_context
1390 if not isinstance(parsed_context, common.nonliterals.BaseContext)
1391 else req.session.eval_context(parsed_context))
1393 parsed_domain = parse_domain(filter["domain"], req.session)
1394 filter["domain"] = (parsed_domain
1395 if not isinstance(parsed_domain, common.nonliterals.BaseDomain)
1396 else req.session.eval_domain(parsed_domain))
1398 logger.exception("Failed to parse custom filter %s in %s",
1399 filter['name'], model)
1400 filter['disabled'] = True
1401 del filter['context']
1402 del filter['domain']
1405 class Binary(openerpweb.Controller):
1406 _cp_path = "/web/binary"
1408 @openerpweb.httprequest
1409 def image(self, req, model, id, field, **kw):
1410 last_update = '__last_update'
1411 Model = req.session.model(model)
1412 context = req.session.eval_context(req.context)
1413 headers = [('Content-Type', 'image/png')]
1414 etag = req.httprequest.headers.get('If-None-Match')
1415 hashed_session = hashlib.md5(req.session_id).hexdigest()
1416 id = None if not id else simplejson.loads(id)
1417 if type(id) is list:
1420 if not id and hashed_session == etag:
1421 return werkzeug.wrappers.Response(status=304)
1423 date = Model.read([id], [last_update], context)[0].get(last_update)
1424 if hashlib.md5(date).hexdigest() == etag:
1425 return werkzeug.wrappers.Response(status=304)
1427 retag = hashed_session
1430 res = Model.default_get([field], context).get(field)
1431 image_data = base64.b64decode(res)
1433 res = Model.read([id], [last_update, field], context)[0]
1434 retag = hashlib.md5(res.get(last_update)).hexdigest()
1435 image_data = base64.b64decode(res.get(field))
1436 except (TypeError, xmlrpclib.Fault):
1437 image_data = self.placeholder(req)
1438 headers.append(('ETag', retag))
1439 headers.append(('Content-Length', len(image_data)))
1441 ncache = int(kw.get('cache'))
1442 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1445 return req.make_response(image_data, headers)
1446 def placeholder(self, req):
1447 addons_path = openerpweb.addons_manifest['web']['addons_path']
1448 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1449 def content_disposition(self, filename, req):
1450 filename = filename.encode('utf8')
1451 escaped = urllib2.quote(filename)
1452 browser = req.httprequest.user_agent.browser
1453 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
1454 if browser == 'msie' and version < 9:
1455 return "attachment; filename=%s" % escaped
1456 elif browser == 'safari':
1457 return "attachment; filename=%s" % filename
1459 return "attachment; filename*=UTF-8''%s" % escaped
1461 @openerpweb.httprequest
1462 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1463 """ Download link for files stored as binary fields.
1465 If the ``id`` parameter is omitted, fetches the default value for the
1466 binary field (via ``default_get``), otherwise fetches the field for
1467 that precise record.
1469 :param req: OpenERP request
1470 :type req: :class:`web.common.http.HttpRequest`
1471 :param str model: name of the model to fetch the binary from
1472 :param str field: binary field
1473 :param str id: id of the record from which to fetch the binary
1474 :param str filename_field: field holding the file's name, if any
1475 :returns: :class:`werkzeug.wrappers.Response`
1477 Model = req.session.model(model)
1478 context = req.session.eval_context(req.context)
1481 fields.append(filename_field)
1483 res = Model.read([int(id)], fields, context)[0]
1485 res = Model.default_get(fields, context)
1486 filecontent = base64.b64decode(res.get(field, ''))
1488 return req.not_found()
1490 filename = '%s_%s' % (model.replace('.', '_'), id)
1492 filename = res.get(filename_field, '') or filename
1493 return req.make_response(filecontent,
1494 [('Content-Type', 'application/octet-stream'),
1495 ('Content-Disposition', self.content_disposition(filename, req))])
1497 @openerpweb.httprequest
1498 def saveas_ajax(self, req, data, token):
1499 jdata = simplejson.loads(data)
1500 model = jdata['model']
1501 field = jdata['field']
1502 id = jdata.get('id', None)
1503 filename_field = jdata.get('filename_field', None)
1504 context = jdata.get('context', dict())
1506 context = req.session.eval_context(context)
1507 Model = req.session.model(model)
1510 fields.append(filename_field)
1512 res = Model.read([int(id)], fields, context)[0]
1514 res = Model.default_get(fields, context)
1515 filecontent = base64.b64decode(res.get(field, ''))
1517 raise ValueError("No content found for field '%s' on '%s:%s'" %
1520 filename = '%s_%s' % (model.replace('.', '_'), id)
1522 filename = res.get(filename_field, '') or filename
1523 return req.make_response(filecontent,
1524 headers=[('Content-Type', 'application/octet-stream'),
1525 ('Content-Disposition', self.content_disposition(filename, req))],
1526 cookies={'fileToken': int(token)})
1528 @openerpweb.httprequest
1529 def upload(self, req, callback, ufile):
1530 # TODO: might be useful to have a configuration flag for max-length file uploads
1532 out = """<script language="javascript" type="text/javascript">
1533 var win = window.top.window;
1534 win.jQuery(win).trigger(%s, %s);
1537 args = [len(data), ufile.filename,
1538 ufile.content_type, base64.b64encode(data)]
1539 except Exception, e:
1540 args = [False, e.message]
1541 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1543 @openerpweb.httprequest
1544 def upload_attachment(self, req, callback, model, id, ufile):
1545 context = req.session.eval_context(req.context)
1546 Model = req.session.model('ir.attachment')
1548 out = """<script language="javascript" type="text/javascript">
1549 var win = window.top.window;
1550 win.jQuery(win).trigger(%s, %s);
1552 attachment_id = Model.create({
1553 'name': ufile.filename,
1554 'datas': base64.encodestring(ufile.read()),
1555 'datas_fname': ufile.filename,
1560 'filename': ufile.filename,
1563 except Exception, e:
1564 args = { 'error': e.message }
1565 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1567 class Action(openerpweb.Controller):
1568 _cp_path = "/web/action"
1570 # For most actions, the type attribute and the model name are the same, but
1571 # there are exceptions. This dict is used to remap action type attributes
1572 # to the "real" model name when they differ.
1574 "ir.actions.act_url": "ir.actions.url",
1577 @openerpweb.jsonrequest
1578 def load(self, req, action_id, do_not_eval=False):
1579 Actions = req.session.model('ir.actions.actions')
1581 context = req.session.eval_context(req.context)
1584 action_id = int(action_id)
1587 module, xmlid = action_id.split('.', 1)
1588 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1589 assert model.startswith('ir.actions.')
1591 action_id = 0 # force failed read
1593 base_action = Actions.read([action_id], ['type'], context)
1596 action_type = base_action[0]['type']
1597 if action_type == 'ir.actions.report.xml':
1598 ctx.update({'bin_size': True})
1600 action_model = self.action_mapping.get(action_type, action_type)
1601 action = req.session.model(action_model).read([action_id], False, ctx)
1603 value = clean_action(req, action[0], do_not_eval)
1604 return {'result': value}
1606 @openerpweb.jsonrequest
1607 def run(self, req, action_id):
1608 return_action = req.session.model('ir.actions.server').run(
1609 [action_id], req.session.eval_context(req.context))
1611 return clean_action(req, return_action)
1616 _cp_path = "/web/export"
1618 @openerpweb.jsonrequest
1619 def formats(self, req):
1620 """ Returns all valid export formats
1622 :returns: for each export format, a pair of identifier and printable name
1623 :rtype: [(str, str)]
1627 for path, controller in openerpweb.controllers_path.iteritems()
1628 if path.startswith(self._cp_path)
1629 if hasattr(controller, 'fmt')
1630 ], key=operator.itemgetter("label"))
1632 def fields_get(self, req, model):
1633 Model = req.session.model(model)
1634 fields = Model.fields_get(False, req.session.eval_context(req.context))
1637 @openerpweb.jsonrequest
1638 def get_fields(self, req, model, prefix='', parent_name= '',
1639 import_compat=True, parent_field_type=None,
1642 if import_compat and parent_field_type == "many2one":
1645 fields = self.fields_get(req, model)
1648 fields.pop('id', None)
1650 fields['.id'] = fields.pop('id', {'string': 'ID'})
1652 fields_sequence = sorted(fields.iteritems(),
1653 key=lambda field: field[1].get('string', ''))
1656 for field_name, field in fields_sequence:
1658 if exclude and field_name in exclude:
1660 if field.get('readonly'):
1661 # If none of the field's states unsets readonly, skip the field
1662 if all(dict(attrs).get('readonly', True)
1663 for attrs in field.get('states', {}).values()):
1666 id = prefix + (prefix and '/'or '') + field_name
1667 name = parent_name + (parent_name and '/' or '') + field['string']
1668 record = {'id': id, 'string': name,
1669 'value': id, 'children': False,
1670 'field_type': field.get('type'),
1671 'required': field.get('required'),
1672 'relation_field': field.get('relation_field')}
1673 records.append(record)
1675 if len(name.split('/')) < 3 and 'relation' in field:
1676 ref = field.pop('relation')
1677 record['value'] += '/id'
1678 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1680 if not import_compat or field['type'] == 'one2many':
1681 # m2m field in import_compat is childless
1682 record['children'] = True
1686 @openerpweb.jsonrequest
1687 def namelist(self,req, model, export_id):
1688 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1689 export = req.session.model("ir.exports").read([export_id])[0]
1690 export_fields_list = req.session.model("ir.exports.line").read(
1691 export['export_fields'])
1693 fields_data = self.fields_info(
1694 req, model, map(operator.itemgetter('name'), export_fields_list))
1697 {'name': field['name'], 'label': fields_data[field['name']]}
1698 for field in export_fields_list
1701 def fields_info(self, req, model, export_fields):
1703 fields = self.fields_get(req, model)
1705 # To make fields retrieval more efficient, fetch all sub-fields of a
1706 # given field at the same time. Because the order in the export list is
1707 # arbitrary, this requires ordering all sub-fields of a given field
1708 # together so they can be fetched at the same time
1710 # Works the following way:
1711 # * sort the list of fields to export, the default sorting order will
1712 # put the field itself (if present, for xmlid) and all of its
1713 # sub-fields right after it
1714 # * then, group on: the first field of the path (which is the same for
1715 # a field and for its subfields and the length of splitting on the
1716 # first '/', which basically means grouping the field on one side and
1717 # all of the subfields on the other. This way, we have the field (for
1718 # the xmlid) with length 1, and all of the subfields with the same
1719 # base but a length "flag" of 2
1720 # * if we have a normal field (length 1), just add it to the info
1721 # mapping (with its string) as-is
1722 # * otherwise, recursively call fields_info via graft_subfields.
1723 # all graft_subfields does is take the result of fields_info (on the
1724 # field's model) and prepend the current base (current field), which
1725 # rebuilds the whole sub-tree for the field
1727 # result: because we're not fetching the fields_get for half the
1728 # database models, fetching a namelist with a dozen fields (including
1729 # relational data) falls from ~6s to ~300ms (on the leads model).
1730 # export lists with no sub-fields (e.g. import_compatible lists with
1731 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1732 # there's a single fields_get to execute)
1733 for (base, length), subfields in itertools.groupby(
1734 sorted(export_fields),
1735 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1736 subfields = list(subfields)
1738 # subfields is a seq of $base/*rest, and not loaded yet
1739 info.update(self.graft_subfields(
1740 req, fields[base]['relation'], base, fields[base]['string'],
1744 info[base] = fields[base]['string']
1748 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1749 export_fields = [field.split('/', 1)[1] for field in fields]
1751 (prefix + '/' + k, prefix_string + '/' + v)
1752 for k, v in self.fields_info(req, model, export_fields).iteritems())
1754 #noinspection PyPropertyDefinition
1756 def content_type(self):
1757 """ Provides the format's content type """
1758 raise NotImplementedError()
1760 def filename(self, base):
1761 """ Creates a valid filename for the format (with extension) from the
1762 provided base name (exension-less)
1764 raise NotImplementedError()
1766 def from_data(self, fields, rows):
1767 """ Conversion method from OpenERP's export data to whatever the
1768 current export class outputs
1770 :params list fields: a list of fields to export
1771 :params list rows: a list of records to export
1775 raise NotImplementedError()
1777 @openerpweb.httprequest
1778 def index(self, req, data, token):
1779 model, fields, ids, domain, import_compat = \
1780 operator.itemgetter('model', 'fields', 'ids', 'domain',
1782 simplejson.loads(data))
1784 context = req.session.eval_context(req.context)
1785 Model = req.session.model(model)
1786 ids = ids or Model.search(domain, 0, False, False, context)
1788 field_names = map(operator.itemgetter('name'), fields)
1789 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1792 columns_headers = field_names
1794 columns_headers = [val['label'].strip() for val in fields]
1797 return req.make_response(self.from_data(columns_headers, import_data),
1798 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1799 ('Content-Type', self.content_type)],
1800 cookies={'fileToken': int(token)})
1802 class CSVExport(Export):
1803 _cp_path = '/web/export/csv'
1804 fmt = {'tag': 'csv', 'label': 'CSV'}
1807 def content_type(self):
1808 return 'text/csv;charset=utf8'
1810 def filename(self, base):
1811 return base + '.csv'
1813 def from_data(self, fields, rows):
1815 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1817 writer.writerow([name.encode('utf-8') for name in fields])
1822 if isinstance(d, basestring):
1823 d = d.replace('\n',' ').replace('\t',' ')
1825 d = d.encode('utf-8')
1826 except UnicodeError:
1828 if d is False: d = None
1830 writer.writerow(row)
1837 class ExcelExport(Export):
1838 _cp_path = '/web/export/xls'
1842 'error': None if xlwt else "XLWT required"
1846 def content_type(self):
1847 return 'application/vnd.ms-excel'
1849 def filename(self, base):
1850 return base + '.xls'
1852 def from_data(self, fields, rows):
1853 workbook = xlwt.Workbook()
1854 worksheet = workbook.add_sheet('Sheet 1')
1856 for i, fieldname in enumerate(fields):
1857 worksheet.write(0, i, fieldname)
1858 worksheet.col(i).width = 8000 # around 220 pixels
1860 style = xlwt.easyxf('align: wrap yes')
1862 for row_index, row in enumerate(rows):
1863 for cell_index, cell_value in enumerate(row):
1864 if isinstance(cell_value, basestring):
1865 cell_value = re.sub("\r", " ", cell_value)
1866 if cell_value is False: cell_value = None
1867 worksheet.write(row_index + 1, cell_index, cell_value, style)
1876 class Reports(View):
1877 _cp_path = "/web/report"
1878 POLLING_DELAY = 0.25
1880 'doc': 'application/vnd.ms-word',
1881 'html': 'text/html',
1882 'odt': 'application/vnd.oasis.opendocument.text',
1883 'pdf': 'application/pdf',
1884 'sxw': 'application/vnd.sun.xml.writer',
1885 'xls': 'application/vnd.ms-excel',
1888 @openerpweb.httprequest
1889 def index(self, req, action, token):
1890 action = simplejson.loads(action)
1892 report_srv = req.session.proxy("report")
1893 context = req.session.eval_context(
1894 common.nonliterals.CompoundContext(
1895 req.context or {}, action[ "context"]))
1898 report_ids = context["active_ids"]
1899 if 'report_type' in action:
1900 report_data['report_type'] = action['report_type']
1901 if 'datas' in action:
1902 if 'ids' in action['datas']:
1903 report_ids = action['datas'].pop('ids')
1904 report_data.update(action['datas'])
1906 report_id = report_srv.report(
1907 req.session._db, req.session._uid, req.session._password,
1908 action["report_name"], report_ids,
1909 report_data, context)
1911 report_struct = None
1913 report_struct = report_srv.report_get(
1914 req.session._db, req.session._uid, req.session._password, report_id)
1915 if report_struct["state"]:
1918 time.sleep(self.POLLING_DELAY)
1920 report = base64.b64decode(report_struct['result'])
1921 if report_struct.get('code') == 'zlib':
1922 report = zlib.decompress(report)
1923 report_mimetype = self.TYPES_MAPPING.get(
1924 report_struct['format'], 'octet-stream')
1925 file_name = action.get('name', None)
1926 if 'name' not in action:
1927 reports = req.session.model('ir.actions.report.xml')
1928 res_id = reports.search([('report_name', '=', action['report_name']),],
1929 0, False, False, context)
1931 file_name = reports.read(res_id[0], ['name'], context)['name']
1933 file_name = action['report_name']
1935 return req.make_response(report,
1937 # maybe we should take of what characters can appear in a file name?
1938 ('Content-Disposition', 'attachment; filename="%s.%s"' % (file_name, report_struct['format'])),
1939 ('Content-Type', report_mimetype),
1940 ('Content-Length', len(report))],
1941 cookies={'fileToken': int(token)})
1944 _cp_path = "/web/import"
1946 def fields_get(self, req, model):
1947 Model = req.session.model(model)
1948 fields = Model.fields_get(False, req.session.eval_context(req.context))
1951 @openerpweb.httprequest
1952 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1954 data = list(csv.reader(
1955 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1956 except csv.Error, e:
1958 return '<script>window.top.%s(%s);</script>' % (
1959 jsonp, simplejson.dumps({'error': {
1960 'message': 'Error parsing CSV file: %s' % e,
1961 # decodes each byte to a unicode character, which may or
1962 # may not be printable, but decoding will succeed.
1963 # Otherwise simplejson will try to decode the `str` using
1964 # utf-8, which is very likely to blow up on characters out
1965 # of the ascii range (in range [128, 256))
1966 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1969 return '<script>window.top.%s(%s);</script>' % (
1970 jsonp, simplejson.dumps(
1971 {'records': data[:10]}, encoding=csvcode))
1972 except UnicodeDecodeError:
1973 return '<script>window.top.%s(%s);</script>' % (
1974 jsonp, simplejson.dumps({
1975 'message': u"Failed to decode CSV file using encoding %s, "
1976 u"try switching to a different encoding" % csvcode
1979 @openerpweb.httprequest
1980 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1982 modle_obj = req.session.model(model)
1983 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1984 simplejson.loads(meta))
1987 if not (csvdel and len(csvdel) == 1):
1988 error = u"The CSV delimiter must be a single character"
1990 if not indices and fields:
1991 error = u"You must select at least one field to import"
1994 return '<script>window.top.%s(%s);</script>' % (
1995 jsonp, simplejson.dumps({'error': {'message': error}}))
1997 # skip ignored records (@skip parameter)
1998 # then skip empty lines (not valid csv)
1999 # nb: should these operations be reverted?
2000 rows_to_import = itertools.ifilter(
2003 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
2006 # if only one index, itemgetter will return an atom rather than a tuple
2007 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
2008 else: mapper = operator.itemgetter(*indices)
2013 # decode each data row
2015 [record.decode(csvcode) for record in row]
2016 for row in itertools.imap(mapper, rows_to_import)
2017 # don't insert completely empty rows (can happen due to fields
2018 # filtering in case of e.g. o2m content rows)
2021 except UnicodeDecodeError:
2022 error = u"Failed to decode CSV file using encoding %s" % csvcode
2023 except csv.Error, e:
2024 error = u"Could not process CSV file: %s" % e
2026 # If the file contains nothing,
2028 error = u"File to import is empty"
2030 return '<script>window.top.%s(%s);</script>' % (
2031 jsonp, simplejson.dumps({'error': {'message': error}}))
2034 (code, record, message, _nope) = modle_obj.import_data(
2035 fields, data, 'init', '', False,
2036 req.session.eval_context(req.context))
2037 except xmlrpclib.Fault, e:
2038 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
2039 return '<script>window.top.%s(%s);</script>' % (
2040 jsonp, simplejson.dumps({'error':error}))
2043 return '<script>window.top.%s(%s);</script>' % (
2044 jsonp, simplejson.dumps({'success':True}))
2046 msg = u"Error during import: %s\n\nTrying to import record %r" % (
2048 return '<script>window.top.%s(%s);</script>' % (
2049 jsonp, simplejson.dumps({'error': {'message':msg}}))
2051 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: