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):
699 return req.session.proxy('common').version()['openerp']
701 class Proxy(openerpweb.Controller):
702 _cp_path = '/web/proxy'
704 @openerpweb.jsonrequest
705 def load(self, req, path):
706 """ Proxies an HTTP request through a JSON request.
708 It is strongly recommended to not request binary files through this,
709 as the result will be a binary data blob as well.
711 :param req: OpenERP request
712 :param path: actual request path
713 :return: file content
715 from werkzeug.test import Client
716 from werkzeug.wrappers import BaseResponse
718 return Client(req.httprequest.app, BaseResponse).get(path).data
720 class Database(openerpweb.Controller):
721 _cp_path = "/web/database"
723 @openerpweb.jsonrequest
724 def get_list(self, req):
726 return {"db_list": dbs}
728 @openerpweb.jsonrequest
729 def create(self, req, fields):
730 params = dict(map(operator.itemgetter('name', 'value'), fields))
732 params['super_admin_pwd'],
734 bool(params.get('demo_data')),
736 params['create_admin_pwd']
739 return req.session.proxy("db").create_database(*create_attrs)
741 @openerpweb.jsonrequest
742 def drop(self, req, fields):
743 password, db = operator.itemgetter(
744 'drop_pwd', 'drop_db')(
745 dict(map(operator.itemgetter('name', 'value'), fields)))
748 return req.session.proxy("db").drop(password, db)
749 except xmlrpclib.Fault, e:
750 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
751 return {'error': e.faultCode, 'title': 'Drop Database'}
752 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
754 @openerpweb.httprequest
755 def backup(self, req, backup_db, backup_pwd, token):
757 db_dump = base64.b64decode(
758 req.session.proxy("db").dump(backup_pwd, backup_db))
759 filename = "%(db)s_%(timestamp)s.dump" % {
761 'timestamp': datetime.datetime.utcnow().strftime(
762 "%Y-%m-%d_%H-%M-%SZ")
764 return req.make_response(db_dump,
765 [('Content-Type', 'application/octet-stream; charset=binary'),
766 ('Content-Disposition', 'attachment; filename="' + filename + '"')],
767 {'fileToken': int(token)}
769 except xmlrpclib.Fault, e:
770 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
772 @openerpweb.httprequest
773 def restore(self, req, db_file, restore_pwd, new_db):
775 data = base64.b64encode(db_file.read())
776 req.session.proxy("db").restore(restore_pwd, new_db, data)
778 except xmlrpclib.Fault, e:
779 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
780 raise Exception("AccessDenied")
782 @openerpweb.jsonrequest
783 def change_password(self, req, fields):
784 old_password, new_password = operator.itemgetter(
785 'old_pwd', 'new_pwd')(
786 dict(map(operator.itemgetter('name', 'value'), fields)))
788 return req.session.proxy("db").change_admin_password(old_password, new_password)
789 except xmlrpclib.Fault, e:
790 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
791 return {'error': e.faultCode, 'title': 'Change Password'}
792 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
794 class Session(openerpweb.Controller):
795 _cp_path = "/web/session"
797 def session_info(self, req):
798 req.session.ensure_valid()
800 "session_id": req.session_id,
801 "uid": req.session._uid,
802 "context": req.session.get_context() if req.session._uid else {},
803 "db": req.session._db,
804 "login": req.session._login,
807 @openerpweb.jsonrequest
808 def get_session_info(self, req):
809 return self.session_info(req)
811 @openerpweb.jsonrequest
812 def authenticate(self, req, db, login, password, base_location=None):
813 wsgienv = req.httprequest.environ
814 release = common.release
816 base_location=base_location,
817 HTTP_HOST=wsgienv['HTTP_HOST'],
818 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
819 user_agent="%s / %s" % (release.name, release.version),
821 req.session.authenticate(db, login, password, env)
823 return self.session_info(req)
825 @openerpweb.jsonrequest
826 def change_password (self,req,fields):
827 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
828 dict(map(operator.itemgetter('name', 'value'), fields)))
829 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
830 return {'error':'All passwords have to be filled.','title': 'Change Password'}
831 if new_password != confirm_password:
832 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
834 if req.session.model('res.users').change_password(
835 old_password, new_password):
836 return {'new_password':new_password}
838 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
839 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
841 @openerpweb.jsonrequest
842 def sc_list(self, req):
843 return req.session.model('ir.ui.view_sc').get_sc(
844 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
846 @openerpweb.jsonrequest
847 def get_lang_list(self, req):
850 'lang_list': (req.session.proxy("db").list_lang() or []),
854 return {"error": e, "title": "Languages"}
856 @openerpweb.jsonrequest
857 def modules(self, req):
858 # return all installed modules. Web client is smart enough to not load a module twice
859 return module_installed(req)
861 @openerpweb.jsonrequest
862 def eval_domain_and_context(self, req, contexts, domains,
864 """ Evaluates sequences of domains and contexts, composing them into
865 a single context, domain or group_by sequence.
867 :param list contexts: list of contexts to merge together. Contexts are
868 evaluated in sequence, all previous contexts
869 are part of their own evaluation context
870 (starting at the session context).
871 :param list domains: list of domains to merge together. Domains are
872 evaluated in sequence and appended to one another
873 (implicit AND), their evaluation domain is the
874 result of merging all contexts.
875 :param list group_by_seq: list of domains (which may be in a different
876 order than the ``contexts`` parameter),
877 evaluated in sequence, their ``'group_by'``
878 key is extracted if they have one.
883 the global context created by merging all of
887 the concatenation of all domains
890 a list of fields to group by, potentially empty (in which case
891 no group by should be performed)
893 context, domain = eval_context_and_domain(req.session,
894 common.nonliterals.CompoundContext(*(contexts or [])),
895 common.nonliterals.CompoundDomain(*(domains or [])))
897 group_by_sequence = []
898 for candidate in (group_by_seq or []):
899 ctx = req.session.eval_context(candidate, context)
900 group_by = ctx.get('group_by')
903 elif isinstance(group_by, basestring):
904 group_by_sequence.append(group_by)
906 group_by_sequence.extend(group_by)
911 'group_by': group_by_sequence
914 @openerpweb.jsonrequest
915 def save_session_action(self, req, the_action):
917 This method store an action object in the session object and returns an integer
918 identifying that action. The method get_session_action() can be used to get
921 :param the_action: The action to save in the session.
922 :type the_action: anything
923 :return: A key identifying the saved action.
926 saved_actions = req.httpsession.get('saved_actions')
927 if not saved_actions:
928 saved_actions = {"next":0, "actions":{}}
929 req.httpsession['saved_actions'] = saved_actions
930 # we don't allow more than 10 stored actions
931 if len(saved_actions["actions"]) >= 10:
932 del saved_actions["actions"][min(saved_actions["actions"])]
933 key = saved_actions["next"]
934 saved_actions["actions"][key] = the_action
935 saved_actions["next"] = key + 1
938 @openerpweb.jsonrequest
939 def get_session_action(self, req, key):
941 Gets back a previously saved action. This method can return None if the action
942 was saved since too much time (this case should be handled in a smart way).
944 :param key: The key given by save_session_action()
946 :return: The saved action or None.
949 saved_actions = req.httpsession.get('saved_actions')
950 if not saved_actions:
952 return saved_actions["actions"].get(key)
954 @openerpweb.jsonrequest
955 def check(self, req):
956 req.session.assert_valid()
959 @openerpweb.jsonrequest
960 def destroy(self, req):
961 req.session._suicide = True
963 class Menu(openerpweb.Controller):
964 _cp_path = "/web/menu"
966 @openerpweb.jsonrequest
968 return {'data': self.do_load(req)}
970 @openerpweb.jsonrequest
971 def get_user_roots(self, req):
972 return self.do_get_user_roots(req)
974 def do_get_user_roots(self, req):
975 """ Return all root menu ids visible for the session user.
977 :param req: A request object, with an OpenERP session attribute
978 :type req: < session -> OpenERPSession >
979 :return: the root menu ids
983 context = s.eval_context(req.context)
984 Menus = s.model('ir.ui.menu')
985 # If a menu action is defined use its domain to get the root menu items
986 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
988 menu_domain = [('parent_id', '=', False)]
990 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
992 menu_domain = ast.literal_eval(domain_string)
994 return Menus.search(menu_domain, 0, False, False, context)
996 def do_load(self, req):
997 """ Loads all menu items (all applications and their sub-menus).
999 :param req: A request object, with an OpenERP session attribute
1000 :type req: < session -> OpenERPSession >
1001 :return: the menu root
1002 :rtype: dict('children': menu_nodes)
1004 context = req.session.eval_context(req.context)
1005 Menus = req.session.model('ir.ui.menu')
1007 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1008 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1010 # menus are loaded fully unlike a regular tree view, cause there are a
1011 # limited number of items (752 when all 6.1 addons are installed)
1012 menu_ids = Menus.search([], 0, False, False, context)
1013 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1014 # adds roots at the end of the sequence, so that they will overwrite
1015 # equivalent menu items from full menu read when put into id:item
1016 # mapping, resulting in children being correctly set on the roots.
1017 menu_items.extend(menu_roots)
1019 # make a tree using parent_id
1020 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1021 for menu_item in menu_items:
1022 if menu_item['parent_id']:
1023 parent = menu_item['parent_id'][0]
1026 if parent in menu_items_map:
1027 menu_items_map[parent].setdefault(
1028 'children', []).append(menu_item)
1030 # sort by sequence a tree using parent_id
1031 for menu_item in menu_items:
1032 menu_item.setdefault('children', []).sort(
1033 key=operator.itemgetter('sequence'))
1037 @openerpweb.jsonrequest
1038 def action(self, req, menu_id):
1039 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1040 [('ir.ui.menu', menu_id)], False)
1041 return {"action": actions}
1043 class DataSet(openerpweb.Controller):
1044 _cp_path = "/web/dataset"
1046 @openerpweb.jsonrequest
1047 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1048 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1049 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1051 """ Performs a search() followed by a read() (if needed) using the
1052 provided search criteria
1054 :param req: a JSON-RPC request object
1055 :type req: openerpweb.JsonRequest
1056 :param str model: the name of the model to search on
1057 :param fields: a list of the fields to return in the result records
1059 :param int offset: from which index should the results start being returned
1060 :param int limit: the maximum number of records to return
1061 :param list domain: the search domain for the query
1062 :param list sort: sorting directives
1063 :returns: A structure (dict) with two keys: ids (all the ids matching
1064 the (domain, context) pair) and records (paginated records
1065 matching fields selection set)
1068 Model = req.session.model(model)
1070 context, domain = eval_context_and_domain(
1071 req.session, req.context, domain)
1073 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1074 if limit and len(ids) == limit:
1075 length = Model.search_count(domain, context)
1077 length = len(ids) + (offset or 0)
1078 if fields and fields == ['id']:
1079 # shortcut read if we only want the ids
1082 'records': [{'id': id} for id in ids]
1085 records = Model.read(ids, fields or False, context)
1086 records.sort(key=lambda obj: ids.index(obj['id']))
1092 @openerpweb.jsonrequest
1093 def load(self, req, model, id, fields):
1094 m = req.session.model(model)
1096 r = m.read([id], False, req.session.eval_context(req.context))
1099 return {'value': value}
1101 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1102 has_domain = domain_id is not None and domain_id < len(args)
1103 has_context = context_id is not None and context_id < len(args)
1105 domain = args[domain_id] if has_domain else []
1106 context = args[context_id] if has_context else {}
1107 c, d = eval_context_and_domain(req.session, context, domain)
1111 args[context_id] = c
1113 return self._call_kw(req, model, method, args, {})
1115 def _call_kw(self, req, model, method, args, kwargs):
1116 for i in xrange(len(args)):
1117 if isinstance(args[i], common.nonliterals.BaseContext):
1118 args[i] = req.session.eval_context(args[i])
1119 elif isinstance(args[i], common.nonliterals.BaseDomain):
1120 args[i] = req.session.eval_domain(args[i])
1121 for k in kwargs.keys():
1122 if isinstance(kwargs[k], common.nonliterals.BaseContext):
1123 kwargs[k] = req.session.eval_context(kwargs[k])
1124 elif isinstance(kwargs[k], common.nonliterals.BaseDomain):
1125 kwargs[k] = req.session.eval_domain(kwargs[k])
1127 # Temporary implements future display_name special field for model#read()
1128 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1129 if 'display_name' in args[1]:
1130 names = req.session.model(model).name_get(args[0], **kwargs)
1131 args[1].remove('display_name')
1132 r = getattr(req.session.model(model), method)(*args, **kwargs)
1133 for i in range(len(r)):
1134 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1137 return getattr(req.session.model(model), method)(*args, **kwargs)
1139 @openerpweb.jsonrequest
1140 def onchange(self, req, model, method, args, context_id=None):
1141 """ Support method for handling onchange calls: behaves much like call
1142 with the following differences:
1144 * Does not take a domain_id
1145 * Is aware of the return value's structure, and will parse the domains
1146 if needed in order to return either parsed literal domains (in JSON)
1147 or non-literal domain instances, allowing those domains to be used
1151 :type req: web.common.http.JsonRequest
1152 :param str model: object type on which to call the method
1153 :param str method: name of the onchange handler method
1154 :param list args: arguments to call the onchange handler with
1155 :param int context_id: index of the context object in the list of
1157 :return: result of the onchange call with all domains parsed
1159 result = self.call_common(req, model, method, args, context_id=context_id)
1160 if not result or 'domain' not in result:
1163 result['domain'] = dict(
1164 (k, parse_domain(v, req.session))
1165 for k, v in result['domain'].iteritems())
1169 @openerpweb.jsonrequest
1170 def call(self, req, model, method, args, domain_id=None, context_id=None):
1171 return self.call_common(req, model, method, args, domain_id, context_id)
1173 @openerpweb.jsonrequest
1174 def call_kw(self, req, model, method, args, kwargs):
1175 return self._call_kw(req, model, method, args, kwargs)
1177 @openerpweb.jsonrequest
1178 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1179 action = self.call_common(req, model, method, args, domain_id, context_id)
1180 if isinstance(action, dict) and action.get('type') != '':
1181 return {'result': clean_action(req, action)}
1182 return {'result': False}
1184 @openerpweb.jsonrequest
1185 def exec_workflow(self, req, model, id, signal):
1186 return req.session.exec_workflow(model, id, signal)
1188 @openerpweb.jsonrequest
1189 def resequence(self, req, model, ids, field='sequence', offset=0):
1190 """ Re-sequences a number of records in the model, by their ids
1192 The re-sequencing starts at the first model of ``ids``, the sequence
1193 number is incremented by one after each record and starts at ``offset``
1195 :param ids: identifiers of the records to resequence, in the new sequence order
1197 :param str field: field used for sequence specification, defaults to
1199 :param int offset: sequence number for first record in ``ids``, allows
1200 starting the resequencing from an arbitrary number,
1203 m = req.session.model(model)
1204 if not m.fields_get([field]):
1206 # python 2.6 has no start parameter
1207 for i, id in enumerate(ids):
1208 m.write(id, { field: i + offset })
1211 class DataGroup(openerpweb.Controller):
1212 _cp_path = "/web/group"
1213 @openerpweb.jsonrequest
1214 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1215 Model = req.session.model(model)
1216 context, domain = eval_context_and_domain(req.session, req.context, domain)
1218 return Model.read_group(
1219 domain or [], fields, group_by_fields, 0, False,
1220 dict(context, group_by=group_by_fields), sort or False)
1222 class View(openerpweb.Controller):
1223 _cp_path = "/web/view"
1225 def fields_view_get(self, req, model, view_id, view_type,
1226 transform=True, toolbar=False, submenu=False):
1227 Model = req.session.model(model)
1228 context = req.session.eval_context(req.context)
1229 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1230 # todo fme?: check that we should pass the evaluated context here
1231 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1232 if toolbar and transform:
1233 self.process_toolbar(req, fvg['toolbar'])
1236 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1237 # depending on how it feels, xmlrpclib.ServerProxy can translate
1238 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1239 # enjoy unicode strings which can not be trivially converted to
1240 # strings, and it blows up during parsing.
1242 # So ensure we fix this retardation by converting view xml back to
1244 if isinstance(fvg['arch'], unicode):
1245 arch = fvg['arch'].encode('utf-8')
1248 fvg['arch_string'] = arch
1251 evaluation_context = session.evaluation_context(context or {})
1252 xml = self.transform_view(arch, session, evaluation_context)
1254 xml = ElementTree.fromstring(arch)
1255 fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
1257 if 'id' in fvg['fields']:
1258 # Special case for id's
1259 id_field = fvg['fields']['id']
1260 id_field['original_type'] = id_field['type']
1261 id_field['type'] = 'id'
1263 for field in fvg['fields'].itervalues():
1264 if field.get('views'):
1265 for view in field["views"].itervalues():
1266 self.process_view(session, view, None, transform)
1267 if field.get('domain'):
1268 field["domain"] = parse_domain(field["domain"], session)
1269 if field.get('context'):
1270 field["context"] = parse_context(field["context"], session)
1272 def process_toolbar(self, req, toolbar):
1274 The toolbar is a mapping of section_key: [action_descriptor]
1276 We need to clean all those actions in order to ensure correct
1279 for actions in toolbar.itervalues():
1280 for action in actions:
1281 if 'context' in action:
1282 action['context'] = parse_context(
1283 action['context'], req.session)
1284 if 'domain' in action:
1285 action['domain'] = parse_domain(
1286 action['domain'], req.session)
1288 @openerpweb.jsonrequest
1289 def add_custom(self, req, view_id, arch):
1290 CustomView = req.session.model('ir.ui.view.custom')
1292 'user_id': req.session._uid,
1295 }, req.session.eval_context(req.context))
1296 return {'result': True}
1298 @openerpweb.jsonrequest
1299 def undo_custom(self, req, view_id, reset=False):
1300 CustomView = req.session.model('ir.ui.view.custom')
1301 context = req.session.eval_context(req.context)
1302 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1303 0, False, False, context)
1306 CustomView.unlink(vcustom, context)
1308 CustomView.unlink([vcustom[0]], context)
1309 return {'result': True}
1310 return {'result': False}
1312 def transform_view(self, view_string, session, context=None):
1313 # transform nodes on the fly via iterparse, instead of
1314 # doing it statically on the parsing result
1315 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1317 for event, elem in parser:
1318 if event == "start":
1321 self.parse_domains_and_contexts(elem, session)
1324 def parse_domains_and_contexts(self, elem, session):
1325 """ Converts domains and contexts from the view into Python objects,
1326 either literals if they can be parsed by literal_eval or a special
1327 placeholder object if the domain or context refers to free variables.
1329 :param elem: the current node being parsed
1330 :type param: xml.etree.ElementTree.Element
1331 :param session: OpenERP session object, used to store and retrieve
1333 :type session: openerpweb.openerpweb.OpenERPSession
1335 for el in ['domain', 'filter_domain']:
1336 domain = elem.get(el, '').strip()
1338 elem.set(el, parse_domain(domain, session))
1339 elem.set(el + '_string', domain)
1340 for el in ['context', 'default_get']:
1341 context_string = elem.get(el, '').strip()
1343 elem.set(el, parse_context(context_string, session))
1344 elem.set(el + '_string', context_string)
1346 @openerpweb.jsonrequest
1347 def load(self, req, model, view_id, view_type, toolbar=False):
1348 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1350 class TreeView(View):
1351 _cp_path = "/web/treeview"
1353 @openerpweb.jsonrequest
1354 def action(self, req, model, id):
1355 return load_actions_from_ir_values(
1356 req,'action', 'tree_but_open',[(model, id)],
1359 class SearchView(View):
1360 _cp_path = "/web/searchview"
1362 @openerpweb.jsonrequest
1363 def load(self, req, model, view_id):
1364 fields_view = self.fields_view_get(req, model, view_id, 'search')
1365 return {'fields_view': fields_view}
1367 @openerpweb.jsonrequest
1368 def fields_get(self, req, model):
1369 Model = req.session.model(model)
1370 fields = Model.fields_get(False, req.session.eval_context(req.context))
1371 for field in fields.values():
1372 # shouldn't convert the views too?
1373 if field.get('domain'):
1374 field["domain"] = parse_domain(field["domain"], req.session)
1375 if field.get('context'):
1376 field["context"] = parse_context(field["context"], req.session)
1377 return {'fields': fields}
1379 @openerpweb.jsonrequest
1380 def get_filters(self, req, model):
1381 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1382 Model = req.session.model("ir.filters")
1383 filters = Model.get_filters(model)
1384 for filter in filters:
1386 parsed_context = parse_context(filter["context"], req.session)
1387 filter["context"] = (parsed_context
1388 if not isinstance(parsed_context, common.nonliterals.BaseContext)
1389 else req.session.eval_context(parsed_context))
1391 parsed_domain = parse_domain(filter["domain"], req.session)
1392 filter["domain"] = (parsed_domain
1393 if not isinstance(parsed_domain, common.nonliterals.BaseDomain)
1394 else req.session.eval_domain(parsed_domain))
1396 logger.exception("Failed to parse custom filter %s in %s",
1397 filter['name'], model)
1398 filter['disabled'] = True
1399 del filter['context']
1400 del filter['domain']
1403 class Binary(openerpweb.Controller):
1404 _cp_path = "/web/binary"
1406 @openerpweb.httprequest
1407 def image(self, req, model, id, field, **kw):
1408 last_update = '__last_update'
1409 Model = req.session.model(model)
1410 context = req.session.eval_context(req.context)
1411 headers = [('Content-Type', 'image/png')]
1412 etag = req.httprequest.headers.get('If-None-Match')
1413 hashed_session = hashlib.md5(req.session_id).hexdigest()
1414 id = None if not id else simplejson.loads(id)
1415 if type(id) is list:
1418 if not id and hashed_session == etag:
1419 return werkzeug.wrappers.Response(status=304)
1421 date = Model.read([id], [last_update], context)[0].get(last_update)
1422 if hashlib.md5(date).hexdigest() == etag:
1423 return werkzeug.wrappers.Response(status=304)
1425 retag = hashed_session
1428 res = Model.default_get([field], context).get(field)
1429 image_data = base64.b64decode(res)
1431 res = Model.read([id], [last_update, field], context)[0]
1432 retag = hashlib.md5(res.get(last_update)).hexdigest()
1433 image_data = base64.b64decode(res.get(field))
1434 except (TypeError, xmlrpclib.Fault):
1435 image_data = self.placeholder(req)
1436 headers.append(('ETag', retag))
1437 headers.append(('Content-Length', len(image_data)))
1439 ncache = int(kw.get('cache'))
1440 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1443 return req.make_response(image_data, headers)
1444 def placeholder(self, req):
1445 addons_path = openerpweb.addons_manifest['web']['addons_path']
1446 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1447 def content_disposition(self, filename, req):
1448 filename = filename.encode('utf8')
1449 escaped = urllib2.quote(filename)
1450 browser = req.httprequest.user_agent.browser
1451 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
1452 if browser == 'msie' and version < 9:
1453 return "attachment; filename=%s" % escaped
1454 elif browser == 'safari':
1455 return "attachment; filename=%s" % filename
1457 return "attachment; filename*=UTF-8''%s" % escaped
1459 @openerpweb.httprequest
1460 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1461 """ Download link for files stored as binary fields.
1463 If the ``id`` parameter is omitted, fetches the default value for the
1464 binary field (via ``default_get``), otherwise fetches the field for
1465 that precise record.
1467 :param req: OpenERP request
1468 :type req: :class:`web.common.http.HttpRequest`
1469 :param str model: name of the model to fetch the binary from
1470 :param str field: binary field
1471 :param str id: id of the record from which to fetch the binary
1472 :param str filename_field: field holding the file's name, if any
1473 :returns: :class:`werkzeug.wrappers.Response`
1475 Model = req.session.model(model)
1476 context = req.session.eval_context(req.context)
1479 fields.append(filename_field)
1481 res = Model.read([int(id)], fields, context)[0]
1483 res = Model.default_get(fields, context)
1484 filecontent = base64.b64decode(res.get(field, ''))
1486 return req.not_found()
1488 filename = '%s_%s' % (model.replace('.', '_'), id)
1490 filename = res.get(filename_field, '') or filename
1491 return req.make_response(filecontent,
1492 [('Content-Type', 'application/octet-stream'),
1493 ('Content-Disposition', self.content_disposition(filename, req))])
1495 @openerpweb.httprequest
1496 def saveas_ajax(self, req, data, token):
1497 jdata = simplejson.loads(data)
1498 model = jdata['model']
1499 field = jdata['field']
1500 id = jdata.get('id', None)
1501 filename_field = jdata.get('filename_field', None)
1502 context = jdata.get('context', dict())
1504 context = req.session.eval_context(context)
1505 Model = req.session.model(model)
1508 fields.append(filename_field)
1510 res = Model.read([int(id)], fields, context)[0]
1512 res = Model.default_get(fields, context)
1513 filecontent = base64.b64decode(res.get(field, ''))
1515 raise ValueError("No content found for field '%s' on '%s:%s'" %
1518 filename = '%s_%s' % (model.replace('.', '_'), id)
1520 filename = res.get(filename_field, '') or filename
1521 return req.make_response(filecontent,
1522 headers=[('Content-Type', 'application/octet-stream'),
1523 ('Content-Disposition', self.content_disposition(filename, req))],
1524 cookies={'fileToken': int(token)})
1526 @openerpweb.httprequest
1527 def upload(self, req, callback, ufile):
1528 # TODO: might be useful to have a configuration flag for max-length file uploads
1530 out = """<script language="javascript" type="text/javascript">
1531 var win = window.top.window;
1532 win.jQuery(win).trigger(%s, %s);
1535 args = [len(data), ufile.filename,
1536 ufile.content_type, base64.b64encode(data)]
1537 except Exception, e:
1538 args = [False, e.message]
1539 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1541 @openerpweb.httprequest
1542 def upload_attachment(self, req, callback, model, id, ufile):
1543 context = req.session.eval_context(req.context)
1544 Model = req.session.model('ir.attachment')
1546 out = """<script language="javascript" type="text/javascript">
1547 var win = window.top.window;
1548 win.jQuery(win).trigger(%s, %s);
1550 attachment_id = Model.create({
1551 'name': ufile.filename,
1552 'datas': base64.encodestring(ufile.read()),
1553 'datas_fname': ufile.filename,
1558 'filename': ufile.filename,
1561 except Exception, e:
1562 args = { 'error': e.message }
1563 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1565 class Action(openerpweb.Controller):
1566 _cp_path = "/web/action"
1568 # For most actions, the type attribute and the model name are the same, but
1569 # there are exceptions. This dict is used to remap action type attributes
1570 # to the "real" model name when they differ.
1572 "ir.actions.act_url": "ir.actions.url",
1575 @openerpweb.jsonrequest
1576 def load(self, req, action_id, do_not_eval=False):
1577 Actions = req.session.model('ir.actions.actions')
1579 context = req.session.eval_context(req.context)
1582 action_id = int(action_id)
1585 module, xmlid = action_id.split('.', 1)
1586 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1587 assert model.startswith('ir.actions.')
1589 action_id = 0 # force failed read
1591 base_action = Actions.read([action_id], ['type'], context)
1594 action_type = base_action[0]['type']
1595 if action_type == 'ir.actions.report.xml':
1596 ctx.update({'bin_size': True})
1598 action_model = self.action_mapping.get(action_type, action_type)
1599 action = req.session.model(action_model).read([action_id], False, ctx)
1601 value = clean_action(req, action[0], do_not_eval)
1602 return {'result': value}
1604 @openerpweb.jsonrequest
1605 def run(self, req, action_id):
1606 return_action = req.session.model('ir.actions.server').run(
1607 [action_id], req.session.eval_context(req.context))
1609 return clean_action(req, return_action)
1614 _cp_path = "/web/export"
1616 @openerpweb.jsonrequest
1617 def formats(self, req):
1618 """ Returns all valid export formats
1620 :returns: for each export format, a pair of identifier and printable name
1621 :rtype: [(str, str)]
1625 for path, controller in openerpweb.controllers_path.iteritems()
1626 if path.startswith(self._cp_path)
1627 if hasattr(controller, 'fmt')
1628 ], key=operator.itemgetter("label"))
1630 def fields_get(self, req, model):
1631 Model = req.session.model(model)
1632 fields = Model.fields_get(False, req.session.eval_context(req.context))
1635 @openerpweb.jsonrequest
1636 def get_fields(self, req, model, prefix='', parent_name= '',
1637 import_compat=True, parent_field_type=None,
1640 if import_compat and parent_field_type == "many2one":
1643 fields = self.fields_get(req, model)
1646 fields.pop('id', None)
1648 fields['.id'] = fields.pop('id', {'string': 'ID'})
1650 fields_sequence = sorted(fields.iteritems(),
1651 key=lambda field: field[1].get('string', ''))
1654 for field_name, field in fields_sequence:
1656 if exclude and field_name in exclude:
1658 if field.get('readonly'):
1659 # If none of the field's states unsets readonly, skip the field
1660 if all(dict(attrs).get('readonly', True)
1661 for attrs in field.get('states', {}).values()):
1664 id = prefix + (prefix and '/'or '') + field_name
1665 name = parent_name + (parent_name and '/' or '') + field['string']
1666 record = {'id': id, 'string': name,
1667 'value': id, 'children': False,
1668 'field_type': field.get('type'),
1669 'required': field.get('required'),
1670 'relation_field': field.get('relation_field')}
1671 records.append(record)
1673 if len(name.split('/')) < 3 and 'relation' in field:
1674 ref = field.pop('relation')
1675 record['value'] += '/id'
1676 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1678 if not import_compat or field['type'] == 'one2many':
1679 # m2m field in import_compat is childless
1680 record['children'] = True
1684 @openerpweb.jsonrequest
1685 def namelist(self,req, model, export_id):
1686 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1687 export = req.session.model("ir.exports").read([export_id])[0]
1688 export_fields_list = req.session.model("ir.exports.line").read(
1689 export['export_fields'])
1691 fields_data = self.fields_info(
1692 req, model, map(operator.itemgetter('name'), export_fields_list))
1695 {'name': field['name'], 'label': fields_data[field['name']]}
1696 for field in export_fields_list
1699 def fields_info(self, req, model, export_fields):
1701 fields = self.fields_get(req, model)
1703 # To make fields retrieval more efficient, fetch all sub-fields of a
1704 # given field at the same time. Because the order in the export list is
1705 # arbitrary, this requires ordering all sub-fields of a given field
1706 # together so they can be fetched at the same time
1708 # Works the following way:
1709 # * sort the list of fields to export, the default sorting order will
1710 # put the field itself (if present, for xmlid) and all of its
1711 # sub-fields right after it
1712 # * then, group on: the first field of the path (which is the same for
1713 # a field and for its subfields and the length of splitting on the
1714 # first '/', which basically means grouping the field on one side and
1715 # all of the subfields on the other. This way, we have the field (for
1716 # the xmlid) with length 1, and all of the subfields with the same
1717 # base but a length "flag" of 2
1718 # * if we have a normal field (length 1), just add it to the info
1719 # mapping (with its string) as-is
1720 # * otherwise, recursively call fields_info via graft_subfields.
1721 # all graft_subfields does is take the result of fields_info (on the
1722 # field's model) and prepend the current base (current field), which
1723 # rebuilds the whole sub-tree for the field
1725 # result: because we're not fetching the fields_get for half the
1726 # database models, fetching a namelist with a dozen fields (including
1727 # relational data) falls from ~6s to ~300ms (on the leads model).
1728 # export lists with no sub-fields (e.g. import_compatible lists with
1729 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1730 # there's a single fields_get to execute)
1731 for (base, length), subfields in itertools.groupby(
1732 sorted(export_fields),
1733 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1734 subfields = list(subfields)
1736 # subfields is a seq of $base/*rest, and not loaded yet
1737 info.update(self.graft_subfields(
1738 req, fields[base]['relation'], base, fields[base]['string'],
1742 info[base] = fields[base]['string']
1746 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1747 export_fields = [field.split('/', 1)[1] for field in fields]
1749 (prefix + '/' + k, prefix_string + '/' + v)
1750 for k, v in self.fields_info(req, model, export_fields).iteritems())
1752 #noinspection PyPropertyDefinition
1754 def content_type(self):
1755 """ Provides the format's content type """
1756 raise NotImplementedError()
1758 def filename(self, base):
1759 """ Creates a valid filename for the format (with extension) from the
1760 provided base name (exension-less)
1762 raise NotImplementedError()
1764 def from_data(self, fields, rows):
1765 """ Conversion method from OpenERP's export data to whatever the
1766 current export class outputs
1768 :params list fields: a list of fields to export
1769 :params list rows: a list of records to export
1773 raise NotImplementedError()
1775 @openerpweb.httprequest
1776 def index(self, req, data, token):
1777 model, fields, ids, domain, import_compat = \
1778 operator.itemgetter('model', 'fields', 'ids', 'domain',
1780 simplejson.loads(data))
1782 context = req.session.eval_context(req.context)
1783 Model = req.session.model(model)
1784 ids = ids or Model.search(domain, 0, False, False, context)
1786 field_names = map(operator.itemgetter('name'), fields)
1787 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1790 columns_headers = field_names
1792 columns_headers = [val['label'].strip() for val in fields]
1795 return req.make_response(self.from_data(columns_headers, import_data),
1796 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1797 ('Content-Type', self.content_type)],
1798 cookies={'fileToken': int(token)})
1800 class CSVExport(Export):
1801 _cp_path = '/web/export/csv'
1802 fmt = {'tag': 'csv', 'label': 'CSV'}
1805 def content_type(self):
1806 return 'text/csv;charset=utf8'
1808 def filename(self, base):
1809 return base + '.csv'
1811 def from_data(self, fields, rows):
1813 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1815 writer.writerow([name.encode('utf-8') for name in fields])
1820 if isinstance(d, basestring):
1821 d = d.replace('\n',' ').replace('\t',' ')
1823 d = d.encode('utf-8')
1824 except UnicodeError:
1826 if d is False: d = None
1828 writer.writerow(row)
1835 class ExcelExport(Export):
1836 _cp_path = '/web/export/xls'
1840 'error': None if xlwt else "XLWT required"
1844 def content_type(self):
1845 return 'application/vnd.ms-excel'
1847 def filename(self, base):
1848 return base + '.xls'
1850 def from_data(self, fields, rows):
1851 workbook = xlwt.Workbook()
1852 worksheet = workbook.add_sheet('Sheet 1')
1854 for i, fieldname in enumerate(fields):
1855 worksheet.write(0, i, fieldname)
1856 worksheet.col(i).width = 8000 # around 220 pixels
1858 style = xlwt.easyxf('align: wrap yes')
1860 for row_index, row in enumerate(rows):
1861 for cell_index, cell_value in enumerate(row):
1862 if isinstance(cell_value, basestring):
1863 cell_value = re.sub("\r", " ", cell_value)
1864 if cell_value is False: cell_value = None
1865 worksheet.write(row_index + 1, cell_index, cell_value, style)
1874 class Reports(View):
1875 _cp_path = "/web/report"
1876 POLLING_DELAY = 0.25
1878 'doc': 'application/vnd.ms-word',
1879 'html': 'text/html',
1880 'odt': 'application/vnd.oasis.opendocument.text',
1881 'pdf': 'application/pdf',
1882 'sxw': 'application/vnd.sun.xml.writer',
1883 'xls': 'application/vnd.ms-excel',
1886 @openerpweb.httprequest
1887 def index(self, req, action, token):
1888 action = simplejson.loads(action)
1890 report_srv = req.session.proxy("report")
1891 context = req.session.eval_context(
1892 common.nonliterals.CompoundContext(
1893 req.context or {}, action[ "context"]))
1896 report_ids = context["active_ids"]
1897 if 'report_type' in action:
1898 report_data['report_type'] = action['report_type']
1899 if 'datas' in action:
1900 if 'ids' in action['datas']:
1901 report_ids = action['datas'].pop('ids')
1902 report_data.update(action['datas'])
1904 report_id = report_srv.report(
1905 req.session._db, req.session._uid, req.session._password,
1906 action["report_name"], report_ids,
1907 report_data, context)
1909 report_struct = None
1911 report_struct = report_srv.report_get(
1912 req.session._db, req.session._uid, req.session._password, report_id)
1913 if report_struct["state"]:
1916 time.sleep(self.POLLING_DELAY)
1918 report = base64.b64decode(report_struct['result'])
1919 if report_struct.get('code') == 'zlib':
1920 report = zlib.decompress(report)
1921 report_mimetype = self.TYPES_MAPPING.get(
1922 report_struct['format'], 'octet-stream')
1923 file_name = action.get('name', 'report')
1924 if 'name' not in action:
1925 reports = req.session.model('ir.actions.report.xml')
1926 res_id = reports.search([('report_name', '=', action['report_name']),],
1927 0, False, False, context)
1929 file_name = reports.read(res_id[0], ['name'], context)['name']
1931 file_name = action['report_name']
1933 return req.make_response(report,
1935 # maybe we should take of what characters can appear in a file name?
1936 ('Content-Disposition', 'attachment; filename="%s.%s"' % (file_name, report_struct['format'])),
1937 ('Content-Type', report_mimetype),
1938 ('Content-Length', len(report))],
1939 cookies={'fileToken': int(token)})
1942 _cp_path = "/web/import"
1944 def fields_get(self, req, model):
1945 Model = req.session.model(model)
1946 fields = Model.fields_get(False, req.session.eval_context(req.context))
1949 @openerpweb.httprequest
1950 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1952 data = list(csv.reader(
1953 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1954 except csv.Error, e:
1956 return '<script>window.top.%s(%s);</script>' % (
1957 jsonp, simplejson.dumps({'error': {
1958 'message': 'Error parsing CSV file: %s' % e,
1959 # decodes each byte to a unicode character, which may or
1960 # may not be printable, but decoding will succeed.
1961 # Otherwise simplejson will try to decode the `str` using
1962 # utf-8, which is very likely to blow up on characters out
1963 # of the ascii range (in range [128, 256))
1964 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1967 return '<script>window.top.%s(%s);</script>' % (
1968 jsonp, simplejson.dumps(
1969 {'records': data[:10]}, encoding=csvcode))
1970 except UnicodeDecodeError:
1971 return '<script>window.top.%s(%s);</script>' % (
1972 jsonp, simplejson.dumps({
1973 'message': u"Failed to decode CSV file using encoding %s, "
1974 u"try switching to a different encoding" % csvcode
1977 @openerpweb.httprequest
1978 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1980 modle_obj = req.session.model(model)
1981 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1982 simplejson.loads(meta))
1985 if not (csvdel and len(csvdel) == 1):
1986 error = u"The CSV delimiter must be a single character"
1988 if not indices and fields:
1989 error = u"You must select at least one field to import"
1992 return '<script>window.top.%s(%s);</script>' % (
1993 jsonp, simplejson.dumps({'error': {'message': error}}))
1995 # skip ignored records (@skip parameter)
1996 # then skip empty lines (not valid csv)
1997 # nb: should these operations be reverted?
1998 rows_to_import = itertools.ifilter(
2001 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
2004 # if only one index, itemgetter will return an atom rather than a tuple
2005 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
2006 else: mapper = operator.itemgetter(*indices)
2011 # decode each data row
2013 [record.decode(csvcode) for record in row]
2014 for row in itertools.imap(mapper, rows_to_import)
2015 # don't insert completely empty rows (can happen due to fields
2016 # filtering in case of e.g. o2m content rows)
2019 except UnicodeDecodeError:
2020 error = u"Failed to decode CSV file using encoding %s" % csvcode
2021 except csv.Error, e:
2022 error = u"Could not process CSV file: %s" % e
2024 # If the file contains nothing,
2026 error = u"File to import is empty"
2028 return '<script>window.top.%s(%s);</script>' % (
2029 jsonp, simplejson.dumps({'error': {'message': error}}))
2032 (code, record, message, _nope) = modle_obj.import_data(
2033 fields, data, 'init', '', False,
2034 req.session.eval_context(req.context))
2035 except xmlrpclib.Fault, e:
2036 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
2037 return '<script>window.top.%s(%s);</script>' % (
2038 jsonp, simplejson.dumps({'error':error}))
2041 return '<script>window.top.%s(%s);</script>' % (
2042 jsonp, simplejson.dumps({'success':True}))
2044 msg = u"Error during import: %s\n\nTrying to import record %r" % (
2046 return '<script>window.top.%s(%s);</script>' % (
2047 jsonp, simplejson.dumps({'error': {'message':msg}}))
2049 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: