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):
230 return [m for m in req.config.server_wide_modules if m in openerpweb.addons_manifest]
231 # TODO the following will be enabled once we separate the module code and translation loading
234 for i in req.config.server_wide_modules:
235 if i in openerpweb.addons_manifest:
237 # if only one db load every module at boot
241 except xmlrpclib.Fault:
242 # ignore access denied
245 dbside = module_installed_bypass_session(dbs[0])
246 dbside = [i for i in dbside if i not in serverside]
247 addons = serverside + dbside
250 def concat_xml(file_list):
251 """Concatenate xml files
253 :param list(str) file_list: list of files to check
254 :returns: (concatenation_result, checksum)
257 checksum = hashlib.new('sha1')
259 return '', checksum.hexdigest()
262 for fname in file_list:
263 with open(fname, 'rb') as fp:
265 checksum.update(contents)
267 xml = ElementTree.parse(fp).getroot()
270 root = ElementTree.Element(xml.tag)
271 #elif root.tag != xml.tag:
272 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
274 for child in xml.getchildren():
276 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
278 def concat_files(file_list, reader=None, intersperse=""):
279 """ Concatenates contents of all provided files
281 :param list(str) file_list: list of files to check
282 :param function reader: reading procedure for each file
283 :param str intersperse: string to intersperse between file contents
284 :returns: (concatenation_result, checksum)
287 checksum = hashlib.new('sha1')
289 return '', checksum.hexdigest()
293 with open(f, 'rb') as fp:
297 for fname in file_list:
298 contents = reader(fname)
299 checksum.update(contents)
300 files_content.append(contents)
302 files_concat = intersperse.join(files_content)
303 return files_concat, checksum.hexdigest()
305 def concat_js(file_list):
306 content, checksum = concat_files(file_list, intersperse=';')
307 content = rjsmin(content)
308 return content, checksum
310 def manifest_glob(req, addons, key):
312 addons = module_boot(req)
314 addons = addons.split(',')
317 manifest = openerpweb.addons_manifest.get(addon, None)
320 # ensure does not ends with /
321 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
322 globlist = manifest.get(key, [])
323 for pattern in globlist:
324 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
325 r.append((path, path[len(addons_path):]))
328 def manifest_list(req, mods, extension):
330 path = '/web/webclient/' + extension
332 path += '?mods=' + mods
334 files = manifest_glob(req, mods, extension)
335 i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
336 req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
338 return [wp for _fp, wp in files]
340 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
342 def get_last_modified(files):
343 """ Returns the modification time of the most recently modified
346 :param list(str) files: names of files to check
347 :return: most recent modification time amongst the fileset
348 :rtype: datetime.datetime
352 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
354 return datetime.datetime(1970, 1, 1)
356 def make_conditional(req, response, last_modified=None, etag=None):
357 """ Makes the provided response conditional based upon the request,
358 and mandates revalidation from clients
360 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
361 setting ``last_modified`` and ``etag`` correctly on the response object
363 :param req: OpenERP request
364 :type req: web.common.http.WebRequest
365 :param response: Werkzeug response
366 :type response: werkzeug.wrappers.Response
367 :param datetime.datetime last_modified: last modification date of the response content
368 :param str etag: some sort of checksum of the content (deep etag)
369 :return: the response object provided
370 :rtype: werkzeug.wrappers.Response
372 response.cache_control.must_revalidate = True
373 response.cache_control.max_age = 0
375 response.last_modified = last_modified
377 response.set_etag(etag)
378 return response.make_conditional(req.httprequest)
380 def login_and_redirect(req, db, login, key, redirect_url='/'):
381 req.session.authenticate(db, login, key, {})
382 return set_cookie_and_redirect(req, redirect_url)
384 def set_cookie_and_redirect(req, redirect_url):
385 redirect = werkzeug.utils.redirect(redirect_url, 303)
386 redirect.autocorrect_location_header = False
387 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
388 redirect.set_cookie('instance0|session_id', cookie_val)
391 def eval_context_and_domain(session, context, domain=None):
392 e_context = session.eval_context(context)
393 # should we give the evaluated context as an evaluation context to the domain?
394 e_domain = session.eval_domain(domain or [])
396 return e_context, e_domain
398 def load_actions_from_ir_values(req, key, key2, models, meta):
399 context = req.session.eval_context(req.context)
400 Values = req.session.model('ir.values')
401 actions = Values.get(key, key2, models, meta, context)
403 return [(id, name, clean_action(req, action))
404 for id, name, action in actions]
406 def clean_action(req, action, do_not_eval=False):
407 action.setdefault('flags', {})
409 context = req.session.eval_context(req.context)
410 eval_ctx = req.session.evaluation_context(context)
413 # values come from the server, we can just eval them
414 if action.get('context') and isinstance(action.get('context'), basestring):
415 action['context'] = eval( action['context'], eval_ctx ) or {}
417 if action.get('domain') and isinstance(action.get('domain'), basestring):
418 action['domain'] = eval( action['domain'], eval_ctx ) or []
420 if 'context' in action:
421 action['context'] = parse_context(action['context'], req.session)
422 if 'domain' in action:
423 action['domain'] = parse_domain(action['domain'], req.session)
425 action_type = action.setdefault('type', 'ir.actions.act_window_close')
426 if action_type == 'ir.actions.act_window':
427 return fix_view_modes(action)
430 # I think generate_views,fix_view_modes should go into js ActionManager
431 def generate_views(action):
433 While the server generates a sequence called "views" computing dependencies
434 between a bunch of stuff for views coming directly from the database
435 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
436 to return custom view dictionaries generated on the fly.
438 In that case, there is no ``views`` key available on the action.
440 Since the web client relies on ``action['views']``, generate it here from
441 ``view_mode`` and ``view_id``.
443 Currently handles two different cases:
445 * no view_id, multiple view_mode
446 * single view_id, single view_mode
448 :param dict action: action descriptor dictionary to generate a views key for
450 view_id = action.get('view_id') or False
451 if isinstance(view_id, (list, tuple)):
454 # providing at least one view mode is a requirement, not an option
455 view_modes = action['view_mode'].split(',')
457 if len(view_modes) > 1:
459 raise ValueError('Non-db action dictionaries should provide '
460 'either multiple view modes or a single view '
461 'mode and an optional view id.\n\n Got view '
462 'modes %r and view id %r for action %r' % (
463 view_modes, view_id, action))
464 action['views'] = [(False, mode) for mode in view_modes]
466 action['views'] = [(view_id, view_modes[0])]
468 def fix_view_modes(action):
469 """ For historical reasons, OpenERP has weird dealings in relation to
470 view_mode and the view_type attribute (on window actions):
472 * one of the view modes is ``tree``, which stands for both list views
474 * the choice is made by checking ``view_type``, which is either
475 ``form`` for a list view or ``tree`` for an actual tree view
477 This methods simply folds the view_type into view_mode by adding a
478 new view mode ``list`` which is the result of the ``tree`` view_mode
479 in conjunction with the ``form`` view_type.
481 TODO: this should go into the doc, some kind of "peculiarities" section
483 :param dict action: an action descriptor
484 :returns: nothing, the action is modified in place
486 if not action.get('views'):
487 generate_views(action)
489 if action.pop('view_type', 'form') != 'form':
492 if 'view_mode' in action:
493 action['view_mode'] = ','.join(
494 mode if mode != 'tree' else 'list'
495 for mode in action['view_mode'].split(','))
497 [id, mode if mode != 'tree' else 'list']
498 for id, mode in action['views']
503 def parse_domain(domain, session):
504 """ Parses an arbitrary string containing a domain, transforms it
505 to either a literal domain or a :class:`common.nonliterals.Domain`
507 :param domain: the domain to parse, if the domain is not a string it
508 is assumed to be a literal domain and is returned as-is
509 :param session: Current OpenERP session
510 :type session: openerpweb.openerpweb.OpenERPSession
512 if not isinstance(domain, basestring):
515 return ast.literal_eval(domain)
518 return common.nonliterals.Domain(session, domain)
520 def parse_context(context, session):
521 """ Parses an arbitrary string containing a context, transforms it
522 to either a literal context or a :class:`common.nonliterals.Context`
524 :param context: the context to parse, if the context is not a string it
525 is assumed to be a literal domain and is returned as-is
526 :param session: Current OpenERP session
527 :type session: openerpweb.openerpweb.OpenERPSession
529 if not isinstance(context, basestring):
532 return ast.literal_eval(context)
534 return common.nonliterals.Context(session, context)
537 def _local_web_translations(trans_file):
540 with open(trans_file) as t_file:
541 po = babel.messages.pofile.read_po(t_file)
545 if x.id and x.string and "openerp-web" in x.auto_comments:
546 messages.append({'id': x.id, 'string': x.string})
550 #----------------------------------------------------------
551 # OpenERP Web web Controllers
552 #----------------------------------------------------------
554 html_template = """<!DOCTYPE html>
555 <html style="height: 100%%">
557 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
558 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
559 <title>OpenERP</title>
560 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
561 <link rel="stylesheet" href="/web/static/src/css/full.css" />
564 <script type="text/javascript">
566 var s = new openerp.init(%(modules)s);
575 class Home(openerpweb.Controller):
578 @openerpweb.httprequest
579 def index(self, req, s_action=None, **kw):
580 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, None, 'js'))
581 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, None, 'css'))
583 r = html_template % {
586 'modules': simplejson.dumps(module_boot(req)),
587 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
591 @openerpweb.httprequest
592 def login(self, req, db, login, key):
593 return login_and_redirect(req, db, login, key)
596 class WebClient(openerpweb.Controller):
597 _cp_path = "/web/webclient"
599 @openerpweb.jsonrequest
600 def csslist(self, req, mods=None):
601 return manifest_list(req, mods, 'css')
603 @openerpweb.jsonrequest
604 def jslist(self, req, mods=None):
605 return manifest_list(req, mods, 'js')
607 @openerpweb.jsonrequest
608 def qweblist(self, req, mods=None):
609 return manifest_list(req, mods, 'qweb')
611 @openerpweb.httprequest
612 def css(self, req, mods=None):
613 files = list(manifest_glob(req, mods, 'css'))
614 last_modified = get_last_modified(f[0] for f in files)
615 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
616 return werkzeug.wrappers.Response(status=304)
618 file_map = dict(files)
620 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
621 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
624 """read the a css file and absolutify all relative uris"""
625 with open(f, 'rb') as fp:
626 data = fp.read().decode('utf-8')
629 # convert FS path into web path
630 web_dir = '/'.join(os.path.dirname(path).split(os.path.sep))
634 r"""@import \1%s/""" % (web_dir,),
640 r"""url(\1%s/""" % (web_dir,),
643 return data.encode('utf-8')
645 content, checksum = concat_files((f[0] for f in files), reader)
647 return make_conditional(
648 req, req.make_response(content, [('Content-Type', 'text/css')]),
649 last_modified, checksum)
651 @openerpweb.httprequest
652 def js(self, req, mods=None):
653 files = [f[0] for f in manifest_glob(req, mods, 'js')]
654 last_modified = get_last_modified(files)
655 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
656 return werkzeug.wrappers.Response(status=304)
658 content, checksum = concat_js(files)
660 return make_conditional(
661 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
662 last_modified, checksum)
664 @openerpweb.httprequest
665 def qweb(self, req, mods=None):
666 files = [f[0] for f in manifest_glob(req, mods, 'qweb')]
667 last_modified = get_last_modified(files)
668 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
669 return werkzeug.wrappers.Response(status=304)
671 content, checksum = concat_xml(files)
673 return make_conditional(
674 req, req.make_response(content, [('Content-Type', 'text/xml')]),
675 last_modified, checksum)
677 @openerpweb.jsonrequest
678 def bootstrap_translations(self, req, mods):
679 """ Load local translations from *.po files, as a temporary solution
680 until we have established a valid session. This is meant only
681 for translating the login page and db management chrome, using
682 the browser's language. """
683 lang = req.httprequest.accept_languages.best or 'en'
684 # For performance reasons we only load a single translation, so for
685 # sub-languages (that should only be partially translated) we load the
686 # main language PO instead - that should be enough for the login screen.
687 if '-' in lang: # RFC2616 uses '-' separators for sublanguages
688 lang = lang.split('-')[0]
690 translations_per_module = {}
691 for addon_name in mods:
692 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
693 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
694 if not os.path.exists(f_name):
696 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
698 return {"modules": translations_per_module,
699 "lang_parameters": None}
701 @openerpweb.jsonrequest
702 def translations(self, req, mods, lang):
703 res_lang = req.session.model('res.lang')
704 ids = res_lang.search([("code", "=", lang)])
707 lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
708 "grouping", "decimal_point", "thousands_sep"])
710 # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
711 # done server-side when the language is loaded, so we only need to load the user's lang.
712 ir_translation = req.session.model('ir.translation')
713 translations_per_module = {}
714 messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
715 ('comments','like','openerp-web'),('value','!=',False),
717 ['module','src','value','lang'], order='module')
718 for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
719 translations_per_module.setdefault(mod,{'messages':[]})
720 translations_per_module[mod]['messages'].extend({'id': m['src'],
721 'string': m['value']} \
723 return {"modules": translations_per_module,
724 "lang_parameters": lang_params}
726 @openerpweb.jsonrequest
727 def version_info(self, req):
729 "version": common.release.version
732 class Proxy(openerpweb.Controller):
733 _cp_path = '/web/proxy'
735 @openerpweb.jsonrequest
736 def load(self, req, path):
737 """ Proxies an HTTP request through a JSON request.
739 It is strongly recommended to not request binary files through this,
740 as the result will be a binary data blob as well.
742 :param req: OpenERP request
743 :param path: actual request path
744 :return: file content
746 from werkzeug.test import Client
747 from werkzeug.wrappers import BaseResponse
749 return Client(req.httprequest.app, BaseResponse).get(path).data
751 class Database(openerpweb.Controller):
752 _cp_path = "/web/database"
754 @openerpweb.jsonrequest
755 def get_list(self, req):
757 return {"db_list": dbs}
759 @openerpweb.jsonrequest
760 def create(self, req, fields):
761 params = dict(map(operator.itemgetter('name', 'value'), fields))
763 params['super_admin_pwd'],
765 bool(params.get('demo_data')),
767 params['create_admin_pwd']
770 return req.session.proxy("db").create_database(*create_attrs)
772 @openerpweb.jsonrequest
773 def drop(self, req, fields):
774 password, db = operator.itemgetter(
775 'drop_pwd', 'drop_db')(
776 dict(map(operator.itemgetter('name', 'value'), fields)))
779 return req.session.proxy("db").drop(password, db)
780 except xmlrpclib.Fault, e:
781 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
782 return {'error': e.faultCode, 'title': 'Drop Database'}
783 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
785 @openerpweb.httprequest
786 def backup(self, req, backup_db, backup_pwd, token):
788 db_dump = base64.b64decode(
789 req.session.proxy("db").dump(backup_pwd, backup_db))
790 filename = "%(db)s_%(timestamp)s.dump" % {
792 'timestamp': datetime.datetime.utcnow().strftime(
793 "%Y-%m-%d_%H-%M-%SZ")
795 return req.make_response(db_dump,
796 [('Content-Type', 'application/octet-stream; charset=binary'),
797 ('Content-Disposition', 'attachment; filename="' + filename + '"')],
798 {'fileToken': int(token)}
800 except xmlrpclib.Fault, e:
801 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
803 @openerpweb.httprequest
804 def restore(self, req, db_file, restore_pwd, new_db):
806 data = base64.b64encode(db_file.read())
807 req.session.proxy("db").restore(restore_pwd, new_db, data)
809 except xmlrpclib.Fault, e:
810 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
811 raise Exception("AccessDenied")
813 @openerpweb.jsonrequest
814 def change_password(self, req, fields):
815 old_password, new_password = operator.itemgetter(
816 'old_pwd', 'new_pwd')(
817 dict(map(operator.itemgetter('name', 'value'), fields)))
819 return req.session.proxy("db").change_admin_password(old_password, new_password)
820 except xmlrpclib.Fault, e:
821 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
822 return {'error': e.faultCode, 'title': 'Change Password'}
823 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
825 class Session(openerpweb.Controller):
826 _cp_path = "/web/session"
828 def session_info(self, req):
829 req.session.ensure_valid()
831 "session_id": req.session_id,
832 "uid": req.session._uid,
833 "context": req.session.get_context() if req.session._uid else {},
834 "db": req.session._db,
835 "login": req.session._login,
838 @openerpweb.jsonrequest
839 def get_session_info(self, req):
840 return self.session_info(req)
842 @openerpweb.jsonrequest
843 def authenticate(self, req, db, login, password, base_location=None):
844 wsgienv = req.httprequest.environ
845 release = common.release
847 base_location=base_location,
848 HTTP_HOST=wsgienv['HTTP_HOST'],
849 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
850 user_agent="%s / %s" % (release.name, release.version),
852 req.session.authenticate(db, login, password, env)
854 return self.session_info(req)
856 @openerpweb.jsonrequest
857 def change_password (self,req,fields):
858 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
859 dict(map(operator.itemgetter('name', 'value'), fields)))
860 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
861 return {'error':'All passwords have to be filled.','title': 'Change Password'}
862 if new_password != confirm_password:
863 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
865 if req.session.model('res.users').change_password(
866 old_password, new_password):
867 return {'new_password':new_password}
869 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
870 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
872 @openerpweb.jsonrequest
873 def sc_list(self, req):
874 return req.session.model('ir.ui.view_sc').get_sc(
875 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
877 @openerpweb.jsonrequest
878 def get_lang_list(self, req):
881 'lang_list': (req.session.proxy("db").list_lang() or []),
885 return {"error": e, "title": "Languages"}
887 @openerpweb.jsonrequest
888 def modules(self, req):
889 # return all installed modules. Web client is smart enough to not load a module twice
890 return module_installed(req)
892 @openerpweb.jsonrequest
893 def eval_domain_and_context(self, req, contexts, domains,
895 """ Evaluates sequences of domains and contexts, composing them into
896 a single context, domain or group_by sequence.
898 :param list contexts: list of contexts to merge together. Contexts are
899 evaluated in sequence, all previous contexts
900 are part of their own evaluation context
901 (starting at the session context).
902 :param list domains: list of domains to merge together. Domains are
903 evaluated in sequence and appended to one another
904 (implicit AND), their evaluation domain is the
905 result of merging all contexts.
906 :param list group_by_seq: list of domains (which may be in a different
907 order than the ``contexts`` parameter),
908 evaluated in sequence, their ``'group_by'``
909 key is extracted if they have one.
914 the global context created by merging all of
918 the concatenation of all domains
921 a list of fields to group by, potentially empty (in which case
922 no group by should be performed)
924 context, domain = eval_context_and_domain(req.session,
925 common.nonliterals.CompoundContext(*(contexts or [])),
926 common.nonliterals.CompoundDomain(*(domains or [])))
928 group_by_sequence = []
929 for candidate in (group_by_seq or []):
930 ctx = req.session.eval_context(candidate, context)
931 group_by = ctx.get('group_by')
934 elif isinstance(group_by, basestring):
935 group_by_sequence.append(group_by)
937 group_by_sequence.extend(group_by)
942 'group_by': group_by_sequence
945 @openerpweb.jsonrequest
946 def save_session_action(self, req, the_action):
948 This method store an action object in the session object and returns an integer
949 identifying that action. The method get_session_action() can be used to get
952 :param the_action: The action to save in the session.
953 :type the_action: anything
954 :return: A key identifying the saved action.
957 saved_actions = req.httpsession.get('saved_actions')
958 if not saved_actions:
959 saved_actions = {"next":0, "actions":{}}
960 req.httpsession['saved_actions'] = saved_actions
961 # we don't allow more than 10 stored actions
962 if len(saved_actions["actions"]) >= 10:
963 del saved_actions["actions"][min(saved_actions["actions"])]
964 key = saved_actions["next"]
965 saved_actions["actions"][key] = the_action
966 saved_actions["next"] = key + 1
969 @openerpweb.jsonrequest
970 def get_session_action(self, req, key):
972 Gets back a previously saved action. This method can return None if the action
973 was saved since too much time (this case should be handled in a smart way).
975 :param key: The key given by save_session_action()
977 :return: The saved action or None.
980 saved_actions = req.httpsession.get('saved_actions')
981 if not saved_actions:
983 return saved_actions["actions"].get(key)
985 @openerpweb.jsonrequest
986 def check(self, req):
987 req.session.assert_valid()
990 @openerpweb.jsonrequest
991 def destroy(self, req):
992 req.session._suicide = True
994 class Menu(openerpweb.Controller):
995 _cp_path = "/web/menu"
997 @openerpweb.jsonrequest
999 return {'data': self.do_load(req)}
1001 @openerpweb.jsonrequest
1002 def get_user_roots(self, req):
1003 return self.do_get_user_roots(req)
1005 def do_get_user_roots(self, req):
1006 """ Return all root menu ids visible for the session user.
1008 :param req: A request object, with an OpenERP session attribute
1009 :type req: < session -> OpenERPSession >
1010 :return: the root menu ids
1014 context = s.eval_context(req.context)
1015 Menus = s.model('ir.ui.menu')
1016 # If a menu action is defined use its domain to get the root menu items
1017 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1019 menu_domain = [('parent_id', '=', False)]
1021 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1023 menu_domain = ast.literal_eval(domain_string)
1025 return Menus.search(menu_domain, 0, False, False, context)
1027 def do_load(self, req):
1028 """ Loads all menu items (all applications and their sub-menus).
1030 :param req: A request object, with an OpenERP session attribute
1031 :type req: < session -> OpenERPSession >
1032 :return: the menu root
1033 :rtype: dict('children': menu_nodes)
1035 context = req.session.eval_context(req.context)
1036 Menus = req.session.model('ir.ui.menu')
1038 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1039 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1041 # menus are loaded fully unlike a regular tree view, cause there are a
1042 # limited number of items (752 when all 6.1 addons are installed)
1043 menu_ids = Menus.search([], 0, False, False, context)
1044 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1045 # adds roots at the end of the sequence, so that they will overwrite
1046 # equivalent menu items from full menu read when put into id:item
1047 # mapping, resulting in children being correctly set on the roots.
1048 menu_items.extend(menu_roots)
1050 # make a tree using parent_id
1051 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1052 for menu_item in menu_items:
1053 if menu_item['parent_id']:
1054 parent = menu_item['parent_id'][0]
1057 if parent in menu_items_map:
1058 menu_items_map[parent].setdefault(
1059 'children', []).append(menu_item)
1061 # sort by sequence a tree using parent_id
1062 for menu_item in menu_items:
1063 menu_item.setdefault('children', []).sort(
1064 key=operator.itemgetter('sequence'))
1068 @openerpweb.jsonrequest
1069 def action(self, req, menu_id):
1070 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1071 [('ir.ui.menu', menu_id)], False)
1072 return {"action": actions}
1074 class DataSet(openerpweb.Controller):
1075 _cp_path = "/web/dataset"
1077 @openerpweb.jsonrequest
1078 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1079 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1080 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1082 """ Performs a search() followed by a read() (if needed) using the
1083 provided search criteria
1085 :param req: a JSON-RPC request object
1086 :type req: openerpweb.JsonRequest
1087 :param str model: the name of the model to search on
1088 :param fields: a list of the fields to return in the result records
1090 :param int offset: from which index should the results start being returned
1091 :param int limit: the maximum number of records to return
1092 :param list domain: the search domain for the query
1093 :param list sort: sorting directives
1094 :returns: A structure (dict) with two keys: ids (all the ids matching
1095 the (domain, context) pair) and records (paginated records
1096 matching fields selection set)
1099 Model = req.session.model(model)
1101 context, domain = eval_context_and_domain(
1102 req.session, req.context, domain)
1104 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1105 if limit and len(ids) == limit:
1106 length = Model.search_count(domain, context)
1108 length = len(ids) + (offset or 0)
1109 if fields and fields == ['id']:
1110 # shortcut read if we only want the ids
1113 'records': [{'id': id} for id in ids]
1116 records = Model.read(ids, fields or False, context)
1117 records.sort(key=lambda obj: ids.index(obj['id']))
1123 @openerpweb.jsonrequest
1124 def load(self, req, model, id, fields):
1125 m = req.session.model(model)
1127 r = m.read([id], False, req.session.eval_context(req.context))
1130 return {'value': value}
1132 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1133 has_domain = domain_id is not None and domain_id < len(args)
1134 has_context = context_id is not None and context_id < len(args)
1136 domain = args[domain_id] if has_domain else []
1137 context = args[context_id] if has_context else {}
1138 c, d = eval_context_and_domain(req.session, context, domain)
1142 args[context_id] = c
1144 return self._call_kw(req, model, method, args, {})
1146 def _call_kw(self, req, model, method, args, kwargs):
1147 for i in xrange(len(args)):
1148 if isinstance(args[i], common.nonliterals.BaseContext):
1149 args[i] = req.session.eval_context(args[i])
1150 elif isinstance(args[i], common.nonliterals.BaseDomain):
1151 args[i] = req.session.eval_domain(args[i])
1152 for k in kwargs.keys():
1153 if isinstance(kwargs[k], common.nonliterals.BaseContext):
1154 kwargs[k] = req.session.eval_context(kwargs[k])
1155 elif isinstance(kwargs[k], common.nonliterals.BaseDomain):
1156 kwargs[k] = req.session.eval_domain(kwargs[k])
1158 # Temporary implements future display_name special field for model#read()
1159 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1160 if 'display_name' in args[1]:
1161 names = req.session.model(model).name_get(args[0], **kwargs)
1162 args[1].remove('display_name')
1163 r = getattr(req.session.model(model), method)(*args, **kwargs)
1164 for i in range(len(r)):
1165 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1168 return getattr(req.session.model(model), method)(*args, **kwargs)
1170 @openerpweb.jsonrequest
1171 def onchange(self, req, model, method, args, context_id=None):
1172 """ Support method for handling onchange calls: behaves much like call
1173 with the following differences:
1175 * Does not take a domain_id
1176 * Is aware of the return value's structure, and will parse the domains
1177 if needed in order to return either parsed literal domains (in JSON)
1178 or non-literal domain instances, allowing those domains to be used
1182 :type req: web.common.http.JsonRequest
1183 :param str model: object type on which to call the method
1184 :param str method: name of the onchange handler method
1185 :param list args: arguments to call the onchange handler with
1186 :param int context_id: index of the context object in the list of
1188 :return: result of the onchange call with all domains parsed
1190 result = self.call_common(req, model, method, args, context_id=context_id)
1191 if not result or 'domain' not in result:
1194 result['domain'] = dict(
1195 (k, parse_domain(v, req.session))
1196 for k, v in result['domain'].iteritems())
1200 @openerpweb.jsonrequest
1201 def call(self, req, model, method, args, domain_id=None, context_id=None):
1202 return self.call_common(req, model, method, args, domain_id, context_id)
1204 @openerpweb.jsonrequest
1205 def call_kw(self, req, model, method, args, kwargs):
1206 return self._call_kw(req, model, method, args, kwargs)
1208 @openerpweb.jsonrequest
1209 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1210 action = self.call_common(req, model, method, args, domain_id, context_id)
1211 if isinstance(action, dict) and action.get('type') != '':
1212 return clean_action(req, action)
1215 @openerpweb.jsonrequest
1216 def exec_workflow(self, req, model, id, signal):
1217 return req.session.exec_workflow(model, id, signal)
1219 @openerpweb.jsonrequest
1220 def resequence(self, req, model, ids, field='sequence', offset=0):
1221 """ Re-sequences a number of records in the model, by their ids
1223 The re-sequencing starts at the first model of ``ids``, the sequence
1224 number is incremented by one after each record and starts at ``offset``
1226 :param ids: identifiers of the records to resequence, in the new sequence order
1228 :param str field: field used for sequence specification, defaults to
1230 :param int offset: sequence number for first record in ``ids``, allows
1231 starting the resequencing from an arbitrary number,
1234 m = req.session.model(model)
1235 if not m.fields_get([field]):
1237 # python 2.6 has no start parameter
1238 for i, id in enumerate(ids):
1239 m.write(id, { field: i + offset })
1242 class DataGroup(openerpweb.Controller):
1243 _cp_path = "/web/group"
1244 @openerpweb.jsonrequest
1245 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1246 Model = req.session.model(model)
1247 context, domain = eval_context_and_domain(req.session, req.context, domain)
1249 return Model.read_group(
1250 domain or [], fields, group_by_fields, 0, False,
1251 dict(context, group_by=group_by_fields), sort or False)
1253 class View(openerpweb.Controller):
1254 _cp_path = "/web/view"
1256 def fields_view_get(self, req, model, view_id, view_type,
1257 transform=True, toolbar=False, submenu=False):
1258 Model = req.session.model(model)
1259 context = req.session.eval_context(req.context)
1260 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1261 # todo fme?: check that we should pass the evaluated context here
1262 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1263 if toolbar and transform:
1264 self.process_toolbar(req, fvg['toolbar'])
1267 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1268 # depending on how it feels, xmlrpclib.ServerProxy can translate
1269 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1270 # enjoy unicode strings which can not be trivially converted to
1271 # strings, and it blows up during parsing.
1273 # So ensure we fix this retardation by converting view xml back to
1275 if isinstance(fvg['arch'], unicode):
1276 arch = fvg['arch'].encode('utf-8')
1279 fvg['arch_string'] = arch
1282 evaluation_context = session.evaluation_context(context or {})
1283 xml = self.transform_view(arch, session, evaluation_context)
1285 xml = ElementTree.fromstring(arch)
1286 fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
1288 if 'id' in fvg['fields']:
1289 # Special case for id's
1290 id_field = fvg['fields']['id']
1291 id_field['original_type'] = id_field['type']
1292 id_field['type'] = 'id'
1294 for field in fvg['fields'].itervalues():
1295 if field.get('views'):
1296 for view in field["views"].itervalues():
1297 self.process_view(session, view, None, transform)
1298 if field.get('domain'):
1299 field["domain"] = parse_domain(field["domain"], session)
1300 if field.get('context'):
1301 field["context"] = parse_context(field["context"], session)
1303 def process_toolbar(self, req, toolbar):
1305 The toolbar is a mapping of section_key: [action_descriptor]
1307 We need to clean all those actions in order to ensure correct
1310 for actions in toolbar.itervalues():
1311 for action in actions:
1312 if 'context' in action:
1313 action['context'] = parse_context(
1314 action['context'], req.session)
1315 if 'domain' in action:
1316 action['domain'] = parse_domain(
1317 action['domain'], req.session)
1319 @openerpweb.jsonrequest
1320 def add_custom(self, req, view_id, arch):
1321 CustomView = req.session.model('ir.ui.view.custom')
1323 'user_id': req.session._uid,
1326 }, req.session.eval_context(req.context))
1327 return {'result': True}
1329 @openerpweb.jsonrequest
1330 def undo_custom(self, req, view_id, reset=False):
1331 CustomView = req.session.model('ir.ui.view.custom')
1332 context = req.session.eval_context(req.context)
1333 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1334 0, False, False, context)
1337 CustomView.unlink(vcustom, context)
1339 CustomView.unlink([vcustom[0]], context)
1340 return {'result': True}
1341 return {'result': False}
1343 def transform_view(self, view_string, session, context=None):
1344 # transform nodes on the fly via iterparse, instead of
1345 # doing it statically on the parsing result
1346 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1348 for event, elem in parser:
1349 if event == "start":
1352 self.parse_domains_and_contexts(elem, session)
1355 def parse_domains_and_contexts(self, elem, session):
1356 """ Converts domains and contexts from the view into Python objects,
1357 either literals if they can be parsed by literal_eval or a special
1358 placeholder object if the domain or context refers to free variables.
1360 :param elem: the current node being parsed
1361 :type param: xml.etree.ElementTree.Element
1362 :param session: OpenERP session object, used to store and retrieve
1364 :type session: openerpweb.openerpweb.OpenERPSession
1366 for el in ['domain', 'filter_domain']:
1367 domain = elem.get(el, '').strip()
1369 elem.set(el, parse_domain(domain, session))
1370 elem.set(el + '_string', domain)
1371 for el in ['context', 'default_get']:
1372 context_string = elem.get(el, '').strip()
1374 elem.set(el, parse_context(context_string, session))
1375 elem.set(el + '_string', context_string)
1377 @openerpweb.jsonrequest
1378 def load(self, req, model, view_id, view_type, toolbar=False):
1379 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1381 class TreeView(View):
1382 _cp_path = "/web/treeview"
1384 @openerpweb.jsonrequest
1385 def action(self, req, model, id):
1386 return load_actions_from_ir_values(
1387 req,'action', 'tree_but_open',[(model, id)],
1390 class SearchView(View):
1391 _cp_path = "/web/searchview"
1393 @openerpweb.jsonrequest
1394 def load(self, req, model, view_id):
1395 fields_view = self.fields_view_get(req, model, view_id, 'search')
1396 return {'fields_view': fields_view}
1398 @openerpweb.jsonrequest
1399 def fields_get(self, req, model):
1400 Model = req.session.model(model)
1401 fields = Model.fields_get(False, req.session.eval_context(req.context))
1402 for field in fields.values():
1403 # shouldn't convert the views too?
1404 if field.get('domain'):
1405 field["domain"] = parse_domain(field["domain"], req.session)
1406 if field.get('context'):
1407 field["context"] = parse_context(field["context"], req.session)
1408 return {'fields': fields}
1410 @openerpweb.jsonrequest
1411 def get_filters(self, req, model):
1412 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1413 Model = req.session.model("ir.filters")
1414 filters = Model.get_filters(model)
1415 for filter in filters:
1417 parsed_context = parse_context(filter["context"], req.session)
1418 filter["context"] = (parsed_context
1419 if not isinstance(parsed_context, common.nonliterals.BaseContext)
1420 else req.session.eval_context(parsed_context))
1422 parsed_domain = parse_domain(filter["domain"], req.session)
1423 filter["domain"] = (parsed_domain
1424 if not isinstance(parsed_domain, common.nonliterals.BaseDomain)
1425 else req.session.eval_domain(parsed_domain))
1427 logger.exception("Failed to parse custom filter %s in %s",
1428 filter['name'], model)
1429 filter['disabled'] = True
1430 del filter['context']
1431 del filter['domain']
1434 class Binary(openerpweb.Controller):
1435 _cp_path = "/web/binary"
1437 @openerpweb.httprequest
1438 def image(self, req, model, id, field, **kw):
1439 last_update = '__last_update'
1440 Model = req.session.model(model)
1441 context = req.session.eval_context(req.context)
1442 headers = [('Content-Type', 'image/png')]
1443 etag = req.httprequest.headers.get('If-None-Match')
1444 hashed_session = hashlib.md5(req.session_id).hexdigest()
1445 id = None if not id else simplejson.loads(id)
1446 if type(id) is list:
1449 if not id and hashed_session == etag:
1450 return werkzeug.wrappers.Response(status=304)
1452 date = Model.read([id], [last_update], context)[0].get(last_update)
1453 if hashlib.md5(date).hexdigest() == etag:
1454 return werkzeug.wrappers.Response(status=304)
1456 retag = hashed_session
1459 res = Model.default_get([field], context).get(field)
1460 image_data = base64.b64decode(res)
1462 res = Model.read([id], [last_update, field], context)[0]
1463 retag = hashlib.md5(res.get(last_update)).hexdigest()
1464 image_data = base64.b64decode(res.get(field))
1465 except (TypeError, xmlrpclib.Fault):
1466 image_data = self.placeholder(req)
1467 headers.append(('ETag', retag))
1468 headers.append(('Content-Length', len(image_data)))
1470 ncache = int(kw.get('cache'))
1471 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1474 return req.make_response(image_data, headers)
1475 def placeholder(self, req):
1476 addons_path = openerpweb.addons_manifest['web']['addons_path']
1477 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1478 def content_disposition(self, filename, req):
1479 filename = filename.encode('utf8')
1480 escaped = urllib2.quote(filename)
1481 browser = req.httprequest.user_agent.browser
1482 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
1483 if browser == 'msie' and version < 9:
1484 return "attachment; filename=%s" % escaped
1485 elif browser == 'safari':
1486 return "attachment; filename=%s" % filename
1488 return "attachment; filename*=UTF-8''%s" % escaped
1490 @openerpweb.httprequest
1491 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1492 """ Download link for files stored as binary fields.
1494 If the ``id`` parameter is omitted, fetches the default value for the
1495 binary field (via ``default_get``), otherwise fetches the field for
1496 that precise record.
1498 :param req: OpenERP request
1499 :type req: :class:`web.common.http.HttpRequest`
1500 :param str model: name of the model to fetch the binary from
1501 :param str field: binary field
1502 :param str id: id of the record from which to fetch the binary
1503 :param str filename_field: field holding the file's name, if any
1504 :returns: :class:`werkzeug.wrappers.Response`
1506 Model = req.session.model(model)
1507 context = req.session.eval_context(req.context)
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 return req.not_found()
1519 filename = '%s_%s' % (model.replace('.', '_'), id)
1521 filename = res.get(filename_field, '') or filename
1522 return req.make_response(filecontent,
1523 [('Content-Type', 'application/octet-stream'),
1524 ('Content-Disposition', self.content_disposition(filename, req))])
1526 @openerpweb.httprequest
1527 def saveas_ajax(self, req, data, token):
1528 jdata = simplejson.loads(data)
1529 model = jdata['model']
1530 field = jdata['field']
1531 id = jdata.get('id', None)
1532 filename_field = jdata.get('filename_field', None)
1533 context = jdata.get('context', dict())
1535 context = req.session.eval_context(context)
1536 Model = req.session.model(model)
1539 fields.append(filename_field)
1541 res = Model.read([int(id)], fields, context)[0]
1543 res = Model.default_get(fields, context)
1544 filecontent = base64.b64decode(res.get(field, ''))
1546 raise ValueError("No content found for field '%s' on '%s:%s'" %
1549 filename = '%s_%s' % (model.replace('.', '_'), id)
1551 filename = res.get(filename_field, '') or filename
1552 return req.make_response(filecontent,
1553 headers=[('Content-Type', 'application/octet-stream'),
1554 ('Content-Disposition', self.content_disposition(filename, req))],
1555 cookies={'fileToken': int(token)})
1557 @openerpweb.httprequest
1558 def upload(self, req, callback, ufile):
1559 # TODO: might be useful to have a configuration flag for max-length file uploads
1561 out = """<script language="javascript" type="text/javascript">
1562 var win = window.top.window;
1563 win.jQuery(win).trigger(%s, %s);
1566 args = [len(data), ufile.filename,
1567 ufile.content_type, base64.b64encode(data)]
1568 except Exception, e:
1569 args = [False, e.message]
1570 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1572 @openerpweb.httprequest
1573 def upload_attachment(self, req, callback, model, id, ufile):
1574 context = req.session.eval_context(req.context)
1575 Model = req.session.model('ir.attachment')
1577 out = """<script language="javascript" type="text/javascript">
1578 var win = window.top.window;
1579 win.jQuery(win).trigger(%s, %s);
1581 attachment_id = Model.create({
1582 'name': ufile.filename,
1583 'datas': base64.encodestring(ufile.read()),
1584 'datas_fname': ufile.filename,
1589 'filename': ufile.filename,
1592 except Exception, e:
1593 args = { 'error': e.message }
1594 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1596 class Action(openerpweb.Controller):
1597 _cp_path = "/web/action"
1599 @openerpweb.jsonrequest
1600 def load(self, req, action_id, do_not_eval=False):
1601 Actions = req.session.model('ir.actions.actions')
1603 context = req.session.eval_context(req.context)
1606 action_id = int(action_id)
1609 module, xmlid = action_id.split('.', 1)
1610 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1611 assert model.startswith('ir.actions.')
1613 action_id = 0 # force failed read
1615 base_action = Actions.read([action_id], ['type'], context)
1618 action_type = base_action[0]['type']
1619 if action_type == 'ir.actions.report.xml':
1620 ctx.update({'bin_size': True})
1622 action = req.session.model(action_type).read([action_id], False, ctx)
1624 value = clean_action(req, action[0], do_not_eval)
1627 @openerpweb.jsonrequest
1628 def run(self, req, action_id):
1629 return_action = req.session.model('ir.actions.server').run(
1630 [action_id], req.session.eval_context(req.context))
1632 return clean_action(req, return_action)
1637 _cp_path = "/web/export"
1639 @openerpweb.jsonrequest
1640 def formats(self, req):
1641 """ Returns all valid export formats
1643 :returns: for each export format, a pair of identifier and printable name
1644 :rtype: [(str, str)]
1648 for path, controller in openerpweb.controllers_path.iteritems()
1649 if path.startswith(self._cp_path)
1650 if hasattr(controller, 'fmt')
1651 ], key=operator.itemgetter("label"))
1653 def fields_get(self, req, model):
1654 Model = req.session.model(model)
1655 fields = Model.fields_get(False, req.session.eval_context(req.context))
1658 @openerpweb.jsonrequest
1659 def get_fields(self, req, model, prefix='', parent_name= '',
1660 import_compat=True, parent_field_type=None,
1663 if import_compat and parent_field_type == "many2one":
1666 fields = self.fields_get(req, model)
1669 fields.pop('id', None)
1671 fields['.id'] = fields.pop('id', {'string': 'ID'})
1673 fields_sequence = sorted(fields.iteritems(),
1674 key=lambda field: field[1].get('string', ''))
1677 for field_name, field in fields_sequence:
1679 if exclude and field_name in exclude:
1681 if field.get('readonly'):
1682 # If none of the field's states unsets readonly, skip the field
1683 if all(dict(attrs).get('readonly', True)
1684 for attrs in field.get('states', {}).values()):
1687 id = prefix + (prefix and '/'or '') + field_name
1688 name = parent_name + (parent_name and '/' or '') + field['string']
1689 record = {'id': id, 'string': name,
1690 'value': id, 'children': False,
1691 'field_type': field.get('type'),
1692 'required': field.get('required'),
1693 'relation_field': field.get('relation_field')}
1694 records.append(record)
1696 if len(name.split('/')) < 3 and 'relation' in field:
1697 ref = field.pop('relation')
1698 record['value'] += '/id'
1699 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1701 if not import_compat or field['type'] == 'one2many':
1702 # m2m field in import_compat is childless
1703 record['children'] = True
1707 @openerpweb.jsonrequest
1708 def namelist(self,req, model, export_id):
1709 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1710 export = req.session.model("ir.exports").read([export_id])[0]
1711 export_fields_list = req.session.model("ir.exports.line").read(
1712 export['export_fields'])
1714 fields_data = self.fields_info(
1715 req, model, map(operator.itemgetter('name'), export_fields_list))
1718 {'name': field['name'], 'label': fields_data[field['name']]}
1719 for field in export_fields_list
1722 def fields_info(self, req, model, export_fields):
1724 fields = self.fields_get(req, model)
1726 # To make fields retrieval more efficient, fetch all sub-fields of a
1727 # given field at the same time. Because the order in the export list is
1728 # arbitrary, this requires ordering all sub-fields of a given field
1729 # together so they can be fetched at the same time
1731 # Works the following way:
1732 # * sort the list of fields to export, the default sorting order will
1733 # put the field itself (if present, for xmlid) and all of its
1734 # sub-fields right after it
1735 # * then, group on: the first field of the path (which is the same for
1736 # a field and for its subfields and the length of splitting on the
1737 # first '/', which basically means grouping the field on one side and
1738 # all of the subfields on the other. This way, we have the field (for
1739 # the xmlid) with length 1, and all of the subfields with the same
1740 # base but a length "flag" of 2
1741 # * if we have a normal field (length 1), just add it to the info
1742 # mapping (with its string) as-is
1743 # * otherwise, recursively call fields_info via graft_subfields.
1744 # all graft_subfields does is take the result of fields_info (on the
1745 # field's model) and prepend the current base (current field), which
1746 # rebuilds the whole sub-tree for the field
1748 # result: because we're not fetching the fields_get for half the
1749 # database models, fetching a namelist with a dozen fields (including
1750 # relational data) falls from ~6s to ~300ms (on the leads model).
1751 # export lists with no sub-fields (e.g. import_compatible lists with
1752 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1753 # there's a single fields_get to execute)
1754 for (base, length), subfields in itertools.groupby(
1755 sorted(export_fields),
1756 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1757 subfields = list(subfields)
1759 # subfields is a seq of $base/*rest, and not loaded yet
1760 info.update(self.graft_subfields(
1761 req, fields[base]['relation'], base, fields[base]['string'],
1765 info[base] = fields[base]['string']
1769 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1770 export_fields = [field.split('/', 1)[1] for field in fields]
1772 (prefix + '/' + k, prefix_string + '/' + v)
1773 for k, v in self.fields_info(req, model, export_fields).iteritems())
1775 #noinspection PyPropertyDefinition
1777 def content_type(self):
1778 """ Provides the format's content type """
1779 raise NotImplementedError()
1781 def filename(self, base):
1782 """ Creates a valid filename for the format (with extension) from the
1783 provided base name (exension-less)
1785 raise NotImplementedError()
1787 def from_data(self, fields, rows):
1788 """ Conversion method from OpenERP's export data to whatever the
1789 current export class outputs
1791 :params list fields: a list of fields to export
1792 :params list rows: a list of records to export
1796 raise NotImplementedError()
1798 @openerpweb.httprequest
1799 def index(self, req, data, token):
1800 model, fields, ids, domain, import_compat = \
1801 operator.itemgetter('model', 'fields', 'ids', 'domain',
1803 simplejson.loads(data))
1805 context = req.session.eval_context(req.context)
1806 Model = req.session.model(model)
1807 ids = ids or Model.search(domain, 0, False, False, context)
1809 field_names = map(operator.itemgetter('name'), fields)
1810 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1813 columns_headers = field_names
1815 columns_headers = [val['label'].strip() for val in fields]
1818 return req.make_response(self.from_data(columns_headers, import_data),
1819 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1820 ('Content-Type', self.content_type)],
1821 cookies={'fileToken': int(token)})
1823 class CSVExport(Export):
1824 _cp_path = '/web/export/csv'
1825 fmt = {'tag': 'csv', 'label': 'CSV'}
1828 def content_type(self):
1829 return 'text/csv;charset=utf8'
1831 def filename(self, base):
1832 return base + '.csv'
1834 def from_data(self, fields, rows):
1836 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1838 writer.writerow([name.encode('utf-8') for name in fields])
1843 if isinstance(d, basestring):
1844 d = d.replace('\n',' ').replace('\t',' ')
1846 d = d.encode('utf-8')
1847 except UnicodeError:
1849 if d is False: d = None
1851 writer.writerow(row)
1858 class ExcelExport(Export):
1859 _cp_path = '/web/export/xls'
1863 'error': None if xlwt else "XLWT required"
1867 def content_type(self):
1868 return 'application/vnd.ms-excel'
1870 def filename(self, base):
1871 return base + '.xls'
1873 def from_data(self, fields, rows):
1874 workbook = xlwt.Workbook()
1875 worksheet = workbook.add_sheet('Sheet 1')
1877 for i, fieldname in enumerate(fields):
1878 worksheet.write(0, i, fieldname)
1879 worksheet.col(i).width = 8000 # around 220 pixels
1881 style = xlwt.easyxf('align: wrap yes')
1883 for row_index, row in enumerate(rows):
1884 for cell_index, cell_value in enumerate(row):
1885 if isinstance(cell_value, basestring):
1886 cell_value = re.sub("\r", " ", cell_value)
1887 if cell_value is False: cell_value = None
1888 worksheet.write(row_index + 1, cell_index, cell_value, style)
1897 class Reports(View):
1898 _cp_path = "/web/report"
1899 POLLING_DELAY = 0.25
1901 'doc': 'application/vnd.ms-word',
1902 'html': 'text/html',
1903 'odt': 'application/vnd.oasis.opendocument.text',
1904 'pdf': 'application/pdf',
1905 'sxw': 'application/vnd.sun.xml.writer',
1906 'xls': 'application/vnd.ms-excel',
1909 @openerpweb.httprequest
1910 def index(self, req, action, token):
1911 action = simplejson.loads(action)
1913 report_srv = req.session.proxy("report")
1914 context = req.session.eval_context(
1915 common.nonliterals.CompoundContext(
1916 req.context or {}, action[ "context"]))
1919 report_ids = context["active_ids"]
1920 if 'report_type' in action:
1921 report_data['report_type'] = action['report_type']
1922 if 'datas' in action:
1923 if 'ids' in action['datas']:
1924 report_ids = action['datas'].pop('ids')
1925 report_data.update(action['datas'])
1927 report_id = report_srv.report(
1928 req.session._db, req.session._uid, req.session._password,
1929 action["report_name"], report_ids,
1930 report_data, context)
1932 report_struct = None
1934 report_struct = report_srv.report_get(
1935 req.session._db, req.session._uid, req.session._password, report_id)
1936 if report_struct["state"]:
1939 time.sleep(self.POLLING_DELAY)
1941 report = base64.b64decode(report_struct['result'])
1942 if report_struct.get('code') == 'zlib':
1943 report = zlib.decompress(report)
1944 report_mimetype = self.TYPES_MAPPING.get(
1945 report_struct['format'], 'octet-stream')
1946 file_name = action.get('name', 'report')
1947 if 'name' not in action:
1948 reports = req.session.model('ir.actions.report.xml')
1949 res_id = reports.search([('report_name', '=', action['report_name']),],
1950 0, False, False, context)
1952 file_name = reports.read(res_id[0], ['name'], context)['name']
1954 file_name = action['report_name']
1956 return req.make_response(report,
1958 # maybe we should take of what characters can appear in a file name?
1959 ('Content-Disposition', 'attachment; filename="%s.%s"' % (file_name, report_struct['format'])),
1960 ('Content-Type', report_mimetype),
1961 ('Content-Length', len(report))],
1962 cookies={'fileToken': int(token)})
1965 _cp_path = "/web/import"
1967 def fields_get(self, req, model):
1968 Model = req.session.model(model)
1969 fields = Model.fields_get(False, req.session.eval_context(req.context))
1972 @openerpweb.httprequest
1973 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1975 data = list(csv.reader(
1976 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1977 except csv.Error, e:
1979 return '<script>window.top.%s(%s);</script>' % (
1980 jsonp, simplejson.dumps({'error': {
1981 'message': 'Error parsing CSV file: %s' % e,
1982 # decodes each byte to a unicode character, which may or
1983 # may not be printable, but decoding will succeed.
1984 # Otherwise simplejson will try to decode the `str` using
1985 # utf-8, which is very likely to blow up on characters out
1986 # of the ascii range (in range [128, 256))
1987 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1990 return '<script>window.top.%s(%s);</script>' % (
1991 jsonp, simplejson.dumps(
1992 {'records': data[:10]}, encoding=csvcode))
1993 except UnicodeDecodeError:
1994 return '<script>window.top.%s(%s);</script>' % (
1995 jsonp, simplejson.dumps({
1996 'message': u"Failed to decode CSV file using encoding %s, "
1997 u"try switching to a different encoding" % csvcode
2000 @openerpweb.httprequest
2001 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
2003 modle_obj = req.session.model(model)
2004 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
2005 simplejson.loads(meta))
2008 if not (csvdel and len(csvdel) == 1):
2009 error = u"The CSV delimiter must be a single character"
2011 if not indices and fields:
2012 error = u"You must select at least one field to import"
2015 return '<script>window.top.%s(%s);</script>' % (
2016 jsonp, simplejson.dumps({'error': {'message': error}}))
2018 # skip ignored records (@skip parameter)
2019 # then skip empty lines (not valid csv)
2020 # nb: should these operations be reverted?
2021 rows_to_import = itertools.ifilter(
2024 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
2027 # if only one index, itemgetter will return an atom rather than a tuple
2028 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
2029 else: mapper = operator.itemgetter(*indices)
2034 # decode each data row
2036 [record.decode(csvcode) for record in row]
2037 for row in itertools.imap(mapper, rows_to_import)
2038 # don't insert completely empty rows (can happen due to fields
2039 # filtering in case of e.g. o2m content rows)
2042 except UnicodeDecodeError:
2043 error = u"Failed to decode CSV file using encoding %s" % csvcode
2044 except csv.Error, e:
2045 error = u"Could not process CSV file: %s" % e
2047 # If the file contains nothing,
2049 error = u"File to import is empty"
2051 return '<script>window.top.%s(%s);</script>' % (
2052 jsonp, simplejson.dumps({'error': {'message': error}}))
2055 (code, record, message, _nope) = modle_obj.import_data(
2056 fields, data, 'init', '', False,
2057 req.session.eval_context(req.context))
2058 except xmlrpclib.Fault, e:
2059 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
2060 return '<script>window.top.%s(%s);</script>' % (
2061 jsonp, simplejson.dumps({'error':error}))
2064 return '<script>window.top.%s(%s);</script>' % (
2065 jsonp, simplejson.dumps({'success':True}))
2067 msg = u"Error during import: %s\n\nTrying to import record %r" % (
2069 return '<script>window.top.%s(%s);</script>' % (
2070 jsonp, simplejson.dumps({'error': {'message':msg}}))
2072 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: