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('-',1)[0], lang]
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 start = datetime.datetime.now()
713 ir_translation = req.session.model('ir.translation')
714 translations_per_module = {}
715 messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
716 ('comments','like','openerp-web'),('value','!=',False),
718 ['module','src','value','lang'], order='module')
719 for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
720 translations_per_module.setdefault(mod,{'messages':[]})
721 translations_per_module[mod]['messages'].extend({'id': m['src'],
722 'string': m['value']} \
725 print "Loaded translations in ", datetime.datetime.now()-start
727 pprint.pprint(translations_per_module)
729 return {"modules": translations_per_module,
730 "lang_parameters": lang_params}
732 @openerpweb.jsonrequest
733 def version_info(self, req):
735 "version": common.release.version
738 class Proxy(openerpweb.Controller):
739 _cp_path = '/web/proxy'
741 @openerpweb.jsonrequest
742 def load(self, req, path):
743 """ Proxies an HTTP request through a JSON request.
745 It is strongly recommended to not request binary files through this,
746 as the result will be a binary data blob as well.
748 :param req: OpenERP request
749 :param path: actual request path
750 :return: file content
752 from werkzeug.test import Client
753 from werkzeug.wrappers import BaseResponse
755 return Client(req.httprequest.app, BaseResponse).get(path).data
757 class Database(openerpweb.Controller):
758 _cp_path = "/web/database"
760 @openerpweb.jsonrequest
761 def get_list(self, req):
763 return {"db_list": dbs}
765 @openerpweb.jsonrequest
766 def create(self, req, fields):
767 params = dict(map(operator.itemgetter('name', 'value'), fields))
769 params['super_admin_pwd'],
771 bool(params.get('demo_data')),
773 params['create_admin_pwd']
776 return req.session.proxy("db").create_database(*create_attrs)
778 @openerpweb.jsonrequest
779 def drop(self, req, fields):
780 password, db = operator.itemgetter(
781 'drop_pwd', 'drop_db')(
782 dict(map(operator.itemgetter('name', 'value'), fields)))
785 return req.session.proxy("db").drop(password, db)
786 except xmlrpclib.Fault, e:
787 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
788 return {'error': e.faultCode, 'title': 'Drop Database'}
789 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
791 @openerpweb.httprequest
792 def backup(self, req, backup_db, backup_pwd, token):
794 db_dump = base64.b64decode(
795 req.session.proxy("db").dump(backup_pwd, backup_db))
796 filename = "%(db)s_%(timestamp)s.dump" % {
798 'timestamp': datetime.datetime.utcnow().strftime(
799 "%Y-%m-%d_%H-%M-%SZ")
801 return req.make_response(db_dump,
802 [('Content-Type', 'application/octet-stream; charset=binary'),
803 ('Content-Disposition', 'attachment; filename="' + filename + '"')],
804 {'fileToken': int(token)}
806 except xmlrpclib.Fault, e:
807 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
809 @openerpweb.httprequest
810 def restore(self, req, db_file, restore_pwd, new_db):
812 data = base64.b64encode(db_file.read())
813 req.session.proxy("db").restore(restore_pwd, new_db, data)
815 except xmlrpclib.Fault, e:
816 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
817 raise Exception("AccessDenied")
819 @openerpweb.jsonrequest
820 def change_password(self, req, fields):
821 old_password, new_password = operator.itemgetter(
822 'old_pwd', 'new_pwd')(
823 dict(map(operator.itemgetter('name', 'value'), fields)))
825 return req.session.proxy("db").change_admin_password(old_password, new_password)
826 except xmlrpclib.Fault, e:
827 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
828 return {'error': e.faultCode, 'title': 'Change Password'}
829 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
831 class Session(openerpweb.Controller):
832 _cp_path = "/web/session"
834 def session_info(self, req):
835 req.session.ensure_valid()
837 "session_id": req.session_id,
838 "uid": req.session._uid,
839 "context": req.session.get_context() if req.session._uid else {},
840 "db": req.session._db,
841 "login": req.session._login,
844 @openerpweb.jsonrequest
845 def get_session_info(self, req):
846 return self.session_info(req)
848 @openerpweb.jsonrequest
849 def authenticate(self, req, db, login, password, base_location=None):
850 wsgienv = req.httprequest.environ
851 release = common.release
853 base_location=base_location,
854 HTTP_HOST=wsgienv['HTTP_HOST'],
855 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
856 user_agent="%s / %s" % (release.name, release.version),
858 req.session.authenticate(db, login, password, env)
860 return self.session_info(req)
862 @openerpweb.jsonrequest
863 def change_password (self,req,fields):
864 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
865 dict(map(operator.itemgetter('name', 'value'), fields)))
866 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
867 return {'error':'All passwords have to be filled.','title': 'Change Password'}
868 if new_password != confirm_password:
869 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
871 if req.session.model('res.users').change_password(
872 old_password, new_password):
873 return {'new_password':new_password}
875 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
876 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
878 @openerpweb.jsonrequest
879 def sc_list(self, req):
880 return req.session.model('ir.ui.view_sc').get_sc(
881 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
883 @openerpweb.jsonrequest
884 def get_lang_list(self, req):
887 'lang_list': (req.session.proxy("db").list_lang() or []),
891 return {"error": e, "title": "Languages"}
893 @openerpweb.jsonrequest
894 def modules(self, req):
895 # return all installed modules. Web client is smart enough to not load a module twice
896 return module_installed(req)
898 @openerpweb.jsonrequest
899 def eval_domain_and_context(self, req, contexts, domains,
901 """ Evaluates sequences of domains and contexts, composing them into
902 a single context, domain or group_by sequence.
904 :param list contexts: list of contexts to merge together. Contexts are
905 evaluated in sequence, all previous contexts
906 are part of their own evaluation context
907 (starting at the session context).
908 :param list domains: list of domains to merge together. Domains are
909 evaluated in sequence and appended to one another
910 (implicit AND), their evaluation domain is the
911 result of merging all contexts.
912 :param list group_by_seq: list of domains (which may be in a different
913 order than the ``contexts`` parameter),
914 evaluated in sequence, their ``'group_by'``
915 key is extracted if they have one.
920 the global context created by merging all of
924 the concatenation of all domains
927 a list of fields to group by, potentially empty (in which case
928 no group by should be performed)
930 context, domain = eval_context_and_domain(req.session,
931 common.nonliterals.CompoundContext(*(contexts or [])),
932 common.nonliterals.CompoundDomain(*(domains or [])))
934 group_by_sequence = []
935 for candidate in (group_by_seq or []):
936 ctx = req.session.eval_context(candidate, context)
937 group_by = ctx.get('group_by')
940 elif isinstance(group_by, basestring):
941 group_by_sequence.append(group_by)
943 group_by_sequence.extend(group_by)
948 'group_by': group_by_sequence
951 @openerpweb.jsonrequest
952 def save_session_action(self, req, the_action):
954 This method store an action object in the session object and returns an integer
955 identifying that action. The method get_session_action() can be used to get
958 :param the_action: The action to save in the session.
959 :type the_action: anything
960 :return: A key identifying the saved action.
963 saved_actions = req.httpsession.get('saved_actions')
964 if not saved_actions:
965 saved_actions = {"next":0, "actions":{}}
966 req.httpsession['saved_actions'] = saved_actions
967 # we don't allow more than 10 stored actions
968 if len(saved_actions["actions"]) >= 10:
969 del saved_actions["actions"][min(saved_actions["actions"])]
970 key = saved_actions["next"]
971 saved_actions["actions"][key] = the_action
972 saved_actions["next"] = key + 1
975 @openerpweb.jsonrequest
976 def get_session_action(self, req, key):
978 Gets back a previously saved action. This method can return None if the action
979 was saved since too much time (this case should be handled in a smart way).
981 :param key: The key given by save_session_action()
983 :return: The saved action or None.
986 saved_actions = req.httpsession.get('saved_actions')
987 if not saved_actions:
989 return saved_actions["actions"].get(key)
991 @openerpweb.jsonrequest
992 def check(self, req):
993 req.session.assert_valid()
996 @openerpweb.jsonrequest
997 def destroy(self, req):
998 req.session._suicide = True
1000 class Menu(openerpweb.Controller):
1001 _cp_path = "/web/menu"
1003 @openerpweb.jsonrequest
1004 def load(self, req):
1005 return {'data': self.do_load(req)}
1007 @openerpweb.jsonrequest
1008 def get_user_roots(self, req):
1009 return self.do_get_user_roots(req)
1011 def do_get_user_roots(self, req):
1012 """ Return all root menu ids visible for the session user.
1014 :param req: A request object, with an OpenERP session attribute
1015 :type req: < session -> OpenERPSession >
1016 :return: the root menu ids
1020 context = s.eval_context(req.context)
1021 Menus = s.model('ir.ui.menu')
1022 # If a menu action is defined use its domain to get the root menu items
1023 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1025 menu_domain = [('parent_id', '=', False)]
1027 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1029 menu_domain = ast.literal_eval(domain_string)
1031 return Menus.search(menu_domain, 0, False, False, context)
1033 def do_load(self, req):
1034 """ Loads all menu items (all applications and their sub-menus).
1036 :param req: A request object, with an OpenERP session attribute
1037 :type req: < session -> OpenERPSession >
1038 :return: the menu root
1039 :rtype: dict('children': menu_nodes)
1041 context = req.session.eval_context(req.context)
1042 Menus = req.session.model('ir.ui.menu')
1044 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1045 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1047 # menus are loaded fully unlike a regular tree view, cause there are a
1048 # limited number of items (752 when all 6.1 addons are installed)
1049 menu_ids = Menus.search([], 0, False, False, context)
1050 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1051 # adds roots at the end of the sequence, so that they will overwrite
1052 # equivalent menu items from full menu read when put into id:item
1053 # mapping, resulting in children being correctly set on the roots.
1054 menu_items.extend(menu_roots)
1056 # make a tree using parent_id
1057 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1058 for menu_item in menu_items:
1059 if menu_item['parent_id']:
1060 parent = menu_item['parent_id'][0]
1063 if parent in menu_items_map:
1064 menu_items_map[parent].setdefault(
1065 'children', []).append(menu_item)
1067 # sort by sequence a tree using parent_id
1068 for menu_item in menu_items:
1069 menu_item.setdefault('children', []).sort(
1070 key=operator.itemgetter('sequence'))
1074 @openerpweb.jsonrequest
1075 def action(self, req, menu_id):
1076 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1077 [('ir.ui.menu', menu_id)], False)
1078 return {"action": actions}
1080 class DataSet(openerpweb.Controller):
1081 _cp_path = "/web/dataset"
1083 @openerpweb.jsonrequest
1084 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1085 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1086 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1088 """ Performs a search() followed by a read() (if needed) using the
1089 provided search criteria
1091 :param req: a JSON-RPC request object
1092 :type req: openerpweb.JsonRequest
1093 :param str model: the name of the model to search on
1094 :param fields: a list of the fields to return in the result records
1096 :param int offset: from which index should the results start being returned
1097 :param int limit: the maximum number of records to return
1098 :param list domain: the search domain for the query
1099 :param list sort: sorting directives
1100 :returns: A structure (dict) with two keys: ids (all the ids matching
1101 the (domain, context) pair) and records (paginated records
1102 matching fields selection set)
1105 Model = req.session.model(model)
1107 context, domain = eval_context_and_domain(
1108 req.session, req.context, domain)
1110 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1111 if limit and len(ids) == limit:
1112 length = Model.search_count(domain, context)
1114 length = len(ids) + (offset or 0)
1115 if fields and fields == ['id']:
1116 # shortcut read if we only want the ids
1119 'records': [{'id': id} for id in ids]
1122 records = Model.read(ids, fields or False, context)
1123 records.sort(key=lambda obj: ids.index(obj['id']))
1129 @openerpweb.jsonrequest
1130 def load(self, req, model, id, fields):
1131 m = req.session.model(model)
1133 r = m.read([id], False, req.session.eval_context(req.context))
1136 return {'value': value}
1138 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1139 has_domain = domain_id is not None and domain_id < len(args)
1140 has_context = context_id is not None and context_id < len(args)
1142 domain = args[domain_id] if has_domain else []
1143 context = args[context_id] if has_context else {}
1144 c, d = eval_context_and_domain(req.session, context, domain)
1148 args[context_id] = c
1150 return self._call_kw(req, model, method, args, {})
1152 def _call_kw(self, req, model, method, args, kwargs):
1153 for i in xrange(len(args)):
1154 if isinstance(args[i], common.nonliterals.BaseContext):
1155 args[i] = req.session.eval_context(args[i])
1156 elif isinstance(args[i], common.nonliterals.BaseDomain):
1157 args[i] = req.session.eval_domain(args[i])
1158 for k in kwargs.keys():
1159 if isinstance(kwargs[k], common.nonliterals.BaseContext):
1160 kwargs[k] = req.session.eval_context(kwargs[k])
1161 elif isinstance(kwargs[k], common.nonliterals.BaseDomain):
1162 kwargs[k] = req.session.eval_domain(kwargs[k])
1164 # Temporary implements future display_name special field for model#read()
1165 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1166 if 'display_name' in args[1]:
1167 names = req.session.model(model).name_get(args[0], **kwargs)
1168 args[1].remove('display_name')
1169 r = getattr(req.session.model(model), method)(*args, **kwargs)
1170 for i in range(len(r)):
1171 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1174 return getattr(req.session.model(model), method)(*args, **kwargs)
1176 @openerpweb.jsonrequest
1177 def onchange(self, req, model, method, args, context_id=None):
1178 """ Support method for handling onchange calls: behaves much like call
1179 with the following differences:
1181 * Does not take a domain_id
1182 * Is aware of the return value's structure, and will parse the domains
1183 if needed in order to return either parsed literal domains (in JSON)
1184 or non-literal domain instances, allowing those domains to be used
1188 :type req: web.common.http.JsonRequest
1189 :param str model: object type on which to call the method
1190 :param str method: name of the onchange handler method
1191 :param list args: arguments to call the onchange handler with
1192 :param int context_id: index of the context object in the list of
1194 :return: result of the onchange call with all domains parsed
1196 result = self.call_common(req, model, method, args, context_id=context_id)
1197 if not result or 'domain' not in result:
1200 result['domain'] = dict(
1201 (k, parse_domain(v, req.session))
1202 for k, v in result['domain'].iteritems())
1206 @openerpweb.jsonrequest
1207 def call(self, req, model, method, args, domain_id=None, context_id=None):
1208 return self.call_common(req, model, method, args, domain_id, context_id)
1210 @openerpweb.jsonrequest
1211 def call_kw(self, req, model, method, args, kwargs):
1212 return self._call_kw(req, model, method, args, kwargs)
1214 @openerpweb.jsonrequest
1215 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1216 action = self.call_common(req, model, method, args, domain_id, context_id)
1217 if isinstance(action, dict) and action.get('type') != '':
1218 return {'result': clean_action(req, action)}
1219 return {'result': False}
1221 @openerpweb.jsonrequest
1222 def exec_workflow(self, req, model, id, signal):
1223 return req.session.exec_workflow(model, id, signal)
1225 @openerpweb.jsonrequest
1226 def resequence(self, req, model, ids, field='sequence', offset=0):
1227 """ Re-sequences a number of records in the model, by their ids
1229 The re-sequencing starts at the first model of ``ids``, the sequence
1230 number is incremented by one after each record and starts at ``offset``
1232 :param ids: identifiers of the records to resequence, in the new sequence order
1234 :param str field: field used for sequence specification, defaults to
1236 :param int offset: sequence number for first record in ``ids``, allows
1237 starting the resequencing from an arbitrary number,
1240 m = req.session.model(model)
1241 if not m.fields_get([field]):
1243 # python 2.6 has no start parameter
1244 for i, id in enumerate(ids):
1245 m.write(id, { field: i + offset })
1248 class DataGroup(openerpweb.Controller):
1249 _cp_path = "/web/group"
1250 @openerpweb.jsonrequest
1251 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1252 Model = req.session.model(model)
1253 context, domain = eval_context_and_domain(req.session, req.context, domain)
1255 return Model.read_group(
1256 domain or [], fields, group_by_fields, 0, False,
1257 dict(context, group_by=group_by_fields), sort or False)
1259 class View(openerpweb.Controller):
1260 _cp_path = "/web/view"
1262 def fields_view_get(self, req, model, view_id, view_type,
1263 transform=True, toolbar=False, submenu=False):
1264 Model = req.session.model(model)
1265 context = req.session.eval_context(req.context)
1266 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1267 # todo fme?: check that we should pass the evaluated context here
1268 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1269 if toolbar and transform:
1270 self.process_toolbar(req, fvg['toolbar'])
1273 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1274 # depending on how it feels, xmlrpclib.ServerProxy can translate
1275 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1276 # enjoy unicode strings which can not be trivially converted to
1277 # strings, and it blows up during parsing.
1279 # So ensure we fix this retardation by converting view xml back to
1281 if isinstance(fvg['arch'], unicode):
1282 arch = fvg['arch'].encode('utf-8')
1285 fvg['arch_string'] = arch
1288 evaluation_context = session.evaluation_context(context or {})
1289 xml = self.transform_view(arch, session, evaluation_context)
1291 xml = ElementTree.fromstring(arch)
1292 fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
1294 if 'id' in fvg['fields']:
1295 # Special case for id's
1296 id_field = fvg['fields']['id']
1297 id_field['original_type'] = id_field['type']
1298 id_field['type'] = 'id'
1300 for field in fvg['fields'].itervalues():
1301 if field.get('views'):
1302 for view in field["views"].itervalues():
1303 self.process_view(session, view, None, transform)
1304 if field.get('domain'):
1305 field["domain"] = parse_domain(field["domain"], session)
1306 if field.get('context'):
1307 field["context"] = parse_context(field["context"], session)
1309 def process_toolbar(self, req, toolbar):
1311 The toolbar is a mapping of section_key: [action_descriptor]
1313 We need to clean all those actions in order to ensure correct
1316 for actions in toolbar.itervalues():
1317 for action in actions:
1318 if 'context' in action:
1319 action['context'] = parse_context(
1320 action['context'], req.session)
1321 if 'domain' in action:
1322 action['domain'] = parse_domain(
1323 action['domain'], req.session)
1325 @openerpweb.jsonrequest
1326 def add_custom(self, req, view_id, arch):
1327 CustomView = req.session.model('ir.ui.view.custom')
1329 'user_id': req.session._uid,
1332 }, req.session.eval_context(req.context))
1333 return {'result': True}
1335 @openerpweb.jsonrequest
1336 def undo_custom(self, req, view_id, reset=False):
1337 CustomView = req.session.model('ir.ui.view.custom')
1338 context = req.session.eval_context(req.context)
1339 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1340 0, False, False, context)
1343 CustomView.unlink(vcustom, context)
1345 CustomView.unlink([vcustom[0]], context)
1346 return {'result': True}
1347 return {'result': False}
1349 def transform_view(self, view_string, session, context=None):
1350 # transform nodes on the fly via iterparse, instead of
1351 # doing it statically on the parsing result
1352 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1354 for event, elem in parser:
1355 if event == "start":
1358 self.parse_domains_and_contexts(elem, session)
1361 def parse_domains_and_contexts(self, elem, session):
1362 """ Converts domains and contexts from the view into Python objects,
1363 either literals if they can be parsed by literal_eval or a special
1364 placeholder object if the domain or context refers to free variables.
1366 :param elem: the current node being parsed
1367 :type param: xml.etree.ElementTree.Element
1368 :param session: OpenERP session object, used to store and retrieve
1370 :type session: openerpweb.openerpweb.OpenERPSession
1372 for el in ['domain', 'filter_domain']:
1373 domain = elem.get(el, '').strip()
1375 elem.set(el, parse_domain(domain, session))
1376 elem.set(el + '_string', domain)
1377 for el in ['context', 'default_get']:
1378 context_string = elem.get(el, '').strip()
1380 elem.set(el, parse_context(context_string, session))
1381 elem.set(el + '_string', context_string)
1383 @openerpweb.jsonrequest
1384 def load(self, req, model, view_id, view_type, toolbar=False):
1385 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1387 class TreeView(View):
1388 _cp_path = "/web/treeview"
1390 @openerpweb.jsonrequest
1391 def action(self, req, model, id):
1392 return load_actions_from_ir_values(
1393 req,'action', 'tree_but_open',[(model, id)],
1396 class SearchView(View):
1397 _cp_path = "/web/searchview"
1399 @openerpweb.jsonrequest
1400 def load(self, req, model, view_id):
1401 fields_view = self.fields_view_get(req, model, view_id, 'search')
1402 return {'fields_view': fields_view}
1404 @openerpweb.jsonrequest
1405 def fields_get(self, req, model):
1406 Model = req.session.model(model)
1407 fields = Model.fields_get(False, req.session.eval_context(req.context))
1408 for field in fields.values():
1409 # shouldn't convert the views too?
1410 if field.get('domain'):
1411 field["domain"] = parse_domain(field["domain"], req.session)
1412 if field.get('context'):
1413 field["context"] = parse_context(field["context"], req.session)
1414 return {'fields': fields}
1416 @openerpweb.jsonrequest
1417 def get_filters(self, req, model):
1418 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1419 Model = req.session.model("ir.filters")
1420 filters = Model.get_filters(model)
1421 for filter in filters:
1423 parsed_context = parse_context(filter["context"], req.session)
1424 filter["context"] = (parsed_context
1425 if not isinstance(parsed_context, common.nonliterals.BaseContext)
1426 else req.session.eval_context(parsed_context))
1428 parsed_domain = parse_domain(filter["domain"], req.session)
1429 filter["domain"] = (parsed_domain
1430 if not isinstance(parsed_domain, common.nonliterals.BaseDomain)
1431 else req.session.eval_domain(parsed_domain))
1433 logger.exception("Failed to parse custom filter %s in %s",
1434 filter['name'], model)
1435 filter['disabled'] = True
1436 del filter['context']
1437 del filter['domain']
1440 class Binary(openerpweb.Controller):
1441 _cp_path = "/web/binary"
1443 @openerpweb.httprequest
1444 def image(self, req, model, id, field, **kw):
1445 last_update = '__last_update'
1446 Model = req.session.model(model)
1447 context = req.session.eval_context(req.context)
1448 headers = [('Content-Type', 'image/png')]
1449 etag = req.httprequest.headers.get('If-None-Match')
1450 hashed_session = hashlib.md5(req.session_id).hexdigest()
1451 id = None if not id else simplejson.loads(id)
1452 if type(id) is list:
1455 if not id and hashed_session == etag:
1456 return werkzeug.wrappers.Response(status=304)
1458 date = Model.read([id], [last_update], context)[0].get(last_update)
1459 if hashlib.md5(date).hexdigest() == etag:
1460 return werkzeug.wrappers.Response(status=304)
1462 retag = hashed_session
1465 res = Model.default_get([field], context).get(field)
1466 image_data = base64.b64decode(res)
1468 res = Model.read([id], [last_update, field], context)[0]
1469 retag = hashlib.md5(res.get(last_update)).hexdigest()
1470 image_data = base64.b64decode(res.get(field))
1471 except (TypeError, xmlrpclib.Fault):
1472 image_data = self.placeholder(req)
1473 headers.append(('ETag', retag))
1474 headers.append(('Content-Length', len(image_data)))
1476 ncache = int(kw.get('cache'))
1477 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1480 return req.make_response(image_data, headers)
1481 def placeholder(self, req):
1482 addons_path = openerpweb.addons_manifest['web']['addons_path']
1483 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1484 def content_disposition(self, filename, req):
1485 filename = filename.encode('utf8')
1486 escaped = urllib2.quote(filename)
1487 browser = req.httprequest.user_agent.browser
1488 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
1489 if browser == 'msie' and version < 9:
1490 return "attachment; filename=%s" % escaped
1491 elif browser == 'safari':
1492 return "attachment; filename=%s" % filename
1494 return "attachment; filename*=UTF-8''%s" % escaped
1496 @openerpweb.httprequest
1497 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1498 """ Download link for files stored as binary fields.
1500 If the ``id`` parameter is omitted, fetches the default value for the
1501 binary field (via ``default_get``), otherwise fetches the field for
1502 that precise record.
1504 :param req: OpenERP request
1505 :type req: :class:`web.common.http.HttpRequest`
1506 :param str model: name of the model to fetch the binary from
1507 :param str field: binary field
1508 :param str id: id of the record from which to fetch the binary
1509 :param str filename_field: field holding the file's name, if any
1510 :returns: :class:`werkzeug.wrappers.Response`
1512 Model = req.session.model(model)
1513 context = req.session.eval_context(req.context)
1516 fields.append(filename_field)
1518 res = Model.read([int(id)], fields, context)[0]
1520 res = Model.default_get(fields, context)
1521 filecontent = base64.b64decode(res.get(field, ''))
1523 return req.not_found()
1525 filename = '%s_%s' % (model.replace('.', '_'), id)
1527 filename = res.get(filename_field, '') or filename
1528 return req.make_response(filecontent,
1529 [('Content-Type', 'application/octet-stream'),
1530 ('Content-Disposition', self.content_disposition(filename, req))])
1532 @openerpweb.httprequest
1533 def saveas_ajax(self, req, data, token):
1534 jdata = simplejson.loads(data)
1535 model = jdata['model']
1536 field = jdata['field']
1537 id = jdata.get('id', None)
1538 filename_field = jdata.get('filename_field', None)
1539 context = jdata.get('context', dict())
1541 context = req.session.eval_context(context)
1542 Model = req.session.model(model)
1545 fields.append(filename_field)
1547 res = Model.read([int(id)], fields, context)[0]
1549 res = Model.default_get(fields, context)
1550 filecontent = base64.b64decode(res.get(field, ''))
1552 raise ValueError("No content found for field '%s' on '%s:%s'" %
1555 filename = '%s_%s' % (model.replace('.', '_'), id)
1557 filename = res.get(filename_field, '') or filename
1558 return req.make_response(filecontent,
1559 headers=[('Content-Type', 'application/octet-stream'),
1560 ('Content-Disposition', self.content_disposition(filename, req))],
1561 cookies={'fileToken': int(token)})
1563 @openerpweb.httprequest
1564 def upload(self, req, callback, ufile):
1565 # TODO: might be useful to have a configuration flag for max-length file uploads
1567 out = """<script language="javascript" type="text/javascript">
1568 var win = window.top.window;
1569 win.jQuery(win).trigger(%s, %s);
1572 args = [len(data), ufile.filename,
1573 ufile.content_type, base64.b64encode(data)]
1574 except Exception, e:
1575 args = [False, e.message]
1576 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1578 @openerpweb.httprequest
1579 def upload_attachment(self, req, callback, model, id, ufile):
1580 context = req.session.eval_context(req.context)
1581 Model = req.session.model('ir.attachment')
1583 out = """<script language="javascript" type="text/javascript">
1584 var win = window.top.window;
1585 win.jQuery(win).trigger(%s, %s);
1587 attachment_id = Model.create({
1588 'name': ufile.filename,
1589 'datas': base64.encodestring(ufile.read()),
1590 'datas_fname': ufile.filename,
1595 'filename': ufile.filename,
1598 except Exception, e:
1599 args = { 'error': e.message }
1600 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1602 class Action(openerpweb.Controller):
1603 _cp_path = "/web/action"
1605 @openerpweb.jsonrequest
1606 def load(self, req, action_id, do_not_eval=False):
1607 Actions = req.session.model('ir.actions.actions')
1609 context = req.session.eval_context(req.context)
1612 action_id = int(action_id)
1615 module, xmlid = action_id.split('.', 1)
1616 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1617 assert model.startswith('ir.actions.')
1619 action_id = 0 # force failed read
1621 base_action = Actions.read([action_id], ['type'], context)
1624 action_type = base_action[0]['type']
1625 if action_type == 'ir.actions.report.xml':
1626 ctx.update({'bin_size': True})
1628 action = req.session.model(action_type).read([action_id], False, ctx)
1630 value = clean_action(req, action[0], do_not_eval)
1631 return {'result': value}
1633 @openerpweb.jsonrequest
1634 def run(self, req, action_id):
1635 return_action = req.session.model('ir.actions.server').run(
1636 [action_id], req.session.eval_context(req.context))
1638 return clean_action(req, return_action)
1643 _cp_path = "/web/export"
1645 @openerpweb.jsonrequest
1646 def formats(self, req):
1647 """ Returns all valid export formats
1649 :returns: for each export format, a pair of identifier and printable name
1650 :rtype: [(str, str)]
1654 for path, controller in openerpweb.controllers_path.iteritems()
1655 if path.startswith(self._cp_path)
1656 if hasattr(controller, 'fmt')
1657 ], key=operator.itemgetter("label"))
1659 def fields_get(self, req, model):
1660 Model = req.session.model(model)
1661 fields = Model.fields_get(False, req.session.eval_context(req.context))
1664 @openerpweb.jsonrequest
1665 def get_fields(self, req, model, prefix='', parent_name= '',
1666 import_compat=True, parent_field_type=None,
1669 if import_compat and parent_field_type == "many2one":
1672 fields = self.fields_get(req, model)
1675 fields.pop('id', None)
1677 fields['.id'] = fields.pop('id', {'string': 'ID'})
1679 fields_sequence = sorted(fields.iteritems(),
1680 key=lambda field: field[1].get('string', ''))
1683 for field_name, field in fields_sequence:
1685 if exclude and field_name in exclude:
1687 if field.get('readonly'):
1688 # If none of the field's states unsets readonly, skip the field
1689 if all(dict(attrs).get('readonly', True)
1690 for attrs in field.get('states', {}).values()):
1693 id = prefix + (prefix and '/'or '') + field_name
1694 name = parent_name + (parent_name and '/' or '') + field['string']
1695 record = {'id': id, 'string': name,
1696 'value': id, 'children': False,
1697 'field_type': field.get('type'),
1698 'required': field.get('required'),
1699 'relation_field': field.get('relation_field')}
1700 records.append(record)
1702 if len(name.split('/')) < 3 and 'relation' in field:
1703 ref = field.pop('relation')
1704 record['value'] += '/id'
1705 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1707 if not import_compat or field['type'] == 'one2many':
1708 # m2m field in import_compat is childless
1709 record['children'] = True
1713 @openerpweb.jsonrequest
1714 def namelist(self,req, model, export_id):
1715 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1716 export = req.session.model("ir.exports").read([export_id])[0]
1717 export_fields_list = req.session.model("ir.exports.line").read(
1718 export['export_fields'])
1720 fields_data = self.fields_info(
1721 req, model, map(operator.itemgetter('name'), export_fields_list))
1724 {'name': field['name'], 'label': fields_data[field['name']]}
1725 for field in export_fields_list
1728 def fields_info(self, req, model, export_fields):
1730 fields = self.fields_get(req, model)
1732 # To make fields retrieval more efficient, fetch all sub-fields of a
1733 # given field at the same time. Because the order in the export list is
1734 # arbitrary, this requires ordering all sub-fields of a given field
1735 # together so they can be fetched at the same time
1737 # Works the following way:
1738 # * sort the list of fields to export, the default sorting order will
1739 # put the field itself (if present, for xmlid) and all of its
1740 # sub-fields right after it
1741 # * then, group on: the first field of the path (which is the same for
1742 # a field and for its subfields and the length of splitting on the
1743 # first '/', which basically means grouping the field on one side and
1744 # all of the subfields on the other. This way, we have the field (for
1745 # the xmlid) with length 1, and all of the subfields with the same
1746 # base but a length "flag" of 2
1747 # * if we have a normal field (length 1), just add it to the info
1748 # mapping (with its string) as-is
1749 # * otherwise, recursively call fields_info via graft_subfields.
1750 # all graft_subfields does is take the result of fields_info (on the
1751 # field's model) and prepend the current base (current field), which
1752 # rebuilds the whole sub-tree for the field
1754 # result: because we're not fetching the fields_get for half the
1755 # database models, fetching a namelist with a dozen fields (including
1756 # relational data) falls from ~6s to ~300ms (on the leads model).
1757 # export lists with no sub-fields (e.g. import_compatible lists with
1758 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1759 # there's a single fields_get to execute)
1760 for (base, length), subfields in itertools.groupby(
1761 sorted(export_fields),
1762 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1763 subfields = list(subfields)
1765 # subfields is a seq of $base/*rest, and not loaded yet
1766 info.update(self.graft_subfields(
1767 req, fields[base]['relation'], base, fields[base]['string'],
1771 info[base] = fields[base]['string']
1775 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1776 export_fields = [field.split('/', 1)[1] for field in fields]
1778 (prefix + '/' + k, prefix_string + '/' + v)
1779 for k, v in self.fields_info(req, model, export_fields).iteritems())
1781 #noinspection PyPropertyDefinition
1783 def content_type(self):
1784 """ Provides the format's content type """
1785 raise NotImplementedError()
1787 def filename(self, base):
1788 """ Creates a valid filename for the format (with extension) from the
1789 provided base name (exension-less)
1791 raise NotImplementedError()
1793 def from_data(self, fields, rows):
1794 """ Conversion method from OpenERP's export data to whatever the
1795 current export class outputs
1797 :params list fields: a list of fields to export
1798 :params list rows: a list of records to export
1802 raise NotImplementedError()
1804 @openerpweb.httprequest
1805 def index(self, req, data, token):
1806 model, fields, ids, domain, import_compat = \
1807 operator.itemgetter('model', 'fields', 'ids', 'domain',
1809 simplejson.loads(data))
1811 context = req.session.eval_context(req.context)
1812 Model = req.session.model(model)
1813 ids = ids or Model.search(domain, 0, False, False, context)
1815 field_names = map(operator.itemgetter('name'), fields)
1816 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1819 columns_headers = field_names
1821 columns_headers = [val['label'].strip() for val in fields]
1824 return req.make_response(self.from_data(columns_headers, import_data),
1825 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1826 ('Content-Type', self.content_type)],
1827 cookies={'fileToken': int(token)})
1829 class CSVExport(Export):
1830 _cp_path = '/web/export/csv'
1831 fmt = {'tag': 'csv', 'label': 'CSV'}
1834 def content_type(self):
1835 return 'text/csv;charset=utf8'
1837 def filename(self, base):
1838 return base + '.csv'
1840 def from_data(self, fields, rows):
1842 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1844 writer.writerow([name.encode('utf-8') for name in fields])
1849 if isinstance(d, basestring):
1850 d = d.replace('\n',' ').replace('\t',' ')
1852 d = d.encode('utf-8')
1853 except UnicodeError:
1855 if d is False: d = None
1857 writer.writerow(row)
1864 class ExcelExport(Export):
1865 _cp_path = '/web/export/xls'
1869 'error': None if xlwt else "XLWT required"
1873 def content_type(self):
1874 return 'application/vnd.ms-excel'
1876 def filename(self, base):
1877 return base + '.xls'
1879 def from_data(self, fields, rows):
1880 workbook = xlwt.Workbook()
1881 worksheet = workbook.add_sheet('Sheet 1')
1883 for i, fieldname in enumerate(fields):
1884 worksheet.write(0, i, fieldname)
1885 worksheet.col(i).width = 8000 # around 220 pixels
1887 style = xlwt.easyxf('align: wrap yes')
1889 for row_index, row in enumerate(rows):
1890 for cell_index, cell_value in enumerate(row):
1891 if isinstance(cell_value, basestring):
1892 cell_value = re.sub("\r", " ", cell_value)
1893 if cell_value is False: cell_value = None
1894 worksheet.write(row_index + 1, cell_index, cell_value, style)
1903 class Reports(View):
1904 _cp_path = "/web/report"
1905 POLLING_DELAY = 0.25
1907 'doc': 'application/vnd.ms-word',
1908 'html': 'text/html',
1909 'odt': 'application/vnd.oasis.opendocument.text',
1910 'pdf': 'application/pdf',
1911 'sxw': 'application/vnd.sun.xml.writer',
1912 'xls': 'application/vnd.ms-excel',
1915 @openerpweb.httprequest
1916 def index(self, req, action, token):
1917 action = simplejson.loads(action)
1919 report_srv = req.session.proxy("report")
1920 context = req.session.eval_context(
1921 common.nonliterals.CompoundContext(
1922 req.context or {}, action[ "context"]))
1925 report_ids = context["active_ids"]
1926 if 'report_type' in action:
1927 report_data['report_type'] = action['report_type']
1928 if 'datas' in action:
1929 if 'ids' in action['datas']:
1930 report_ids = action['datas'].pop('ids')
1931 report_data.update(action['datas'])
1933 report_id = report_srv.report(
1934 req.session._db, req.session._uid, req.session._password,
1935 action["report_name"], report_ids,
1936 report_data, context)
1938 report_struct = None
1940 report_struct = report_srv.report_get(
1941 req.session._db, req.session._uid, req.session._password, report_id)
1942 if report_struct["state"]:
1945 time.sleep(self.POLLING_DELAY)
1947 report = base64.b64decode(report_struct['result'])
1948 if report_struct.get('code') == 'zlib':
1949 report = zlib.decompress(report)
1950 report_mimetype = self.TYPES_MAPPING.get(
1951 report_struct['format'], 'octet-stream')
1952 file_name = action.get('name', 'report')
1953 if 'name' not in action:
1954 reports = req.session.model('ir.actions.report.xml')
1955 res_id = reports.search([('report_name', '=', action['report_name']),],
1956 0, False, False, context)
1958 file_name = reports.read(res_id[0], ['name'], context)['name']
1960 file_name = action['report_name']
1962 return req.make_response(report,
1964 # maybe we should take of what characters can appear in a file name?
1965 ('Content-Disposition', 'attachment; filename="%s.%s"' % (file_name, report_struct['format'])),
1966 ('Content-Type', report_mimetype),
1967 ('Content-Length', len(report))],
1968 cookies={'fileToken': int(token)})
1971 _cp_path = "/web/import"
1973 def fields_get(self, req, model):
1974 Model = req.session.model(model)
1975 fields = Model.fields_get(False, req.session.eval_context(req.context))
1978 @openerpweb.httprequest
1979 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1981 data = list(csv.reader(
1982 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1983 except csv.Error, e:
1985 return '<script>window.top.%s(%s);</script>' % (
1986 jsonp, simplejson.dumps({'error': {
1987 'message': 'Error parsing CSV file: %s' % e,
1988 # decodes each byte to a unicode character, which may or
1989 # may not be printable, but decoding will succeed.
1990 # Otherwise simplejson will try to decode the `str` using
1991 # utf-8, which is very likely to blow up on characters out
1992 # of the ascii range (in range [128, 256))
1993 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1996 return '<script>window.top.%s(%s);</script>' % (
1997 jsonp, simplejson.dumps(
1998 {'records': data[:10]}, encoding=csvcode))
1999 except UnicodeDecodeError:
2000 return '<script>window.top.%s(%s);</script>' % (
2001 jsonp, simplejson.dumps({
2002 'message': u"Failed to decode CSV file using encoding %s, "
2003 u"try switching to a different encoding" % csvcode
2006 @openerpweb.httprequest
2007 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
2009 modle_obj = req.session.model(model)
2010 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
2011 simplejson.loads(meta))
2014 if not (csvdel and len(csvdel) == 1):
2015 error = u"The CSV delimiter must be a single character"
2017 if not indices and fields:
2018 error = u"You must select at least one field to import"
2021 return '<script>window.top.%s(%s);</script>' % (
2022 jsonp, simplejson.dumps({'error': {'message': error}}))
2024 # skip ignored records (@skip parameter)
2025 # then skip empty lines (not valid csv)
2026 # nb: should these operations be reverted?
2027 rows_to_import = itertools.ifilter(
2030 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
2033 # if only one index, itemgetter will return an atom rather than a tuple
2034 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
2035 else: mapper = operator.itemgetter(*indices)
2040 # decode each data row
2042 [record.decode(csvcode) for record in row]
2043 for row in itertools.imap(mapper, rows_to_import)
2044 # don't insert completely empty rows (can happen due to fields
2045 # filtering in case of e.g. o2m content rows)
2048 except UnicodeDecodeError:
2049 error = u"Failed to decode CSV file using encoding %s" % csvcode
2050 except csv.Error, e:
2051 error = u"Could not process CSV file: %s" % e
2053 # If the file contains nothing,
2055 error = u"File to import is empty"
2057 return '<script>window.top.%s(%s);</script>' % (
2058 jsonp, simplejson.dumps({'error': {'message': error}}))
2061 (code, record, message, _nope) = modle_obj.import_data(
2062 fields, data, 'init', '', False,
2063 req.session.eval_context(req.context))
2064 except xmlrpclib.Fault, e:
2065 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
2066 return '<script>window.top.%s(%s);</script>' % (
2067 jsonp, simplejson.dumps({'error':error}))
2070 return '<script>window.top.%s(%s);</script>' % (
2071 jsonp, simplejson.dumps({'success':True}))
2073 msg = u"Error during import: %s\n\nTrying to import record %r" % (
2075 return '<script>window.top.%s(%s);</script>' % (
2076 jsonp, simplejson.dumps({'error': {'message':msg}}))
2078 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: