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 # Validated by diff -u of sass2scss against:
39 # sass-convert -F sass -T scss openerp.sass openerp.scss
42 reComment = re.compile(r'//.*$')
43 reIndent = re.compile(r'^\s+')
44 reIgnore = re.compile(r'^\s*(//.*)?$')
45 reFixes = { re.compile(r'\(\((.*)\)\)') : r'(\1)', }
48 for l in src.split('\n'):
50 if reIgnore.search(l): continue
51 l = reComment.sub('', l)
53 indent = reIndent.match(l)
54 level = indent.end() if indent else 0
57 prevBlocks[lastLevel] = block
59 block[-1] = (block[-1], newBlock)
62 block = prevBlocks[level]
66 for ereg, repl in reFixes.items():
67 l = ereg.sub(repl if type(repl)==str else repl(), l)
70 def write(sass, level=-1):
75 out += indent+sass[0]+" {\n"
77 out += write(e, level+1)
79 out = out.rstrip(" \n")
84 out += indent+sass+";\n"
89 proxy = req.session.proxy("db")
91 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
93 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
94 dbs = [i for i in dbs if re.match(r, i)]
97 def module_topological_sort(modules):
98 """ Return a list of module names sorted so that their dependencies of the
99 modules are listed before the module itself
101 modules is a dict of {module_name: dependencies}
103 :param modules: modules to sort
108 dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
109 # incoming edge: dependency on other module (if a depends on b, a has an
110 # incoming edge from b, aka there's an edge from b to a)
111 # outgoing edge: other module depending on this one
113 # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
114 #L ← Empty list that will contain the sorted nodes
116 #S ← Set of all nodes with no outgoing edges (modules on which no other
118 S = set(module for module in modules if module not in dependencies)
121 #function visit(node n)
123 #if n has not been visited yet then
127 #change: n not web module, can not be resolved, ignore
128 if n not in modules: return
129 #for each node m with an edge from m to n do (dependencies of n)
135 #for each node n in S do
141 def module_installed(req):
142 # Candidates module the current heuristic is the /static dir
143 loadable = openerpweb.addons_manifest.keys()
146 # Retrieve database installed modules
147 # TODO The following code should move to ir.module.module.list_installed_modules()
148 Modules = req.session.model('ir.module.module')
149 domain = [('state','=','installed'), ('name','in', loadable)]
150 for module in Modules.search_read(domain, ['name', 'dependencies_id']):
151 modules[module['name']] = []
152 deps = module.get('dependencies_id')
154 deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
155 dependencies = [i['name'] for i in deps_read]
156 modules[module['name']] = dependencies
158 sorted_modules = module_topological_sort(modules)
159 return sorted_modules
161 def module_installed_bypass_session(dbname):
162 loadable = openerpweb.addons_manifest.keys()
165 import openerp.modules.registry
166 registry = openerp.modules.registry.RegistryManager.get(dbname)
167 with registry.cursor() as cr:
168 m = registry.get('ir.module.module')
169 # TODO The following code should move to ir.module.module.list_installed_modules()
170 domain = [('state','=','installed'), ('name','in', loadable)]
171 ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
172 for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
173 modules[module['name']] = []
174 deps = module.get('dependencies_id')
176 deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
177 dependencies = [i['name'] for i in deps_read]
178 modules[module['name']] = dependencies
181 sorted_modules = module_topological_sort(modules)
182 return sorted_modules
184 def module_boot(req):
188 for i in req.config.server_wide_modules:
189 if i in openerpweb.addons_manifest:
192 # if only one db load every module at boot
193 dbside = module_installed_bypass_session(dbs[0])
194 dbside = [i for i in dbside if i not in serverside]
195 addons = serverside + dbside
198 def concat_xml(file_list):
199 """Concatenate xml files
201 :param list(str) file_list: list of files to check
202 :returns: (concatenation_result, checksum)
205 checksum = hashlib.new('sha1')
207 return '', checksum.hexdigest()
210 for fname in file_list:
211 with open(fname, 'rb') as fp:
213 checksum.update(contents)
215 xml = ElementTree.parse(fp).getroot()
218 root = ElementTree.Element(xml.tag)
219 #elif root.tag != xml.tag:
220 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
222 for child in xml.getchildren():
224 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
226 def concat_files(file_list, reader=None, intersperse=""):
227 """ Concatenates contents of all provided files
229 :param list(str) file_list: list of files to check
230 :param function reader: reading procedure for each file
231 :param str intersperse: string to intersperse between file contents
232 :returns: (concatenation_result, checksum)
235 checksum = hashlib.new('sha1')
237 return '', checksum.hexdigest()
241 with open(f, 'rb') as fp:
245 for fname in file_list:
246 contents = reader(fname)
247 checksum.update(contents)
248 files_content.append(contents)
250 files_concat = intersperse.join(files_content)
251 return files_concat, checksum.hexdigest()
253 def manifest_glob(req, addons, key):
255 addons = module_boot(req)
257 addons = addons.split(',')
260 manifest = openerpweb.addons_manifest.get(addon, None)
263 # ensure does not ends with /
264 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
265 globlist = manifest.get(key, [])
266 for pattern in globlist:
267 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
268 r.append((path, path[len(addons_path):]))
271 def manifest_list(req, mods, extension):
273 path = '/web/webclient/' + extension
275 path += '?mods=' + mods
277 files = manifest_glob(req, mods, extension)
278 i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
279 req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
281 return [wp for _fp, wp in files]
283 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
285 def get_last_modified(files):
286 """ Returns the modification time of the most recently modified
289 :param list(str) files: names of files to check
290 :return: most recent modification time amongst the fileset
291 :rtype: datetime.datetime
295 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
297 return datetime.datetime(1970, 1, 1)
299 def make_conditional(req, response, last_modified=None, etag=None):
300 """ Makes the provided response conditional based upon the request,
301 and mandates revalidation from clients
303 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
304 setting ``last_modified`` and ``etag`` correctly on the response object
306 :param req: OpenERP request
307 :type req: web.common.http.WebRequest
308 :param response: Werkzeug response
309 :type response: werkzeug.wrappers.Response
310 :param datetime.datetime last_modified: last modification date of the response content
311 :param str etag: some sort of checksum of the content (deep etag)
312 :return: the response object provided
313 :rtype: werkzeug.wrappers.Response
315 response.cache_control.must_revalidate = True
316 response.cache_control.max_age = 0
318 response.last_modified = last_modified
320 response.set_etag(etag)
321 return response.make_conditional(req.httprequest)
323 def login_and_redirect(req, db, login, key, redirect_url='/'):
324 req.session.authenticate(db, login, key, {})
325 return set_cookie_and_redirect(req, redirect_url)
327 def set_cookie_and_redirect(req, redirect_url):
328 redirect = werkzeug.utils.redirect(redirect_url, 303)
329 redirect.autocorrect_location_header = False
330 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
331 redirect.set_cookie('instance0|session_id', cookie_val)
334 def eval_context_and_domain(session, context, domain=None):
335 e_context = session.eval_context(context)
336 # should we give the evaluated context as an evaluation context to the domain?
337 e_domain = session.eval_domain(domain or [])
339 return e_context, e_domain
341 def load_actions_from_ir_values(req, key, key2, models, meta):
342 context = req.session.eval_context(req.context)
343 Values = req.session.model('ir.values')
344 actions = Values.get(key, key2, models, meta, context)
346 return [(id, name, clean_action(req, action))
347 for id, name, action in actions]
349 def clean_action(req, action, do_not_eval=False):
350 action.setdefault('flags', {})
352 context = req.session.eval_context(req.context)
353 eval_ctx = req.session.evaluation_context(context)
356 # values come from the server, we can just eval them
357 if action.get('context') and isinstance(action.get('context'), basestring):
358 action['context'] = eval( action['context'], eval_ctx ) or {}
360 if action.get('domain') and isinstance(action.get('domain'), basestring):
361 action['domain'] = eval( action['domain'], eval_ctx ) or []
363 if 'context' in action:
364 action['context'] = parse_context(action['context'], req.session)
365 if 'domain' in action:
366 action['domain'] = parse_domain(action['domain'], req.session)
368 action_type = action.setdefault('type', 'ir.actions.act_window_close')
369 if action_type == 'ir.actions.act_window':
370 return fix_view_modes(action)
373 # I think generate_views,fix_view_modes should go into js ActionManager
374 def generate_views(action):
376 While the server generates a sequence called "views" computing dependencies
377 between a bunch of stuff for views coming directly from the database
378 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
379 to return custom view dictionaries generated on the fly.
381 In that case, there is no ``views`` key available on the action.
383 Since the web client relies on ``action['views']``, generate it here from
384 ``view_mode`` and ``view_id``.
386 Currently handles two different cases:
388 * no view_id, multiple view_mode
389 * single view_id, single view_mode
391 :param dict action: action descriptor dictionary to generate a views key for
393 view_id = action.get('view_id') or False
394 if isinstance(view_id, (list, tuple)):
397 # providing at least one view mode is a requirement, not an option
398 view_modes = action['view_mode'].split(',')
400 if len(view_modes) > 1:
402 raise ValueError('Non-db action dictionaries should provide '
403 'either multiple view modes or a single view '
404 'mode and an optional view id.\n\n Got view '
405 'modes %r and view id %r for action %r' % (
406 view_modes, view_id, action))
407 action['views'] = [(False, mode) for mode in view_modes]
409 action['views'] = [(view_id, view_modes[0])]
411 def fix_view_modes(action):
412 """ For historical reasons, OpenERP has weird dealings in relation to
413 view_mode and the view_type attribute (on window actions):
415 * one of the view modes is ``tree``, which stands for both list views
417 * the choice is made by checking ``view_type``, which is either
418 ``form`` for a list view or ``tree`` for an actual tree view
420 This methods simply folds the view_type into view_mode by adding a
421 new view mode ``list`` which is the result of the ``tree`` view_mode
422 in conjunction with the ``form`` view_type.
424 TODO: this should go into the doc, some kind of "peculiarities" section
426 :param dict action: an action descriptor
427 :returns: nothing, the action is modified in place
429 if not action.get('views'):
430 generate_views(action)
433 for index, (id, mode) in enumerate(action['views']):
438 if action.pop('view_type', 'form') != 'form':
442 [id, mode if mode != 'tree' else 'list']
443 for id, mode in action['views']
448 def parse_domain(domain, session):
449 """ Parses an arbitrary string containing a domain, transforms it
450 to either a literal domain or a :class:`common.nonliterals.Domain`
452 :param domain: the domain to parse, if the domain is not a string it
453 is assumed to be a literal domain and is returned as-is
454 :param session: Current OpenERP session
455 :type session: openerpweb.openerpweb.OpenERPSession
457 if not isinstance(domain, basestring):
460 return ast.literal_eval(domain)
463 return common.nonliterals.Domain(session, domain)
465 def parse_context(context, session):
466 """ Parses an arbitrary string containing a context, transforms it
467 to either a literal context or a :class:`common.nonliterals.Context`
469 :param context: the context to parse, if the context is not a string it
470 is assumed to be a literal domain and is returned as-is
471 :param session: Current OpenERP session
472 :type session: openerpweb.openerpweb.OpenERPSession
474 if not isinstance(context, basestring):
477 return ast.literal_eval(context)
479 return common.nonliterals.Context(session, context)
481 #----------------------------------------------------------
482 # OpenERP Web web Controllers
483 #----------------------------------------------------------
485 html_template = """<!DOCTYPE html>
486 <html style="height: 100%%">
488 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
489 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
490 <title>OpenERP</title>
491 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
492 <link rel="stylesheet" href="/web/static/src/css/full.css" />
495 <script type="text/javascript">
497 var s = new openerp.init(%(modules)s);
506 class Home(openerpweb.Controller):
509 @openerpweb.httprequest
510 def index(self, req, s_action=None, **kw):
511 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, None, 'js'))
512 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, None, 'css'))
514 r = html_template % {
517 'modules': simplejson.dumps(module_boot(req)),
518 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
522 @openerpweb.httprequest
523 def login(self, req, db, login, key):
524 return login_and_redirect(req, db, login, key)
526 class WebClient(openerpweb.Controller):
527 _cp_path = "/web/webclient"
529 @openerpweb.jsonrequest
530 def csslist(self, req, mods=None):
531 return manifest_list(req, mods, 'css')
533 @openerpweb.jsonrequest
534 def jslist(self, req, mods=None):
535 return manifest_list(req, mods, 'js')
537 @openerpweb.jsonrequest
538 def qweblist(self, req, mods=None):
539 return manifest_list(req, mods, 'qweb')
541 @openerpweb.httprequest
542 def css(self, req, mods=None):
543 files = list(manifest_glob(req, mods, 'css'))
544 last_modified = get_last_modified(f[0] for f in files)
545 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
546 return werkzeug.wrappers.Response(status=304)
548 file_map = dict(files)
550 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
551 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
554 """read the a css file and absolutify all relative uris"""
555 with open(f, 'rb') as fp:
556 data = fp.read().decode('utf-8')
559 # convert FS path into web path
560 web_dir = '/'.join(os.path.dirname(path).split(os.path.sep))
564 r"""@import \1%s/""" % (web_dir,),
570 r"""url(\1%s/""" % (web_dir,),
573 return data.encode('utf-8')
575 content, checksum = concat_files((f[0] for f in files), reader)
577 return make_conditional(
578 req, req.make_response(content, [('Content-Type', 'text/css')]),
579 last_modified, checksum)
581 @openerpweb.httprequest
582 def js(self, req, mods=None):
583 files = [f[0] for f in manifest_glob(req, mods, 'js')]
584 last_modified = get_last_modified(files)
585 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
586 return werkzeug.wrappers.Response(status=304)
588 content, checksum = concat_files(files, intersperse=';')
590 return make_conditional(
591 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
592 last_modified, checksum)
594 @openerpweb.httprequest
595 def qweb(self, req, mods=None):
596 files = [f[0] for f in manifest_glob(req, mods, 'qweb')]
597 last_modified = get_last_modified(files)
598 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
599 return werkzeug.wrappers.Response(status=304)
601 content, checksum = concat_xml(files)
603 return make_conditional(
604 req, req.make_response(content, [('Content-Type', 'text/xml')]),
605 last_modified, checksum)
607 @openerpweb.jsonrequest
608 def translations(self, req, mods, lang):
609 lang_model = req.session.model('res.lang')
610 ids = lang_model.search([("code", "=", lang)])
612 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
613 "grouping", "decimal_point", "thousands_sep"])
621 langs = lang.split(separator)
622 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
625 for addon_name in mods:
626 transl = {"messages":[]}
627 transs[addon_name] = transl
628 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
630 f_name = os.path.join(addons_path, addon_name, "i18n", l + ".po")
631 if not os.path.exists(f_name):
634 with open(f_name) as t_file:
635 po = babel.messages.pofile.read_po(t_file)
639 if x.id and x.string and "openerp-web" in x.auto_comments:
640 transl["messages"].append({'id': x.id, 'string': x.string})
641 return {"modules": transs,
642 "lang_parameters": lang_obj}
644 @openerpweb.jsonrequest
645 def version_info(self, req):
647 "version": common.release.version
650 class Proxy(openerpweb.Controller):
651 _cp_path = '/web/proxy'
653 @openerpweb.jsonrequest
654 def load(self, req, path):
655 """ Proxies an HTTP request through a JSON request.
657 It is strongly recommended to not request binary files through this,
658 as the result will be a binary data blob as well.
660 :param req: OpenERP request
661 :param path: actual request path
662 :return: file content
664 from werkzeug.test import Client
665 from werkzeug.wrappers import BaseResponse
667 return Client(req.httprequest.app, BaseResponse).get(path).data
669 class Database(openerpweb.Controller):
670 _cp_path = "/web/database"
672 @openerpweb.jsonrequest
673 def get_list(self, req):
675 return {"db_list": dbs}
677 @openerpweb.jsonrequest
678 def create(self, req, fields):
679 params = dict(map(operator.itemgetter('name', 'value'), fields))
681 params['super_admin_pwd'],
683 bool(params.get('demo_data')),
685 params['create_admin_pwd']
688 return req.session.proxy("db").create_database(*create_attrs)
690 @openerpweb.jsonrequest
691 def drop(self, req, fields):
692 password, db = operator.itemgetter(
693 'drop_pwd', 'drop_db')(
694 dict(map(operator.itemgetter('name', 'value'), fields)))
697 return req.session.proxy("db").drop(password, db)
698 except xmlrpclib.Fault, e:
699 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
700 return {'error': e.faultCode, 'title': 'Drop Database'}
701 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
703 @openerpweb.httprequest
704 def backup(self, req, backup_db, backup_pwd, token):
706 db_dump = base64.b64decode(
707 req.session.proxy("db").dump(backup_pwd, backup_db))
708 filename = "%(db)s_%(timestamp)s.dump" % {
710 'timestamp': datetime.datetime.utcnow().strftime(
711 "%Y-%m-%d_%H-%M-%SZ")
713 return req.make_response(db_dump,
714 [('Content-Type', 'application/octet-stream; charset=binary'),
715 ('Content-Disposition', 'attachment; filename="' + filename + '"')],
716 {'fileToken': int(token)}
718 except xmlrpclib.Fault, e:
719 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
721 @openerpweb.httprequest
722 def restore(self, req, db_file, restore_pwd, new_db):
724 data = base64.b64encode(db_file.read())
725 req.session.proxy("db").restore(restore_pwd, new_db, data)
727 except xmlrpclib.Fault, e:
728 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
729 raise Exception("AccessDenied")
731 @openerpweb.jsonrequest
732 def change_password(self, req, fields):
733 old_password, new_password = operator.itemgetter(
734 'old_pwd', 'new_pwd')(
735 dict(map(operator.itemgetter('name', 'value'), fields)))
737 return req.session.proxy("db").change_admin_password(old_password, new_password)
738 except xmlrpclib.Fault, e:
739 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
740 return {'error': e.faultCode, 'title': 'Change Password'}
741 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
743 class Session(openerpweb.Controller):
744 _cp_path = "/web/session"
746 def session_info(self, req):
747 req.session.ensure_valid()
749 "session_id": req.session_id,
750 "uid": req.session._uid,
751 "context": req.session.get_context() if req.session._uid else {},
752 "db": req.session._db,
753 "login": req.session._login,
754 "openerp_entreprise": req.session.openerp_entreprise(),
757 @openerpweb.jsonrequest
758 def get_session_info(self, req):
759 return self.session_info(req)
761 @openerpweb.jsonrequest
762 def authenticate(self, req, db, login, password, base_location=None):
763 wsgienv = req.httprequest.environ
764 release = common.release
766 base_location=base_location,
767 HTTP_HOST=wsgienv['HTTP_HOST'],
768 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
769 user_agent="%s / %s" % (release.name, release.version),
771 req.session.authenticate(db, login, password, env)
773 return self.session_info(req)
775 @openerpweb.jsonrequest
776 def change_password (self,req,fields):
777 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
778 dict(map(operator.itemgetter('name', 'value'), fields)))
779 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
780 return {'error':'All passwords have to be filled.','title': 'Change Password'}
781 if new_password != confirm_password:
782 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
784 if req.session.model('res.users').change_password(
785 old_password, new_password):
786 return {'new_password':new_password}
788 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
789 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
791 @openerpweb.jsonrequest
792 def sc_list(self, req):
793 return req.session.model('ir.ui.view_sc').get_sc(
794 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
796 @openerpweb.jsonrequest
797 def get_lang_list(self, req):
800 'lang_list': (req.session.proxy("db").list_lang() or []),
804 return {"error": e, "title": "Languages"}
806 @openerpweb.jsonrequest
807 def modules(self, req):
808 loaded = module_boot(req)
809 modules = module_installed(req)
810 return [module for module in modules if module not in loaded]
812 @openerpweb.jsonrequest
813 def eval_domain_and_context(self, req, contexts, domains,
815 """ Evaluates sequences of domains and contexts, composing them into
816 a single context, domain or group_by sequence.
818 :param list contexts: list of contexts to merge together. Contexts are
819 evaluated in sequence, all previous contexts
820 are part of their own evaluation context
821 (starting at the session context).
822 :param list domains: list of domains to merge together. Domains are
823 evaluated in sequence and appended to one another
824 (implicit AND), their evaluation domain is the
825 result of merging all contexts.
826 :param list group_by_seq: list of domains (which may be in a different
827 order than the ``contexts`` parameter),
828 evaluated in sequence, their ``'group_by'``
829 key is extracted if they have one.
834 the global context created by merging all of
838 the concatenation of all domains
841 a list of fields to group by, potentially empty (in which case
842 no group by should be performed)
844 context, domain = eval_context_and_domain(req.session,
845 common.nonliterals.CompoundContext(*(contexts or [])),
846 common.nonliterals.CompoundDomain(*(domains or [])))
848 group_by_sequence = []
849 for candidate in (group_by_seq or []):
850 ctx = req.session.eval_context(candidate, context)
851 group_by = ctx.get('group_by')
854 elif isinstance(group_by, basestring):
855 group_by_sequence.append(group_by)
857 group_by_sequence.extend(group_by)
862 'group_by': group_by_sequence
865 @openerpweb.jsonrequest
866 def save_session_action(self, req, the_action):
868 This method store an action object in the session object and returns an integer
869 identifying that action. The method get_session_action() can be used to get
872 :param the_action: The action to save in the session.
873 :type the_action: anything
874 :return: A key identifying the saved action.
877 saved_actions = req.httpsession.get('saved_actions')
878 if not saved_actions:
879 saved_actions = {"next":0, "actions":{}}
880 req.httpsession['saved_actions'] = saved_actions
881 # we don't allow more than 10 stored actions
882 if len(saved_actions["actions"]) >= 10:
883 del saved_actions["actions"][min(saved_actions["actions"])]
884 key = saved_actions["next"]
885 saved_actions["actions"][key] = the_action
886 saved_actions["next"] = key + 1
889 @openerpweb.jsonrequest
890 def get_session_action(self, req, key):
892 Gets back a previously saved action. This method can return None if the action
893 was saved since too much time (this case should be handled in a smart way).
895 :param key: The key given by save_session_action()
897 :return: The saved action or None.
900 saved_actions = req.httpsession.get('saved_actions')
901 if not saved_actions:
903 return saved_actions["actions"].get(key)
905 @openerpweb.jsonrequest
906 def check(self, req):
907 req.session.assert_valid()
910 @openerpweb.jsonrequest
911 def destroy(self, req):
912 req.session._suicide = True
914 class Menu(openerpweb.Controller):
915 _cp_path = "/web/menu"
917 @openerpweb.jsonrequest
919 return {'data': self.do_load(req)}
921 @openerpweb.jsonrequest
922 def get_user_roots(self, req):
923 return self.do_get_user_roots(req)
925 def do_get_user_roots(self, req):
926 """ Return all root menu ids visible for the session user.
928 :param req: A request object, with an OpenERP session attribute
929 :type req: < session -> OpenERPSession >
930 :return: the root menu ids
934 context = s.eval_context(req.context)
935 Menus = s.model('ir.ui.menu')
936 # If a menu action is defined use its domain to get the root menu items
937 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
939 menu_domain = [('parent_id', '=', False)]
941 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
943 menu_domain = ast.literal_eval(domain_string)
945 return Menus.search(menu_domain, 0, False, False, context)
947 def do_load(self, req):
948 """ Loads all menu items (all applications and their sub-menus).
950 :param req: A request object, with an OpenERP session attribute
951 :type req: < session -> OpenERPSession >
952 :return: the menu root
953 :rtype: dict('children': menu_nodes)
955 context = req.session.eval_context(req.context)
956 Menus = req.session.model('ir.ui.menu')
958 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
959 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
961 # menus are loaded fully unlike a regular tree view, cause there are a
962 # limited number of items (752 when all 6.1 addons are installed)
963 menu_ids = Menus.search([], 0, False, False, context)
964 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
965 # adds roots at the end of the sequence, so that they will overwrite
966 # equivalent menu items from full menu read when put into id:item
967 # mapping, resulting in children being correctly set on the roots.
968 menu_items.extend(menu_roots)
970 # make a tree using parent_id
971 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
972 for menu_item in menu_items:
973 if menu_item['parent_id']:
974 parent = menu_item['parent_id'][0]
977 if parent in menu_items_map:
978 menu_items_map[parent].setdefault(
979 'children', []).append(menu_item)
981 # sort by sequence a tree using parent_id
982 for menu_item in menu_items:
983 menu_item.setdefault('children', []).sort(
984 key=operator.itemgetter('sequence'))
988 @openerpweb.jsonrequest
989 def action(self, req, menu_id):
990 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
991 [('ir.ui.menu', menu_id)], False)
992 return {"action": actions}
994 class DataSet(openerpweb.Controller):
995 _cp_path = "/web/dataset"
997 @openerpweb.jsonrequest
998 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
999 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1000 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1002 """ Performs a search() followed by a read() (if needed) using the
1003 provided search criteria
1005 :param req: a JSON-RPC request object
1006 :type req: openerpweb.JsonRequest
1007 :param str model: the name of the model to search on
1008 :param fields: a list of the fields to return in the result records
1010 :param int offset: from which index should the results start being returned
1011 :param int limit: the maximum number of records to return
1012 :param list domain: the search domain for the query
1013 :param list sort: sorting directives
1014 :returns: A structure (dict) with two keys: ids (all the ids matching
1015 the (domain, context) pair) and records (paginated records
1016 matching fields selection set)
1019 Model = req.session.model(model)
1021 context, domain = eval_context_and_domain(
1022 req.session, req.context, domain)
1024 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1025 if limit and len(ids) == limit:
1026 length = Model.search_count(domain, context)
1028 length = len(ids) + (offset or 0)
1029 if fields and fields == ['id']:
1030 # shortcut read if we only want the ids
1033 'records': [{'id': id} for id in ids]
1036 records = Model.read(ids, fields or False, context)
1037 records.sort(key=lambda obj: ids.index(obj['id']))
1043 @openerpweb.jsonrequest
1044 def load(self, req, model, id, fields):
1045 m = req.session.model(model)
1047 r = m.read([id], False, req.session.eval_context(req.context))
1050 return {'value': value}
1052 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1053 has_domain = domain_id is not None and domain_id < len(args)
1054 has_context = context_id is not None and context_id < len(args)
1056 domain = args[domain_id] if has_domain else []
1057 context = args[context_id] if has_context else {}
1058 c, d = eval_context_and_domain(req.session, context, domain)
1062 args[context_id] = c
1064 return self._call_kw(req, model, method, args, {})
1066 def _call_kw(self, req, model, method, args, kwargs):
1067 for i in xrange(len(args)):
1068 if isinstance(args[i], common.nonliterals.BaseContext):
1069 args[i] = req.session.eval_context(args[i])
1070 elif isinstance(args[i], common.nonliterals.BaseDomain):
1071 args[i] = req.session.eval_domain(args[i])
1072 for k in kwargs.keys():
1073 if isinstance(kwargs[k], common.nonliterals.BaseContext):
1074 kwargs[k] = req.session.eval_context(kwargs[k])
1075 elif isinstance(kwargs[k], common.nonliterals.BaseDomain):
1076 kwargs[k] = req.session.eval_domain(kwargs[k])
1078 # Temporary implements future display_name special field for model#read()
1079 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1080 if 'display_name' in args[1]:
1081 names = req.session.model(model).name_get(args[0], **kwargs)
1082 args[1].remove('display_name')
1083 r = getattr(req.session.model(model), method)(*args, **kwargs)
1084 for i in range(len(r)):
1085 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1088 return getattr(req.session.model(model), method)(*args, **kwargs)
1090 @openerpweb.jsonrequest
1091 def onchange(self, req, model, method, args, context_id=None):
1092 """ Support method for handling onchange calls: behaves much like call
1093 with the following differences:
1095 * Does not take a domain_id
1096 * Is aware of the return value's structure, and will parse the domains
1097 if needed in order to return either parsed literal domains (in JSON)
1098 or non-literal domain instances, allowing those domains to be used
1102 :type req: web.common.http.JsonRequest
1103 :param str model: object type on which to call the method
1104 :param str method: name of the onchange handler method
1105 :param list args: arguments to call the onchange handler with
1106 :param int context_id: index of the context object in the list of
1108 :return: result of the onchange call with all domains parsed
1110 result = self.call_common(req, model, method, args, context_id=context_id)
1111 if not result or 'domain' not in result:
1114 result['domain'] = dict(
1115 (k, parse_domain(v, req.session))
1116 for k, v in result['domain'].iteritems())
1120 @openerpweb.jsonrequest
1121 def call(self, req, model, method, args, domain_id=None, context_id=None):
1122 return self.call_common(req, model, method, args, domain_id, context_id)
1124 @openerpweb.jsonrequest
1125 def call_kw(self, req, model, method, args, kwargs):
1126 return self._call_kw(req, model, method, args, kwargs)
1128 @openerpweb.jsonrequest
1129 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1130 action = self.call_common(req, model, method, args, domain_id, context_id)
1131 if isinstance(action, dict) and action.get('type') != '':
1132 return {'result': clean_action(req, action)}
1133 return {'result': False}
1135 @openerpweb.jsonrequest
1136 def exec_workflow(self, req, model, id, signal):
1137 return req.session.exec_workflow(model, id, signal)
1139 class DataGroup(openerpweb.Controller):
1140 _cp_path = "/web/group"
1141 @openerpweb.jsonrequest
1142 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1143 Model = req.session.model(model)
1144 context, domain = eval_context_and_domain(req.session, req.context, domain)
1146 return Model.read_group(
1147 domain or [], fields, group_by_fields, 0, False,
1148 dict(context, group_by=group_by_fields), sort or False)
1150 class View(openerpweb.Controller):
1151 _cp_path = "/web/view"
1153 def fields_view_get(self, req, model, view_id, view_type,
1154 transform=True, toolbar=False, submenu=False):
1155 Model = req.session.model(model)
1156 context = req.session.eval_context(req.context)
1157 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1158 # todo fme?: check that we should pass the evaluated context here
1159 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1160 if toolbar and transform:
1161 self.process_toolbar(req, fvg['toolbar'])
1164 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1165 # depending on how it feels, xmlrpclib.ServerProxy can translate
1166 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1167 # enjoy unicode strings which can not be trivially converted to
1168 # strings, and it blows up during parsing.
1170 # So ensure we fix this retardation by converting view xml back to
1172 if isinstance(fvg['arch'], unicode):
1173 arch = fvg['arch'].encode('utf-8')
1176 fvg['arch_string'] = arch
1179 evaluation_context = session.evaluation_context(context or {})
1180 xml = self.transform_view(arch, session, evaluation_context)
1182 xml = ElementTree.fromstring(arch)
1183 fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
1185 if 'id' in fvg['fields']:
1186 # Special case for id's
1187 id_field = fvg['fields']['id']
1188 id_field['original_type'] = id_field['type']
1189 id_field['type'] = 'id'
1191 for field in fvg['fields'].itervalues():
1192 if field.get('views'):
1193 for view in field["views"].itervalues():
1194 self.process_view(session, view, None, transform)
1195 if field.get('domain'):
1196 field["domain"] = parse_domain(field["domain"], session)
1197 if field.get('context'):
1198 field["context"] = parse_context(field["context"], session)
1200 def process_toolbar(self, req, toolbar):
1202 The toolbar is a mapping of section_key: [action_descriptor]
1204 We need to clean all those actions in order to ensure correct
1207 for actions in toolbar.itervalues():
1208 for action in actions:
1209 if 'context' in action:
1210 action['context'] = parse_context(
1211 action['context'], req.session)
1212 if 'domain' in action:
1213 action['domain'] = parse_domain(
1214 action['domain'], req.session)
1216 @openerpweb.jsonrequest
1217 def add_custom(self, req, view_id, arch):
1218 CustomView = req.session.model('ir.ui.view.custom')
1220 'user_id': req.session._uid,
1223 }, req.session.eval_context(req.context))
1224 return {'result': True}
1226 @openerpweb.jsonrequest
1227 def undo_custom(self, req, view_id, reset=False):
1228 CustomView = req.session.model('ir.ui.view.custom')
1229 context = req.session.eval_context(req.context)
1230 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1231 0, False, False, context)
1234 CustomView.unlink(vcustom, context)
1236 CustomView.unlink([vcustom[0]], context)
1237 return {'result': True}
1238 return {'result': False}
1240 def transform_view(self, view_string, session, context=None):
1241 # transform nodes on the fly via iterparse, instead of
1242 # doing it statically on the parsing result
1243 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1245 for event, elem in parser:
1246 if event == "start":
1249 self.parse_domains_and_contexts(elem, session)
1252 def parse_domains_and_contexts(self, elem, session):
1253 """ Converts domains and contexts from the view into Python objects,
1254 either literals if they can be parsed by literal_eval or a special
1255 placeholder object if the domain or context refers to free variables.
1257 :param elem: the current node being parsed
1258 :type param: xml.etree.ElementTree.Element
1259 :param session: OpenERP session object, used to store and retrieve
1261 :type session: openerpweb.openerpweb.OpenERPSession
1263 for el in ['domain', 'filter_domain']:
1264 domain = elem.get(el, '').strip()
1266 elem.set(el, parse_domain(domain, session))
1267 elem.set(el + '_string', domain)
1268 for el in ['context', 'default_get']:
1269 context_string = elem.get(el, '').strip()
1271 elem.set(el, parse_context(context_string, session))
1272 elem.set(el + '_string', context_string)
1274 @openerpweb.jsonrequest
1275 def load(self, req, model, view_id, view_type, toolbar=False):
1276 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1278 class ListView(View):
1279 _cp_path = "/web/listview"
1281 def process_colors(self, view, row, context):
1282 colors = view['arch']['attrs'].get('colors')
1289 for pair in colors.split(';')
1290 if eval(pair.split(':')[1], dict(context, **row))
1295 elif len(color) == 1:
1299 class TreeView(View):
1300 _cp_path = "/web/treeview"
1302 @openerpweb.jsonrequest
1303 def action(self, req, model, id):
1304 return load_actions_from_ir_values(
1305 req,'action', 'tree_but_open',[(model, id)],
1308 class SearchView(View):
1309 _cp_path = "/web/searchview"
1311 @openerpweb.jsonrequest
1312 def load(self, req, model, view_id):
1313 fields_view = self.fields_view_get(req, model, view_id, 'search')
1314 return {'fields_view': fields_view}
1316 @openerpweb.jsonrequest
1317 def fields_get(self, req, model):
1318 Model = req.session.model(model)
1319 fields = Model.fields_get(False, req.session.eval_context(req.context))
1320 for field in fields.values():
1321 # shouldn't convert the views too?
1322 if field.get('domain'):
1323 field["domain"] = parse_domain(field["domain"], req.session)
1324 if field.get('context'):
1325 field["context"] = parse_context(field["context"], req.session)
1326 return {'fields': fields}
1328 @openerpweb.jsonrequest
1329 def get_filters(self, req, model):
1330 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1331 Model = req.session.model("ir.filters")
1332 filters = Model.get_filters(model)
1333 for filter in filters:
1335 parsed_context = parse_context(filter["context"], req.session)
1336 filter["context"] = (parsed_context
1337 if not isinstance(parsed_context, common.nonliterals.BaseContext)
1338 else req.session.eval_context(parsed_context))
1340 parsed_domain = parse_domain(filter["domain"], req.session)
1341 filter["domain"] = (parsed_domain
1342 if not isinstance(parsed_domain, common.nonliterals.BaseDomain)
1343 else req.session.eval_domain(parsed_domain))
1345 logger.exception("Failed to parse custom filter %s in %s",
1346 filter['name'], model)
1347 filter['disabled'] = True
1348 del filter['context']
1349 del filter['domain']
1352 class Binary(openerpweb.Controller):
1353 _cp_path = "/web/binary"
1355 @openerpweb.httprequest
1356 def image(self, req, model, id, field, **kw):
1357 last_update = '__last_update'
1358 Model = req.session.model(model)
1359 context = req.session.eval_context(req.context)
1360 headers = [('Content-Type', 'image/png')]
1361 etag = req.httprequest.headers.get('If-None-Match')
1362 hashed_session = hashlib.md5(req.session_id).hexdigest()
1363 id = None if not id else simplejson.loads(id)
1364 if type(id) is list:
1367 if not id and hashed_session == etag:
1368 return werkzeug.wrappers.Response(status=304)
1370 date = Model.read([id], [last_update], context)[0].get(last_update)
1371 if hashlib.md5(date).hexdigest() == etag:
1372 return werkzeug.wrappers.Response(status=304)
1374 retag = hashed_session
1377 res = Model.default_get([field], context).get(field)
1378 image_data = base64.b64decode(res)
1380 res = Model.read([id], [last_update, field], context)[0]
1381 retag = hashlib.md5(res.get(last_update)).hexdigest()
1382 image_data = base64.b64decode(res.get(field))
1383 except (TypeError, xmlrpclib.Fault):
1384 image_data = self.placeholder(req)
1385 headers.append(('ETag', retag))
1386 headers.append(('Content-Length', len(image_data)))
1388 ncache = int(kw.get('cache'))
1389 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1392 return req.make_response(image_data, headers)
1393 def placeholder(self, req):
1394 addons_path = openerpweb.addons_manifest['web']['addons_path']
1395 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1396 def content_disposition(self, filename, req):
1397 filename = filename.encode('utf8')
1398 escaped = urllib2.quote(filename)
1399 browser = req.httprequest.user_agent.browser
1400 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
1401 if browser == 'msie' and version < 9:
1402 return "attachment; filename=%s" % escaped
1403 elif browser == 'safari':
1404 return "attachment; filename=%s" % filename
1406 return "attachment; filename*=UTF-8''%s" % escaped
1408 @openerpweb.httprequest
1409 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1410 """ Download link for files stored as binary fields.
1412 If the ``id`` parameter is omitted, fetches the default value for the
1413 binary field (via ``default_get``), otherwise fetches the field for
1414 that precise record.
1416 :param req: OpenERP request
1417 :type req: :class:`web.common.http.HttpRequest`
1418 :param str model: name of the model to fetch the binary from
1419 :param str field: binary field
1420 :param str id: id of the record from which to fetch the binary
1421 :param str filename_field: field holding the file's name, if any
1422 :returns: :class:`werkzeug.wrappers.Response`
1424 Model = req.session.model(model)
1425 context = req.session.eval_context(req.context)
1428 fields.append(filename_field)
1430 res = Model.read([int(id)], fields, context)[0]
1432 res = Model.default_get(fields, context)
1433 filecontent = base64.b64decode(res.get(field, ''))
1435 return req.not_found()
1437 filename = '%s_%s' % (model.replace('.', '_'), id)
1439 filename = res.get(filename_field, '') or filename
1440 return req.make_response(filecontent,
1441 [('Content-Type', 'application/octet-stream'),
1442 ('Content-Disposition', self.content_disposition(filename, req))])
1444 @openerpweb.httprequest
1445 def saveas_ajax(self, req, data, token):
1446 jdata = simplejson.loads(data)
1447 model = jdata['model']
1448 field = jdata['field']
1449 id = jdata.get('id', None)
1450 filename_field = jdata.get('filename_field', None)
1451 context = jdata.get('context', dict())
1453 context = req.session.eval_context(context)
1454 Model = req.session.model(model)
1457 fields.append(filename_field)
1459 res = Model.read([int(id)], fields, context)[0]
1461 res = Model.default_get(fields, context)
1462 filecontent = base64.b64decode(res.get(field, ''))
1464 raise ValueError("No content found for field '%s' on '%s:%s'" %
1467 filename = '%s_%s' % (model.replace('.', '_'), id)
1469 filename = res.get(filename_field, '') or filename
1470 return req.make_response(filecontent,
1471 headers=[('Content-Type', 'application/octet-stream'),
1472 ('Content-Disposition', self.content_disposition(filename, req))],
1473 cookies={'fileToken': int(token)})
1475 @openerpweb.httprequest
1476 def upload(self, req, callback, ufile):
1477 # TODO: might be useful to have a configuration flag for max-length file uploads
1479 out = """<script language="javascript" type="text/javascript">
1480 var win = window.top.window;
1481 win.jQuery(win).trigger(%s, %s);
1484 args = [len(data), ufile.filename,
1485 ufile.content_type, base64.b64encode(data)]
1486 except Exception, e:
1487 args = [False, e.message]
1488 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1490 @openerpweb.httprequest
1491 def upload_attachment(self, req, callback, model, id, ufile):
1492 context = req.session.eval_context(req.context)
1493 Model = req.session.model('ir.attachment')
1495 out = """<script language="javascript" type="text/javascript">
1496 var win = window.top.window;
1497 win.jQuery(win).trigger(%s, %s);
1499 attachment_id = Model.create({
1500 'name': ufile.filename,
1501 'datas': base64.encodestring(ufile.read()),
1502 'datas_fname': ufile.filename,
1507 'filename': ufile.filename,
1510 except Exception, e:
1511 args = { 'error': e.message }
1512 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1514 class Action(openerpweb.Controller):
1515 _cp_path = "/web/action"
1517 # For most actions, the type attribute and the model name are the same, but
1518 # there are exceptions. This dict is used to remap action type attributes
1519 # to the "real" model name when they differ.
1521 "ir.actions.act_url": "ir.actions.url",
1524 @openerpweb.jsonrequest
1525 def load(self, req, action_id, do_not_eval=False):
1526 Actions = req.session.model('ir.actions.actions')
1528 context = req.session.eval_context(req.context)
1531 action_id = int(action_id)
1534 module, xmlid = action_id.split('.', 1)
1535 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1536 assert model.startswith('ir.actions.')
1538 action_id = 0 # force failed read
1540 base_action = Actions.read([action_id], ['type'], context)
1543 action_type = base_action[0]['type']
1544 if action_type == 'ir.actions.report.xml':
1545 ctx.update({'bin_size': True})
1547 action_model = self.action_mapping.get(action_type, action_type)
1548 action = req.session.model(action_model).read([action_id], False, ctx)
1550 value = clean_action(req, action[0], do_not_eval)
1551 return {'result': value}
1553 @openerpweb.jsonrequest
1554 def run(self, req, action_id):
1555 return_action = req.session.model('ir.actions.server').run(
1556 [action_id], req.session.eval_context(req.context))
1558 return clean_action(req, return_action)
1563 _cp_path = "/web/export"
1565 @openerpweb.jsonrequest
1566 def formats(self, req):
1567 """ Returns all valid export formats
1569 :returns: for each export format, a pair of identifier and printable name
1570 :rtype: [(str, str)]
1574 for path, controller in openerpweb.controllers_path.iteritems()
1575 if path.startswith(self._cp_path)
1576 if hasattr(controller, 'fmt')
1577 ], key=operator.itemgetter("label"))
1579 def fields_get(self, req, model):
1580 Model = req.session.model(model)
1581 fields = Model.fields_get(False, req.session.eval_context(req.context))
1584 @openerpweb.jsonrequest
1585 def get_fields(self, req, model, prefix='', parent_name= '',
1586 import_compat=True, parent_field_type=None,
1589 if import_compat and parent_field_type == "many2one":
1592 fields = self.fields_get(req, model)
1595 fields.pop('id', None)
1597 fields['.id'] = fields.pop('id', {'string': 'ID'})
1599 fields_sequence = sorted(fields.iteritems(),
1600 key=lambda field: field[1].get('string', ''))
1603 for field_name, field in fields_sequence:
1605 if exclude and field_name in exclude:
1607 if field.get('readonly'):
1608 # If none of the field's states unsets readonly, skip the field
1609 if all(dict(attrs).get('readonly', True)
1610 for attrs in field.get('states', {}).values()):
1613 id = prefix + (prefix and '/'or '') + field_name
1614 name = parent_name + (parent_name and '/' or '') + field['string']
1615 record = {'id': id, 'string': name,
1616 'value': id, 'children': False,
1617 'field_type': field.get('type'),
1618 'required': field.get('required'),
1619 'relation_field': field.get('relation_field')}
1620 records.append(record)
1622 if len(name.split('/')) < 3 and 'relation' in field:
1623 ref = field.pop('relation')
1624 record['value'] += '/id'
1625 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1627 if not import_compat or field['type'] == 'one2many':
1628 # m2m field in import_compat is childless
1629 record['children'] = True
1633 @openerpweb.jsonrequest
1634 def namelist(self,req, model, export_id):
1635 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1636 export = req.session.model("ir.exports").read([export_id])[0]
1637 export_fields_list = req.session.model("ir.exports.line").read(
1638 export['export_fields'])
1640 fields_data = self.fields_info(
1641 req, model, map(operator.itemgetter('name'), export_fields_list))
1644 {'name': field['name'], 'label': fields_data[field['name']]}
1645 for field in export_fields_list
1648 def fields_info(self, req, model, export_fields):
1650 fields = self.fields_get(req, model)
1652 # To make fields retrieval more efficient, fetch all sub-fields of a
1653 # given field at the same time. Because the order in the export list is
1654 # arbitrary, this requires ordering all sub-fields of a given field
1655 # together so they can be fetched at the same time
1657 # Works the following way:
1658 # * sort the list of fields to export, the default sorting order will
1659 # put the field itself (if present, for xmlid) and all of its
1660 # sub-fields right after it
1661 # * then, group on: the first field of the path (which is the same for
1662 # a field and for its subfields and the length of splitting on the
1663 # first '/', which basically means grouping the field on one side and
1664 # all of the subfields on the other. This way, we have the field (for
1665 # the xmlid) with length 1, and all of the subfields with the same
1666 # base but a length "flag" of 2
1667 # * if we have a normal field (length 1), just add it to the info
1668 # mapping (with its string) as-is
1669 # * otherwise, recursively call fields_info via graft_subfields.
1670 # all graft_subfields does is take the result of fields_info (on the
1671 # field's model) and prepend the current base (current field), which
1672 # rebuilds the whole sub-tree for the field
1674 # result: because we're not fetching the fields_get for half the
1675 # database models, fetching a namelist with a dozen fields (including
1676 # relational data) falls from ~6s to ~300ms (on the leads model).
1677 # export lists with no sub-fields (e.g. import_compatible lists with
1678 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1679 # there's a single fields_get to execute)
1680 for (base, length), subfields in itertools.groupby(
1681 sorted(export_fields),
1682 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1683 subfields = list(subfields)
1685 # subfields is a seq of $base/*rest, and not loaded yet
1686 info.update(self.graft_subfields(
1687 req, fields[base]['relation'], base, fields[base]['string'],
1691 info[base] = fields[base]['string']
1695 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1696 export_fields = [field.split('/', 1)[1] for field in fields]
1698 (prefix + '/' + k, prefix_string + '/' + v)
1699 for k, v in self.fields_info(req, model, export_fields).iteritems())
1701 #noinspection PyPropertyDefinition
1703 def content_type(self):
1704 """ Provides the format's content type """
1705 raise NotImplementedError()
1707 def filename(self, base):
1708 """ Creates a valid filename for the format (with extension) from the
1709 provided base name (exension-less)
1711 raise NotImplementedError()
1713 def from_data(self, fields, rows):
1714 """ Conversion method from OpenERP's export data to whatever the
1715 current export class outputs
1717 :params list fields: a list of fields to export
1718 :params list rows: a list of records to export
1722 raise NotImplementedError()
1724 @openerpweb.httprequest
1725 def index(self, req, data, token):
1726 model, fields, ids, domain, import_compat = \
1727 operator.itemgetter('model', 'fields', 'ids', 'domain',
1729 simplejson.loads(data))
1731 context = req.session.eval_context(req.context)
1732 Model = req.session.model(model)
1733 ids = ids or Model.search(domain, 0, False, False, context)
1735 field_names = map(operator.itemgetter('name'), fields)
1736 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1739 columns_headers = field_names
1741 columns_headers = [val['label'].strip() for val in fields]
1744 return req.make_response(self.from_data(columns_headers, import_data),
1745 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1746 ('Content-Type', self.content_type)],
1747 cookies={'fileToken': int(token)})
1749 class CSVExport(Export):
1750 _cp_path = '/web/export/csv'
1751 fmt = {'tag': 'csv', 'label': 'CSV'}
1754 def content_type(self):
1755 return 'text/csv;charset=utf8'
1757 def filename(self, base):
1758 return base + '.csv'
1760 def from_data(self, fields, rows):
1762 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1764 writer.writerow([name.encode('utf-8') for name in fields])
1769 if isinstance(d, basestring):
1770 d = d.replace('\n',' ').replace('\t',' ')
1772 d = d.encode('utf-8')
1773 except UnicodeError:
1775 if d is False: d = None
1777 writer.writerow(row)
1784 class ExcelExport(Export):
1785 _cp_path = '/web/export/xls'
1789 'error': None if xlwt else "XLWT required"
1793 def content_type(self):
1794 return 'application/vnd.ms-excel'
1796 def filename(self, base):
1797 return base + '.xls'
1799 def from_data(self, fields, rows):
1800 workbook = xlwt.Workbook()
1801 worksheet = workbook.add_sheet('Sheet 1')
1803 for i, fieldname in enumerate(fields):
1804 worksheet.write(0, i, fieldname)
1805 worksheet.col(i).width = 8000 # around 220 pixels
1807 style = xlwt.easyxf('align: wrap yes')
1809 for row_index, row in enumerate(rows):
1810 for cell_index, cell_value in enumerate(row):
1811 if isinstance(cell_value, basestring):
1812 cell_value = re.sub("\r", " ", cell_value)
1813 if cell_value is False: cell_value = None
1814 worksheet.write(row_index + 1, cell_index, cell_value, style)
1823 class Reports(View):
1824 _cp_path = "/web/report"
1825 POLLING_DELAY = 0.25
1827 'doc': 'application/vnd.ms-word',
1828 'html': 'text/html',
1829 'odt': 'application/vnd.oasis.opendocument.text',
1830 'pdf': 'application/pdf',
1831 'sxw': 'application/vnd.sun.xml.writer',
1832 'xls': 'application/vnd.ms-excel',
1835 @openerpweb.httprequest
1836 def index(self, req, action, token):
1837 action = simplejson.loads(action)
1839 report_srv = req.session.proxy("report")
1840 context = req.session.eval_context(
1841 common.nonliterals.CompoundContext(
1842 req.context or {}, action[ "context"]))
1845 report_ids = context["active_ids"]
1846 if 'report_type' in action:
1847 report_data['report_type'] = action['report_type']
1848 if 'datas' in action:
1849 if 'ids' in action['datas']:
1850 report_ids = action['datas'].pop('ids')
1851 report_data.update(action['datas'])
1853 report_id = report_srv.report(
1854 req.session._db, req.session._uid, req.session._password,
1855 action["report_name"], report_ids,
1856 report_data, context)
1858 report_struct = None
1860 report_struct = report_srv.report_get(
1861 req.session._db, req.session._uid, req.session._password, report_id)
1862 if report_struct["state"]:
1865 time.sleep(self.POLLING_DELAY)
1867 report = base64.b64decode(report_struct['result'])
1868 if report_struct.get('code') == 'zlib':
1869 report = zlib.decompress(report)
1870 report_mimetype = self.TYPES_MAPPING.get(
1871 report_struct['format'], 'octet-stream')
1873 if 'name' not in action:
1874 reports = req.session.model('ir.actions.report.xml')
1875 res_id = reports.search([('report_name', '=', action['report_name']),],
1876 0, False, False, context)
1878 file_name = reports.read(res_id[0], ['name'], context)['name']
1880 file_name = action['report_name']
1882 return req.make_response(report,
1884 # maybe we should take of what characters can appear in a file name?
1885 ('Content-Disposition', 'attachment; filename="%s.%s"' % (file_name, report_struct['format'])),
1886 ('Content-Type', report_mimetype),
1887 ('Content-Length', len(report))],
1888 cookies={'fileToken': int(token)})
1891 _cp_path = "/web/import"
1893 def fields_get(self, req, model):
1894 Model = req.session.model(model)
1895 fields = Model.fields_get(False, req.session.eval_context(req.context))
1898 @openerpweb.httprequest
1899 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1901 data = list(csv.reader(
1902 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1903 except csv.Error, e:
1905 return '<script>window.top.%s(%s);</script>' % (
1906 jsonp, simplejson.dumps({'error': {
1907 'message': 'Error parsing CSV file: %s' % e,
1908 # decodes each byte to a unicode character, which may or
1909 # may not be printable, but decoding will succeed.
1910 # Otherwise simplejson will try to decode the `str` using
1911 # utf-8, which is very likely to blow up on characters out
1912 # of the ascii range (in range [128, 256))
1913 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1916 return '<script>window.top.%s(%s);</script>' % (
1917 jsonp, simplejson.dumps(
1918 {'records': data[:10]}, encoding=csvcode))
1919 except UnicodeDecodeError:
1920 return '<script>window.top.%s(%s);</script>' % (
1921 jsonp, simplejson.dumps({
1922 'message': u"Failed to decode CSV file using encoding %s, "
1923 u"try switching to a different encoding" % csvcode
1926 @openerpweb.httprequest
1927 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1929 modle_obj = req.session.model(model)
1930 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1931 simplejson.loads(meta))
1934 if not (csvdel and len(csvdel) == 1):
1935 error = u"The CSV delimiter must be a single character"
1937 if not indices and fields:
1938 error = u"You must select at least one field to import"
1941 return '<script>window.top.%s(%s);</script>' % (
1942 jsonp, simplejson.dumps({'error': {'message': error}}))
1944 # skip ignored records (@skip parameter)
1945 # then skip empty lines (not valid csv)
1946 # nb: should these operations be reverted?
1947 rows_to_import = itertools.ifilter(
1950 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1953 # if only one index, itemgetter will return an atom rather than a tuple
1954 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1955 else: mapper = operator.itemgetter(*indices)
1960 # decode each data row
1962 [record.decode(csvcode) for record in row]
1963 for row in itertools.imap(mapper, rows_to_import)
1964 # don't insert completely empty rows (can happen due to fields
1965 # filtering in case of e.g. o2m content rows)
1968 except UnicodeDecodeError:
1969 error = u"Failed to decode CSV file using encoding %s" % csvcode
1970 except csv.Error, e:
1971 error = u"Could not process CSV file: %s" % e
1973 # If the file contains nothing,
1975 error = u"File to import is empty"
1977 return '<script>window.top.%s(%s);</script>' % (
1978 jsonp, simplejson.dumps({'error': {'message': error}}))
1981 (code, record, message, _nope) = modle_obj.import_data(
1982 fields, data, 'init', '', False,
1983 req.session.eval_context(req.context))
1984 except xmlrpclib.Fault, e:
1985 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1986 return '<script>window.top.%s(%s);</script>' % (
1987 jsonp, simplejson.dumps({'error':error}))
1990 return '<script>window.top.%s(%s);</script>' % (
1991 jsonp, simplejson.dumps({'success':True}))
1993 msg = u"Error during import: %s\n\nTrying to import record %r" % (
1995 return '<script>window.top.%s(%s);</script>' % (
1996 jsonp, simplejson.dumps({'error': {'message':msg}}))
1998 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: