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 @openerpweb.jsonrequest
1140 def resequence(self, req, model, ids):
1141 m = req.session.model(model)
1142 if not len(m.fields_get(['sequence'])):
1144 for i in range(len(ids)):
1145 m.write([ids[i]], { 'sequence': i })
1148 class DataGroup(openerpweb.Controller):
1149 _cp_path = "/web/group"
1150 @openerpweb.jsonrequest
1151 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1152 Model = req.session.model(model)
1153 context, domain = eval_context_and_domain(req.session, req.context, domain)
1155 return Model.read_group(
1156 domain or [], fields, group_by_fields, 0, False,
1157 dict(context, group_by=group_by_fields), sort or False)
1159 class View(openerpweb.Controller):
1160 _cp_path = "/web/view"
1162 def fields_view_get(self, req, model, view_id, view_type,
1163 transform=True, toolbar=False, submenu=False):
1164 Model = req.session.model(model)
1165 context = req.session.eval_context(req.context)
1166 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1167 # todo fme?: check that we should pass the evaluated context here
1168 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1169 if toolbar and transform:
1170 self.process_toolbar(req, fvg['toolbar'])
1173 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1174 # depending on how it feels, xmlrpclib.ServerProxy can translate
1175 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1176 # enjoy unicode strings which can not be trivially converted to
1177 # strings, and it blows up during parsing.
1179 # So ensure we fix this retardation by converting view xml back to
1181 if isinstance(fvg['arch'], unicode):
1182 arch = fvg['arch'].encode('utf-8')
1185 fvg['arch_string'] = arch
1188 evaluation_context = session.evaluation_context(context or {})
1189 xml = self.transform_view(arch, session, evaluation_context)
1191 xml = ElementTree.fromstring(arch)
1192 fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
1194 if 'id' in fvg['fields']:
1195 # Special case for id's
1196 id_field = fvg['fields']['id']
1197 id_field['original_type'] = id_field['type']
1198 id_field['type'] = 'id'
1200 for field in fvg['fields'].itervalues():
1201 if field.get('views'):
1202 for view in field["views"].itervalues():
1203 self.process_view(session, view, None, transform)
1204 if field.get('domain'):
1205 field["domain"] = parse_domain(field["domain"], session)
1206 if field.get('context'):
1207 field["context"] = parse_context(field["context"], session)
1209 def process_toolbar(self, req, toolbar):
1211 The toolbar is a mapping of section_key: [action_descriptor]
1213 We need to clean all those actions in order to ensure correct
1216 for actions in toolbar.itervalues():
1217 for action in actions:
1218 if 'context' in action:
1219 action['context'] = parse_context(
1220 action['context'], req.session)
1221 if 'domain' in action:
1222 action['domain'] = parse_domain(
1223 action['domain'], req.session)
1225 @openerpweb.jsonrequest
1226 def add_custom(self, req, view_id, arch):
1227 CustomView = req.session.model('ir.ui.view.custom')
1229 'user_id': req.session._uid,
1232 }, req.session.eval_context(req.context))
1233 return {'result': True}
1235 @openerpweb.jsonrequest
1236 def undo_custom(self, req, view_id, reset=False):
1237 CustomView = req.session.model('ir.ui.view.custom')
1238 context = req.session.eval_context(req.context)
1239 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1240 0, False, False, context)
1243 CustomView.unlink(vcustom, context)
1245 CustomView.unlink([vcustom[0]], context)
1246 return {'result': True}
1247 return {'result': False}
1249 def transform_view(self, view_string, session, context=None):
1250 # transform nodes on the fly via iterparse, instead of
1251 # doing it statically on the parsing result
1252 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1254 for event, elem in parser:
1255 if event == "start":
1258 self.parse_domains_and_contexts(elem, session)
1261 def parse_domains_and_contexts(self, elem, session):
1262 """ Converts domains and contexts from the view into Python objects,
1263 either literals if they can be parsed by literal_eval or a special
1264 placeholder object if the domain or context refers to free variables.
1266 :param elem: the current node being parsed
1267 :type param: xml.etree.ElementTree.Element
1268 :param session: OpenERP session object, used to store and retrieve
1270 :type session: openerpweb.openerpweb.OpenERPSession
1272 for el in ['domain', 'filter_domain']:
1273 domain = elem.get(el, '').strip()
1275 elem.set(el, parse_domain(domain, session))
1276 elem.set(el + '_string', domain)
1277 for el in ['context', 'default_get']:
1278 context_string = elem.get(el, '').strip()
1280 elem.set(el, parse_context(context_string, session))
1281 elem.set(el + '_string', context_string)
1283 @openerpweb.jsonrequest
1284 def load(self, req, model, view_id, view_type, toolbar=False):
1285 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1287 class ListView(View):
1288 _cp_path = "/web/listview"
1290 def process_colors(self, view, row, context):
1291 colors = view['arch']['attrs'].get('colors')
1298 for pair in colors.split(';')
1299 if eval(pair.split(':')[1], dict(context, **row))
1304 elif len(color) == 1:
1308 class TreeView(View):
1309 _cp_path = "/web/treeview"
1311 @openerpweb.jsonrequest
1312 def action(self, req, model, id):
1313 return load_actions_from_ir_values(
1314 req,'action', 'tree_but_open',[(model, id)],
1317 class SearchView(View):
1318 _cp_path = "/web/searchview"
1320 @openerpweb.jsonrequest
1321 def load(self, req, model, view_id):
1322 fields_view = self.fields_view_get(req, model, view_id, 'search')
1323 return {'fields_view': fields_view}
1325 @openerpweb.jsonrequest
1326 def fields_get(self, req, model):
1327 Model = req.session.model(model)
1328 fields = Model.fields_get(False, req.session.eval_context(req.context))
1329 for field in fields.values():
1330 # shouldn't convert the views too?
1331 if field.get('domain'):
1332 field["domain"] = parse_domain(field["domain"], req.session)
1333 if field.get('context'):
1334 field["context"] = parse_context(field["context"], req.session)
1335 return {'fields': fields}
1337 @openerpweb.jsonrequest
1338 def get_filters(self, req, model):
1339 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1340 Model = req.session.model("ir.filters")
1341 filters = Model.get_filters(model)
1342 for filter in filters:
1344 parsed_context = parse_context(filter["context"], req.session)
1345 filter["context"] = (parsed_context
1346 if not isinstance(parsed_context, common.nonliterals.BaseContext)
1347 else req.session.eval_context(parsed_context))
1349 parsed_domain = parse_domain(filter["domain"], req.session)
1350 filter["domain"] = (parsed_domain
1351 if not isinstance(parsed_domain, common.nonliterals.BaseDomain)
1352 else req.session.eval_domain(parsed_domain))
1354 logger.exception("Failed to parse custom filter %s in %s",
1355 filter['name'], model)
1356 filter['disabled'] = True
1357 del filter['context']
1358 del filter['domain']
1361 class Binary(openerpweb.Controller):
1362 _cp_path = "/web/binary"
1364 @openerpweb.httprequest
1365 def image(self, req, model, id, field, **kw):
1366 last_update = '__last_update'
1367 Model = req.session.model(model)
1368 context = req.session.eval_context(req.context)
1369 headers = [('Content-Type', 'image/png')]
1370 etag = req.httprequest.headers.get('If-None-Match')
1371 hashed_session = hashlib.md5(req.session_id).hexdigest()
1372 id = None if not id else simplejson.loads(id)
1373 if type(id) is list:
1376 if not id and hashed_session == etag:
1377 return werkzeug.wrappers.Response(status=304)
1379 date = Model.read([id], [last_update], context)[0].get(last_update)
1380 if hashlib.md5(date).hexdigest() == etag:
1381 return werkzeug.wrappers.Response(status=304)
1383 retag = hashed_session
1386 res = Model.default_get([field], context).get(field)
1387 image_data = base64.b64decode(res)
1389 res = Model.read([id], [last_update, field], context)[0]
1390 retag = hashlib.md5(res.get(last_update)).hexdigest()
1391 image_data = base64.b64decode(res.get(field))
1392 except (TypeError, xmlrpclib.Fault):
1393 image_data = self.placeholder(req)
1394 headers.append(('ETag', retag))
1395 headers.append(('Content-Length', len(image_data)))
1397 ncache = int(kw.get('cache'))
1398 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1401 return req.make_response(image_data, headers)
1402 def placeholder(self, req):
1403 addons_path = openerpweb.addons_manifest['web']['addons_path']
1404 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1405 def content_disposition(self, filename, req):
1406 filename = filename.encode('utf8')
1407 escaped = urllib2.quote(filename)
1408 browser = req.httprequest.user_agent.browser
1409 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
1410 if browser == 'msie' and version < 9:
1411 return "attachment; filename=%s" % escaped
1412 elif browser == 'safari':
1413 return "attachment; filename=%s" % filename
1415 return "attachment; filename*=UTF-8''%s" % escaped
1417 @openerpweb.httprequest
1418 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1419 """ Download link for files stored as binary fields.
1421 If the ``id`` parameter is omitted, fetches the default value for the
1422 binary field (via ``default_get``), otherwise fetches the field for
1423 that precise record.
1425 :param req: OpenERP request
1426 :type req: :class:`web.common.http.HttpRequest`
1427 :param str model: name of the model to fetch the binary from
1428 :param str field: binary field
1429 :param str id: id of the record from which to fetch the binary
1430 :param str filename_field: field holding the file's name, if any
1431 :returns: :class:`werkzeug.wrappers.Response`
1433 Model = req.session.model(model)
1434 context = req.session.eval_context(req.context)
1437 fields.append(filename_field)
1439 res = Model.read([int(id)], fields, context)[0]
1441 res = Model.default_get(fields, context)
1442 filecontent = base64.b64decode(res.get(field, ''))
1444 return req.not_found()
1446 filename = '%s_%s' % (model.replace('.', '_'), id)
1448 filename = res.get(filename_field, '') or filename
1449 return req.make_response(filecontent,
1450 [('Content-Type', 'application/octet-stream'),
1451 ('Content-Disposition', self.content_disposition(filename, req))])
1453 @openerpweb.httprequest
1454 def saveas_ajax(self, req, data, token):
1455 jdata = simplejson.loads(data)
1456 model = jdata['model']
1457 field = jdata['field']
1458 id = jdata.get('id', None)
1459 filename_field = jdata.get('filename_field', None)
1460 context = jdata.get('context', dict())
1462 context = req.session.eval_context(context)
1463 Model = req.session.model(model)
1466 fields.append(filename_field)
1468 res = Model.read([int(id)], fields, context)[0]
1470 res = Model.default_get(fields, context)
1471 filecontent = base64.b64decode(res.get(field, ''))
1473 raise ValueError("No content found for field '%s' on '%s:%s'" %
1476 filename = '%s_%s' % (model.replace('.', '_'), id)
1478 filename = res.get(filename_field, '') or filename
1479 return req.make_response(filecontent,
1480 headers=[('Content-Type', 'application/octet-stream'),
1481 ('Content-Disposition', self.content_disposition(filename, req))],
1482 cookies={'fileToken': int(token)})
1484 @openerpweb.httprequest
1485 def upload(self, req, callback, ufile):
1486 # TODO: might be useful to have a configuration flag for max-length file uploads
1488 out = """<script language="javascript" type="text/javascript">
1489 var win = window.top.window;
1490 win.jQuery(win).trigger(%s, %s);
1493 args = [len(data), ufile.filename,
1494 ufile.content_type, base64.b64encode(data)]
1495 except Exception, e:
1496 args = [False, e.message]
1497 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1499 @openerpweb.httprequest
1500 def upload_attachment(self, req, callback, model, id, ufile):
1501 context = req.session.eval_context(req.context)
1502 Model = req.session.model('ir.attachment')
1504 out = """<script language="javascript" type="text/javascript">
1505 var win = window.top.window;
1506 win.jQuery(win).trigger(%s, %s);
1508 attachment_id = Model.create({
1509 'name': ufile.filename,
1510 'datas': base64.encodestring(ufile.read()),
1511 'datas_fname': ufile.filename,
1516 'filename': ufile.filename,
1519 except Exception, e:
1520 args = { 'error': e.message }
1521 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1523 class Action(openerpweb.Controller):
1524 _cp_path = "/web/action"
1526 # For most actions, the type attribute and the model name are the same, but
1527 # there are exceptions. This dict is used to remap action type attributes
1528 # to the "real" model name when they differ.
1530 "ir.actions.act_url": "ir.actions.url",
1533 @openerpweb.jsonrequest
1534 def load(self, req, action_id, do_not_eval=False):
1535 Actions = req.session.model('ir.actions.actions')
1537 context = req.session.eval_context(req.context)
1540 action_id = int(action_id)
1543 module, xmlid = action_id.split('.', 1)
1544 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1545 assert model.startswith('ir.actions.')
1547 action_id = 0 # force failed read
1549 base_action = Actions.read([action_id], ['type'], context)
1552 action_type = base_action[0]['type']
1553 if action_type == 'ir.actions.report.xml':
1554 ctx.update({'bin_size': True})
1556 action_model = self.action_mapping.get(action_type, action_type)
1557 action = req.session.model(action_model).read([action_id], False, ctx)
1559 value = clean_action(req, action[0], do_not_eval)
1560 return {'result': value}
1562 @openerpweb.jsonrequest
1563 def run(self, req, action_id):
1564 return_action = req.session.model('ir.actions.server').run(
1565 [action_id], req.session.eval_context(req.context))
1567 return clean_action(req, return_action)
1572 _cp_path = "/web/export"
1574 @openerpweb.jsonrequest
1575 def formats(self, req):
1576 """ Returns all valid export formats
1578 :returns: for each export format, a pair of identifier and printable name
1579 :rtype: [(str, str)]
1583 for path, controller in openerpweb.controllers_path.iteritems()
1584 if path.startswith(self._cp_path)
1585 if hasattr(controller, 'fmt')
1586 ], key=operator.itemgetter("label"))
1588 def fields_get(self, req, model):
1589 Model = req.session.model(model)
1590 fields = Model.fields_get(False, req.session.eval_context(req.context))
1593 @openerpweb.jsonrequest
1594 def get_fields(self, req, model, prefix='', parent_name= '',
1595 import_compat=True, parent_field_type=None,
1598 if import_compat and parent_field_type == "many2one":
1601 fields = self.fields_get(req, model)
1604 fields.pop('id', None)
1606 fields['.id'] = fields.pop('id', {'string': 'ID'})
1608 fields_sequence = sorted(fields.iteritems(),
1609 key=lambda field: field[1].get('string', ''))
1612 for field_name, field in fields_sequence:
1614 if exclude and field_name in exclude:
1616 if field.get('readonly'):
1617 # If none of the field's states unsets readonly, skip the field
1618 if all(dict(attrs).get('readonly', True)
1619 for attrs in field.get('states', {}).values()):
1622 id = prefix + (prefix and '/'or '') + field_name
1623 name = parent_name + (parent_name and '/' or '') + field['string']
1624 record = {'id': id, 'string': name,
1625 'value': id, 'children': False,
1626 'field_type': field.get('type'),
1627 'required': field.get('required'),
1628 'relation_field': field.get('relation_field')}
1629 records.append(record)
1631 if len(name.split('/')) < 3 and 'relation' in field:
1632 ref = field.pop('relation')
1633 record['value'] += '/id'
1634 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1636 if not import_compat or field['type'] == 'one2many':
1637 # m2m field in import_compat is childless
1638 record['children'] = True
1642 @openerpweb.jsonrequest
1643 def namelist(self,req, model, export_id):
1644 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1645 export = req.session.model("ir.exports").read([export_id])[0]
1646 export_fields_list = req.session.model("ir.exports.line").read(
1647 export['export_fields'])
1649 fields_data = self.fields_info(
1650 req, model, map(operator.itemgetter('name'), export_fields_list))
1653 {'name': field['name'], 'label': fields_data[field['name']]}
1654 for field in export_fields_list
1657 def fields_info(self, req, model, export_fields):
1659 fields = self.fields_get(req, model)
1661 # To make fields retrieval more efficient, fetch all sub-fields of a
1662 # given field at the same time. Because the order in the export list is
1663 # arbitrary, this requires ordering all sub-fields of a given field
1664 # together so they can be fetched at the same time
1666 # Works the following way:
1667 # * sort the list of fields to export, the default sorting order will
1668 # put the field itself (if present, for xmlid) and all of its
1669 # sub-fields right after it
1670 # * then, group on: the first field of the path (which is the same for
1671 # a field and for its subfields and the length of splitting on the
1672 # first '/', which basically means grouping the field on one side and
1673 # all of the subfields on the other. This way, we have the field (for
1674 # the xmlid) with length 1, and all of the subfields with the same
1675 # base but a length "flag" of 2
1676 # * if we have a normal field (length 1), just add it to the info
1677 # mapping (with its string) as-is
1678 # * otherwise, recursively call fields_info via graft_subfields.
1679 # all graft_subfields does is take the result of fields_info (on the
1680 # field's model) and prepend the current base (current field), which
1681 # rebuilds the whole sub-tree for the field
1683 # result: because we're not fetching the fields_get for half the
1684 # database models, fetching a namelist with a dozen fields (including
1685 # relational data) falls from ~6s to ~300ms (on the leads model).
1686 # export lists with no sub-fields (e.g. import_compatible lists with
1687 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1688 # there's a single fields_get to execute)
1689 for (base, length), subfields in itertools.groupby(
1690 sorted(export_fields),
1691 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1692 subfields = list(subfields)
1694 # subfields is a seq of $base/*rest, and not loaded yet
1695 info.update(self.graft_subfields(
1696 req, fields[base]['relation'], base, fields[base]['string'],
1700 info[base] = fields[base]['string']
1704 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1705 export_fields = [field.split('/', 1)[1] for field in fields]
1707 (prefix + '/' + k, prefix_string + '/' + v)
1708 for k, v in self.fields_info(req, model, export_fields).iteritems())
1710 #noinspection PyPropertyDefinition
1712 def content_type(self):
1713 """ Provides the format's content type """
1714 raise NotImplementedError()
1716 def filename(self, base):
1717 """ Creates a valid filename for the format (with extension) from the
1718 provided base name (exension-less)
1720 raise NotImplementedError()
1722 def from_data(self, fields, rows):
1723 """ Conversion method from OpenERP's export data to whatever the
1724 current export class outputs
1726 :params list fields: a list of fields to export
1727 :params list rows: a list of records to export
1731 raise NotImplementedError()
1733 @openerpweb.httprequest
1734 def index(self, req, data, token):
1735 model, fields, ids, domain, import_compat = \
1736 operator.itemgetter('model', 'fields', 'ids', 'domain',
1738 simplejson.loads(data))
1740 context = req.session.eval_context(req.context)
1741 Model = req.session.model(model)
1742 ids = ids or Model.search(domain, 0, False, False, context)
1744 field_names = map(operator.itemgetter('name'), fields)
1745 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1748 columns_headers = field_names
1750 columns_headers = [val['label'].strip() for val in fields]
1753 return req.make_response(self.from_data(columns_headers, import_data),
1754 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1755 ('Content-Type', self.content_type)],
1756 cookies={'fileToken': int(token)})
1758 class CSVExport(Export):
1759 _cp_path = '/web/export/csv'
1760 fmt = {'tag': 'csv', 'label': 'CSV'}
1763 def content_type(self):
1764 return 'text/csv;charset=utf8'
1766 def filename(self, base):
1767 return base + '.csv'
1769 def from_data(self, fields, rows):
1771 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1773 writer.writerow([name.encode('utf-8') for name in fields])
1778 if isinstance(d, basestring):
1779 d = d.replace('\n',' ').replace('\t',' ')
1781 d = d.encode('utf-8')
1782 except UnicodeError:
1784 if d is False: d = None
1786 writer.writerow(row)
1793 class ExcelExport(Export):
1794 _cp_path = '/web/export/xls'
1798 'error': None if xlwt else "XLWT required"
1802 def content_type(self):
1803 return 'application/vnd.ms-excel'
1805 def filename(self, base):
1806 return base + '.xls'
1808 def from_data(self, fields, rows):
1809 workbook = xlwt.Workbook()
1810 worksheet = workbook.add_sheet('Sheet 1')
1812 for i, fieldname in enumerate(fields):
1813 worksheet.write(0, i, fieldname)
1814 worksheet.col(i).width = 8000 # around 220 pixels
1816 style = xlwt.easyxf('align: wrap yes')
1818 for row_index, row in enumerate(rows):
1819 for cell_index, cell_value in enumerate(row):
1820 if isinstance(cell_value, basestring):
1821 cell_value = re.sub("\r", " ", cell_value)
1822 if cell_value is False: cell_value = None
1823 worksheet.write(row_index + 1, cell_index, cell_value, style)
1832 class Reports(View):
1833 _cp_path = "/web/report"
1834 POLLING_DELAY = 0.25
1836 'doc': 'application/vnd.ms-word',
1837 'html': 'text/html',
1838 'odt': 'application/vnd.oasis.opendocument.text',
1839 'pdf': 'application/pdf',
1840 'sxw': 'application/vnd.sun.xml.writer',
1841 'xls': 'application/vnd.ms-excel',
1844 @openerpweb.httprequest
1845 def index(self, req, action, token):
1846 action = simplejson.loads(action)
1848 report_srv = req.session.proxy("report")
1849 context = req.session.eval_context(
1850 common.nonliterals.CompoundContext(
1851 req.context or {}, action[ "context"]))
1854 report_ids = context["active_ids"]
1855 if 'report_type' in action:
1856 report_data['report_type'] = action['report_type']
1857 if 'datas' in action:
1858 if 'ids' in action['datas']:
1859 report_ids = action['datas'].pop('ids')
1860 report_data.update(action['datas'])
1862 report_id = report_srv.report(
1863 req.session._db, req.session._uid, req.session._password,
1864 action["report_name"], report_ids,
1865 report_data, context)
1867 report_struct = None
1869 report_struct = report_srv.report_get(
1870 req.session._db, req.session._uid, req.session._password, report_id)
1871 if report_struct["state"]:
1874 time.sleep(self.POLLING_DELAY)
1876 report = base64.b64decode(report_struct['result'])
1877 if report_struct.get('code') == 'zlib':
1878 report = zlib.decompress(report)
1879 report_mimetype = self.TYPES_MAPPING.get(
1880 report_struct['format'], 'octet-stream')
1882 if 'name' not in action:
1883 reports = req.session.model('ir.actions.report.xml')
1884 res_id = reports.search([('report_name', '=', action['report_name']),],
1885 0, False, False, context)
1887 file_name = reports.read(res_id[0], ['name'], context)['name']
1889 file_name = action['report_name']
1891 return req.make_response(report,
1893 # maybe we should take of what characters can appear in a file name?
1894 ('Content-Disposition', 'attachment; filename="%s.%s"' % (file_name, report_struct['format'])),
1895 ('Content-Type', report_mimetype),
1896 ('Content-Length', len(report))],
1897 cookies={'fileToken': int(token)})
1900 _cp_path = "/web/import"
1902 def fields_get(self, req, model):
1903 Model = req.session.model(model)
1904 fields = Model.fields_get(False, req.session.eval_context(req.context))
1907 @openerpweb.httprequest
1908 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1910 data = list(csv.reader(
1911 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1912 except csv.Error, e:
1914 return '<script>window.top.%s(%s);</script>' % (
1915 jsonp, simplejson.dumps({'error': {
1916 'message': 'Error parsing CSV file: %s' % e,
1917 # decodes each byte to a unicode character, which may or
1918 # may not be printable, but decoding will succeed.
1919 # Otherwise simplejson will try to decode the `str` using
1920 # utf-8, which is very likely to blow up on characters out
1921 # of the ascii range (in range [128, 256))
1922 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1925 return '<script>window.top.%s(%s);</script>' % (
1926 jsonp, simplejson.dumps(
1927 {'records': data[:10]}, encoding=csvcode))
1928 except UnicodeDecodeError:
1929 return '<script>window.top.%s(%s);</script>' % (
1930 jsonp, simplejson.dumps({
1931 'message': u"Failed to decode CSV file using encoding %s, "
1932 u"try switching to a different encoding" % csvcode
1935 @openerpweb.httprequest
1936 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1938 modle_obj = req.session.model(model)
1939 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1940 simplejson.loads(meta))
1943 if not (csvdel and len(csvdel) == 1):
1944 error = u"The CSV delimiter must be a single character"
1946 if not indices and fields:
1947 error = u"You must select at least one field to import"
1950 return '<script>window.top.%s(%s);</script>' % (
1951 jsonp, simplejson.dumps({'error': {'message': error}}))
1953 # skip ignored records (@skip parameter)
1954 # then skip empty lines (not valid csv)
1955 # nb: should these operations be reverted?
1956 rows_to_import = itertools.ifilter(
1959 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1962 # if only one index, itemgetter will return an atom rather than a tuple
1963 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1964 else: mapper = operator.itemgetter(*indices)
1969 # decode each data row
1971 [record.decode(csvcode) for record in row]
1972 for row in itertools.imap(mapper, rows_to_import)
1973 # don't insert completely empty rows (can happen due to fields
1974 # filtering in case of e.g. o2m content rows)
1977 except UnicodeDecodeError:
1978 error = u"Failed to decode CSV file using encoding %s" % csvcode
1979 except csv.Error, e:
1980 error = u"Could not process CSV file: %s" % e
1982 # If the file contains nothing,
1984 error = u"File to import is empty"
1986 return '<script>window.top.%s(%s);</script>' % (
1987 jsonp, simplejson.dumps({'error': {'message': error}}))
1990 (code, record, message, _nope) = modle_obj.import_data(
1991 fields, data, 'init', '', False,
1992 req.session.eval_context(req.context))
1993 except xmlrpclib.Fault, e:
1994 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1995 return '<script>window.top.%s(%s);</script>' % (
1996 jsonp, simplejson.dumps({'error':error}))
1999 return '<script>window.top.%s(%s);</script>' % (
2000 jsonp, simplejson.dumps({'success':True}))
2002 msg = u"Error during import: %s\n\nTrying to import record %r" % (
2004 return '<script>window.top.%s(%s);</script>' % (
2005 jsonp, simplejson.dumps({'error': {'message':msg}}))
2007 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: