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"
90 proxy = req.session.proxy("db")
92 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
94 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
95 dbs = [i for i in dbs if re.match(r, i)]
98 def module_topological_sort(modules):
99 """ Return a list of module names sorted so that their dependencies of the
100 modules are listed before the module itself
102 modules is a dict of {module_name: dependencies}
104 :param modules: modules to sort
109 dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
110 # incoming edge: dependency on other module (if a depends on b, a has an
111 # incoming edge from b, aka there's an edge from b to a)
112 # outgoing edge: other module depending on this one
114 # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
115 #L ← Empty list that will contain the sorted nodes
117 #S ← Set of all nodes with no outgoing edges (modules on which no other
119 S = set(module for module in modules if module not in dependencies)
122 #function visit(node n)
124 #if n has not been visited yet then
128 #change: n not web module, can not be resolved, ignore
129 if n not in modules: return
130 #for each node m with an edge from m to n do (dependencies of n)
136 #for each node n in S do
142 def module_installed(req):
143 # Candidates module the current heuristic is the /static dir
144 loadable = openerpweb.addons_manifest.keys()
147 # Retrieve database installed modules
148 # TODO The following code should move to ir.module.module.list_installed_modules()
149 Modules = req.session.model('ir.module.module')
150 domain = [('state','=','installed'), ('name','in', loadable)]
151 for module in Modules.search_read(domain, ['name', 'dependencies_id']):
152 modules[module['name']] = []
153 deps = module.get('dependencies_id')
155 deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
156 dependencies = [i['name'] for i in deps_read]
157 modules[module['name']] = dependencies
159 sorted_modules = module_topological_sort(modules)
160 return sorted_modules
162 def module_installed_bypass_session(dbname):
163 loadable = openerpweb.addons_manifest.keys()
166 import openerp.modules.registry
167 registry = openerp.modules.registry.RegistryManager.get(dbname)
168 with registry.cursor() as cr:
169 m = registry.get('ir.module.module')
170 # TODO The following code should move to ir.module.module.list_installed_modules()
171 domain = [('state','=','installed'), ('name','in', loadable)]
172 ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
173 for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
174 modules[module['name']] = []
175 deps = module.get('dependencies_id')
177 deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
178 dependencies = [i['name'] for i in deps_read]
179 modules[module['name']] = dependencies
182 sorted_modules = module_topological_sort(modules)
183 return sorted_modules
185 def module_boot(req):
188 for i in req.config.server_wide_modules:
189 if i in openerpweb.addons_manifest:
191 # if only one db load every module at boot
195 except xmlrpclib.Fault:
196 # ignore access denied
199 dbside = module_installed_bypass_session(dbs[0])
200 dbside = [i for i in dbside if i not in serverside]
201 addons = serverside + dbside
204 def concat_xml(file_list):
205 """Concatenate xml files
207 :param list(str) file_list: list of files to check
208 :returns: (concatenation_result, checksum)
211 checksum = hashlib.new('sha1')
213 return '', checksum.hexdigest()
216 for fname in file_list:
217 with open(fname, 'rb') as fp:
219 checksum.update(contents)
221 xml = ElementTree.parse(fp).getroot()
224 root = ElementTree.Element(xml.tag)
225 #elif root.tag != xml.tag:
226 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
228 for child in xml.getchildren():
230 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
232 def concat_files(file_list, reader=None, intersperse=""):
233 """ Concatenates contents of all provided files
235 :param list(str) file_list: list of files to check
236 :param function reader: reading procedure for each file
237 :param str intersperse: string to intersperse between file contents
238 :returns: (concatenation_result, checksum)
241 checksum = hashlib.new('sha1')
243 return '', checksum.hexdigest()
247 with open(f, 'rb') as fp:
251 for fname in file_list:
252 contents = reader(fname)
253 checksum.update(contents)
254 files_content.append(contents)
256 files_concat = intersperse.join(files_content)
257 return files_concat, checksum.hexdigest()
259 def manifest_glob(req, addons, key):
261 addons = module_boot(req)
263 addons = addons.split(',')
266 manifest = openerpweb.addons_manifest.get(addon, None)
269 # ensure does not ends with /
270 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
271 globlist = manifest.get(key, [])
272 for pattern in globlist:
273 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
274 r.append((path, path[len(addons_path):]))
277 def manifest_list(req, mods, extension):
279 path = '/web/webclient/' + extension
281 path += '?mods=' + mods
283 files = manifest_glob(req, mods, extension)
284 i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
285 req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
287 return [wp for _fp, wp in files]
289 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
291 def get_last_modified(files):
292 """ Returns the modification time of the most recently modified
295 :param list(str) files: names of files to check
296 :return: most recent modification time amongst the fileset
297 :rtype: datetime.datetime
301 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
303 return datetime.datetime(1970, 1, 1)
305 def make_conditional(req, response, last_modified=None, etag=None):
306 """ Makes the provided response conditional based upon the request,
307 and mandates revalidation from clients
309 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
310 setting ``last_modified`` and ``etag`` correctly on the response object
312 :param req: OpenERP request
313 :type req: web.common.http.WebRequest
314 :param response: Werkzeug response
315 :type response: werkzeug.wrappers.Response
316 :param datetime.datetime last_modified: last modification date of the response content
317 :param str etag: some sort of checksum of the content (deep etag)
318 :return: the response object provided
319 :rtype: werkzeug.wrappers.Response
321 response.cache_control.must_revalidate = True
322 response.cache_control.max_age = 0
324 response.last_modified = last_modified
326 response.set_etag(etag)
327 return response.make_conditional(req.httprequest)
329 def login_and_redirect(req, db, login, key, redirect_url='/'):
330 req.session.authenticate(db, login, key, {})
331 return set_cookie_and_redirect(req, redirect_url)
333 def set_cookie_and_redirect(req, redirect_url):
334 redirect = werkzeug.utils.redirect(redirect_url, 303)
335 redirect.autocorrect_location_header = False
336 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
337 redirect.set_cookie('instance0|session_id', cookie_val)
340 def eval_context_and_domain(session, context, domain=None):
341 e_context = session.eval_context(context)
342 # should we give the evaluated context as an evaluation context to the domain?
343 e_domain = session.eval_domain(domain or [])
345 return e_context, e_domain
347 def load_actions_from_ir_values(req, key, key2, models, meta):
348 context = req.session.eval_context(req.context)
349 Values = req.session.model('ir.values')
350 actions = Values.get(key, key2, models, meta, context)
352 return [(id, name, clean_action(req, action))
353 for id, name, action in actions]
355 def clean_action(req, action, do_not_eval=False):
356 action.setdefault('flags', {})
358 context = req.session.eval_context(req.context)
359 eval_ctx = req.session.evaluation_context(context)
362 # values come from the server, we can just eval them
363 if action.get('context') and isinstance(action.get('context'), basestring):
364 action['context'] = eval( action['context'], eval_ctx ) or {}
366 if action.get('domain') and isinstance(action.get('domain'), basestring):
367 action['domain'] = eval( action['domain'], eval_ctx ) or []
369 if 'context' in action:
370 action['context'] = parse_context(action['context'], req.session)
371 if 'domain' in action:
372 action['domain'] = parse_domain(action['domain'], req.session)
374 action_type = action.setdefault('type', 'ir.actions.act_window_close')
375 if action_type == 'ir.actions.act_window':
376 return fix_view_modes(action)
379 # I think generate_views,fix_view_modes should go into js ActionManager
380 def generate_views(action):
382 While the server generates a sequence called "views" computing dependencies
383 between a bunch of stuff for views coming directly from the database
384 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
385 to return custom view dictionaries generated on the fly.
387 In that case, there is no ``views`` key available on the action.
389 Since the web client relies on ``action['views']``, generate it here from
390 ``view_mode`` and ``view_id``.
392 Currently handles two different cases:
394 * no view_id, multiple view_mode
395 * single view_id, single view_mode
397 :param dict action: action descriptor dictionary to generate a views key for
399 view_id = action.get('view_id') or False
400 if isinstance(view_id, (list, tuple)):
403 # providing at least one view mode is a requirement, not an option
404 view_modes = action['view_mode'].split(',')
406 if len(view_modes) > 1:
408 raise ValueError('Non-db action dictionaries should provide '
409 'either multiple view modes or a single view '
410 'mode and an optional view id.\n\n Got view '
411 'modes %r and view id %r for action %r' % (
412 view_modes, view_id, action))
413 action['views'] = [(False, mode) for mode in view_modes]
415 action['views'] = [(view_id, view_modes[0])]
417 def fix_view_modes(action):
418 """ For historical reasons, OpenERP has weird dealings in relation to
419 view_mode and the view_type attribute (on window actions):
421 * one of the view modes is ``tree``, which stands for both list views
423 * the choice is made by checking ``view_type``, which is either
424 ``form`` for a list view or ``tree`` for an actual tree view
426 This methods simply folds the view_type into view_mode by adding a
427 new view mode ``list`` which is the result of the ``tree`` view_mode
428 in conjunction with the ``form`` view_type.
430 TODO: this should go into the doc, some kind of "peculiarities" section
432 :param dict action: an action descriptor
433 :returns: nothing, the action is modified in place
435 if not action.get('views'):
436 generate_views(action)
439 for index, (id, mode) in enumerate(action['views']):
444 if action.pop('view_type', 'form') != 'form':
448 [id, mode if mode != 'tree' else 'list']
449 for id, mode in action['views']
454 def parse_domain(domain, session):
455 """ Parses an arbitrary string containing a domain, transforms it
456 to either a literal domain or a :class:`common.nonliterals.Domain`
458 :param domain: the domain to parse, if the domain is not a string it
459 is assumed to be a literal domain and is returned as-is
460 :param session: Current OpenERP session
461 :type session: openerpweb.openerpweb.OpenERPSession
463 if not isinstance(domain, basestring):
466 return ast.literal_eval(domain)
469 return common.nonliterals.Domain(session, domain)
471 def parse_context(context, session):
472 """ Parses an arbitrary string containing a context, transforms it
473 to either a literal context or a :class:`common.nonliterals.Context`
475 :param context: the context to parse, if the context is not a string it
476 is assumed to be a literal domain and is returned as-is
477 :param session: Current OpenERP session
478 :type session: openerpweb.openerpweb.OpenERPSession
480 if not isinstance(context, basestring):
483 return ast.literal_eval(context)
485 return common.nonliterals.Context(session, context)
487 #----------------------------------------------------------
488 # OpenERP Web web Controllers
489 #----------------------------------------------------------
491 html_template = """<!DOCTYPE html>
492 <html style="height: 100%%">
494 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
495 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
496 <title>OpenERP</title>
497 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
498 <link rel="stylesheet" href="/web/static/src/css/full.css" />
501 <script type="text/javascript">
503 var s = new openerp.init(%(modules)s);
512 class Home(openerpweb.Controller):
515 @openerpweb.httprequest
516 def index(self, req, s_action=None, **kw):
517 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, None, 'js'))
518 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, None, 'css'))
520 r = html_template % {
523 'modules': simplejson.dumps(module_boot(req)),
524 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
528 @openerpweb.httprequest
529 def login(self, req, db, login, key):
530 return login_and_redirect(req, db, login, key)
532 class WebClient(openerpweb.Controller):
533 _cp_path = "/web/webclient"
535 @openerpweb.jsonrequest
536 def csslist(self, req, mods=None):
537 return manifest_list(req, mods, 'css')
539 @openerpweb.jsonrequest
540 def jslist(self, req, mods=None):
541 return manifest_list(req, mods, 'js')
543 @openerpweb.jsonrequest
544 def qweblist(self, req, mods=None):
545 return manifest_list(req, mods, 'qweb')
547 @openerpweb.httprequest
548 def css(self, req, mods=None):
549 files = list(manifest_glob(req, mods, 'css'))
550 last_modified = get_last_modified(f[0] for f in files)
551 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
552 return werkzeug.wrappers.Response(status=304)
554 file_map = dict(files)
556 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
557 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
560 """read the a css file and absolutify all relative uris"""
561 with open(f, 'rb') as fp:
562 data = fp.read().decode('utf-8')
565 # convert FS path into web path
566 web_dir = '/'.join(os.path.dirname(path).split(os.path.sep))
570 r"""@import \1%s/""" % (web_dir,),
576 r"""url(\1%s/""" % (web_dir,),
579 return data.encode('utf-8')
581 content, checksum = concat_files((f[0] for f in files), reader)
583 return make_conditional(
584 req, req.make_response(content, [('Content-Type', 'text/css')]),
585 last_modified, checksum)
587 @openerpweb.httprequest
588 def js(self, req, mods=None):
589 files = [f[0] for f in manifest_glob(req, mods, 'js')]
590 last_modified = get_last_modified(files)
591 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
592 return werkzeug.wrappers.Response(status=304)
594 content, checksum = concat_files(files, intersperse=';')
596 return make_conditional(
597 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
598 last_modified, checksum)
600 @openerpweb.httprequest
601 def qweb(self, req, mods=None):
602 files = [f[0] for f in manifest_glob(req, mods, 'qweb')]
603 last_modified = get_last_modified(files)
604 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
605 return werkzeug.wrappers.Response(status=304)
607 content, checksum = concat_xml(files)
609 return make_conditional(
610 req, req.make_response(content, [('Content-Type', 'text/xml')]),
611 last_modified, checksum)
613 @openerpweb.jsonrequest
614 def translations(self, req, mods, lang):
615 lang_model = req.session.model('res.lang')
616 ids = lang_model.search([("code", "=", lang)])
618 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
619 "grouping", "decimal_point", "thousands_sep"])
627 langs = lang.split(separator)
628 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
631 for addon_name in mods:
632 transl = {"messages":[]}
633 transs[addon_name] = transl
634 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
636 f_name = os.path.join(addons_path, addon_name, "i18n", l + ".po")
637 if not os.path.exists(f_name):
640 with open(f_name) as t_file:
641 po = babel.messages.pofile.read_po(t_file)
645 if x.id and x.string and "openerp-web" in x.auto_comments:
646 transl["messages"].append({'id': x.id, 'string': x.string})
647 return {"modules": transs,
648 "lang_parameters": lang_obj}
650 @openerpweb.jsonrequest
651 def version_info(self, req):
653 "version": common.release.version
656 class Proxy(openerpweb.Controller):
657 _cp_path = '/web/proxy'
659 @openerpweb.jsonrequest
660 def load(self, req, path):
661 """ Proxies an HTTP request through a JSON request.
663 It is strongly recommended to not request binary files through this,
664 as the result will be a binary data blob as well.
666 :param req: OpenERP request
667 :param path: actual request path
668 :return: file content
670 from werkzeug.test import Client
671 from werkzeug.wrappers import BaseResponse
673 return Client(req.httprequest.app, BaseResponse).get(path).data
675 class Database(openerpweb.Controller):
676 _cp_path = "/web/database"
678 @openerpweb.jsonrequest
679 def get_list(self, req):
681 return {"db_list": dbs}
683 @openerpweb.jsonrequest
684 def create(self, req, fields):
685 params = dict(map(operator.itemgetter('name', 'value'), fields))
687 params['super_admin_pwd'],
689 bool(params.get('demo_data')),
691 params['create_admin_pwd']
694 return req.session.proxy("db").create_database(*create_attrs)
696 @openerpweb.jsonrequest
697 def drop(self, req, fields):
698 password, db = operator.itemgetter(
699 'drop_pwd', 'drop_db')(
700 dict(map(operator.itemgetter('name', 'value'), fields)))
703 return req.session.proxy("db").drop(password, db)
704 except xmlrpclib.Fault, e:
705 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
706 return {'error': e.faultCode, 'title': 'Drop Database'}
707 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
709 @openerpweb.httprequest
710 def backup(self, req, backup_db, backup_pwd, token):
712 db_dump = base64.b64decode(
713 req.session.proxy("db").dump(backup_pwd, backup_db))
714 filename = "%(db)s_%(timestamp)s.dump" % {
716 'timestamp': datetime.datetime.utcnow().strftime(
717 "%Y-%m-%d_%H-%M-%SZ")
719 return req.make_response(db_dump,
720 [('Content-Type', 'application/octet-stream; charset=binary'),
721 ('Content-Disposition', 'attachment; filename="' + filename + '"')],
722 {'fileToken': int(token)}
724 except xmlrpclib.Fault, e:
725 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
727 @openerpweb.httprequest
728 def restore(self, req, db_file, restore_pwd, new_db):
730 data = base64.b64encode(db_file.read())
731 req.session.proxy("db").restore(restore_pwd, new_db, data)
733 except xmlrpclib.Fault, e:
734 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
735 raise Exception("AccessDenied")
737 @openerpweb.jsonrequest
738 def change_password(self, req, fields):
739 old_password, new_password = operator.itemgetter(
740 'old_pwd', 'new_pwd')(
741 dict(map(operator.itemgetter('name', 'value'), fields)))
743 return req.session.proxy("db").change_admin_password(old_password, new_password)
744 except xmlrpclib.Fault, e:
745 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
746 return {'error': e.faultCode, 'title': 'Change Password'}
747 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
749 class Session(openerpweb.Controller):
750 _cp_path = "/web/session"
752 def session_info(self, req):
753 req.session.ensure_valid()
755 "session_id": req.session_id,
756 "uid": req.session._uid,
757 "context": req.session.get_context() if req.session._uid else {},
758 "db": req.session._db,
759 "login": req.session._login,
760 "openerp_entreprise": req.session.openerp_entreprise(),
763 @openerpweb.jsonrequest
764 def get_session_info(self, req):
765 return self.session_info(req)
767 @openerpweb.jsonrequest
768 def authenticate(self, req, db, login, password, base_location=None):
769 wsgienv = req.httprequest.environ
770 release = common.release
772 base_location=base_location,
773 HTTP_HOST=wsgienv['HTTP_HOST'],
774 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
775 user_agent="%s / %s" % (release.name, release.version),
777 req.session.authenticate(db, login, password, env)
779 return self.session_info(req)
781 @openerpweb.jsonrequest
782 def change_password (self,req,fields):
783 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
784 dict(map(operator.itemgetter('name', 'value'), fields)))
785 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
786 return {'error':'All passwords have to be filled.','title': 'Change Password'}
787 if new_password != confirm_password:
788 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
790 if req.session.model('res.users').change_password(
791 old_password, new_password):
792 return {'new_password':new_password}
794 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
795 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
797 @openerpweb.jsonrequest
798 def sc_list(self, req):
799 return req.session.model('ir.ui.view_sc').get_sc(
800 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
802 @openerpweb.jsonrequest
803 def get_lang_list(self, req):
806 'lang_list': (req.session.proxy("db").list_lang() or []),
810 return {"error": e, "title": "Languages"}
812 @openerpweb.jsonrequest
813 def modules(self, req):
814 # return all installed modules. Web client is smart enough to not load a module twice
815 return module_installed(req)
817 @openerpweb.jsonrequest
818 def eval_domain_and_context(self, req, contexts, domains,
820 """ Evaluates sequences of domains and contexts, composing them into
821 a single context, domain or group_by sequence.
823 :param list contexts: list of contexts to merge together. Contexts are
824 evaluated in sequence, all previous contexts
825 are part of their own evaluation context
826 (starting at the session context).
827 :param list domains: list of domains to merge together. Domains are
828 evaluated in sequence and appended to one another
829 (implicit AND), their evaluation domain is the
830 result of merging all contexts.
831 :param list group_by_seq: list of domains (which may be in a different
832 order than the ``contexts`` parameter),
833 evaluated in sequence, their ``'group_by'``
834 key is extracted if they have one.
839 the global context created by merging all of
843 the concatenation of all domains
846 a list of fields to group by, potentially empty (in which case
847 no group by should be performed)
849 context, domain = eval_context_and_domain(req.session,
850 common.nonliterals.CompoundContext(*(contexts or [])),
851 common.nonliterals.CompoundDomain(*(domains or [])))
853 group_by_sequence = []
854 for candidate in (group_by_seq or []):
855 ctx = req.session.eval_context(candidate, context)
856 group_by = ctx.get('group_by')
859 elif isinstance(group_by, basestring):
860 group_by_sequence.append(group_by)
862 group_by_sequence.extend(group_by)
867 'group_by': group_by_sequence
870 @openerpweb.jsonrequest
871 def save_session_action(self, req, the_action):
873 This method store an action object in the session object and returns an integer
874 identifying that action. The method get_session_action() can be used to get
877 :param the_action: The action to save in the session.
878 :type the_action: anything
879 :return: A key identifying the saved action.
882 saved_actions = req.httpsession.get('saved_actions')
883 if not saved_actions:
884 saved_actions = {"next":0, "actions":{}}
885 req.httpsession['saved_actions'] = saved_actions
886 # we don't allow more than 10 stored actions
887 if len(saved_actions["actions"]) >= 10:
888 del saved_actions["actions"][min(saved_actions["actions"])]
889 key = saved_actions["next"]
890 saved_actions["actions"][key] = the_action
891 saved_actions["next"] = key + 1
894 @openerpweb.jsonrequest
895 def get_session_action(self, req, key):
897 Gets back a previously saved action. This method can return None if the action
898 was saved since too much time (this case should be handled in a smart way).
900 :param key: The key given by save_session_action()
902 :return: The saved action or None.
905 saved_actions = req.httpsession.get('saved_actions')
906 if not saved_actions:
908 return saved_actions["actions"].get(key)
910 @openerpweb.jsonrequest
911 def check(self, req):
912 req.session.assert_valid()
915 @openerpweb.jsonrequest
916 def destroy(self, req):
917 req.session._suicide = True
919 class Menu(openerpweb.Controller):
920 _cp_path = "/web/menu"
922 @openerpweb.jsonrequest
924 return {'data': self.do_load(req)}
926 @openerpweb.jsonrequest
927 def get_user_roots(self, req):
928 return self.do_get_user_roots(req)
930 def do_get_user_roots(self, req):
931 """ Return all root menu ids visible for the session user.
933 :param req: A request object, with an OpenERP session attribute
934 :type req: < session -> OpenERPSession >
935 :return: the root menu ids
939 context = s.eval_context(req.context)
940 Menus = s.model('ir.ui.menu')
941 # If a menu action is defined use its domain to get the root menu items
942 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
944 menu_domain = [('parent_id', '=', False)]
946 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
948 menu_domain = ast.literal_eval(domain_string)
950 return Menus.search(menu_domain, 0, False, False, context)
952 def do_load(self, req):
953 """ Loads all menu items (all applications and their sub-menus).
955 :param req: A request object, with an OpenERP session attribute
956 :type req: < session -> OpenERPSession >
957 :return: the menu root
958 :rtype: dict('children': menu_nodes)
960 context = req.session.eval_context(req.context)
961 Menus = req.session.model('ir.ui.menu')
963 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
964 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
966 # menus are loaded fully unlike a regular tree view, cause there are a
967 # limited number of items (752 when all 6.1 addons are installed)
968 menu_ids = Menus.search([], 0, False, False, context)
969 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
970 # adds roots at the end of the sequence, so that they will overwrite
971 # equivalent menu items from full menu read when put into id:item
972 # mapping, resulting in children being correctly set on the roots.
973 menu_items.extend(menu_roots)
975 # make a tree using parent_id
976 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
977 for menu_item in menu_items:
978 if menu_item['parent_id']:
979 parent = menu_item['parent_id'][0]
982 if parent in menu_items_map:
983 menu_items_map[parent].setdefault(
984 'children', []).append(menu_item)
986 # sort by sequence a tree using parent_id
987 for menu_item in menu_items:
988 menu_item.setdefault('children', []).sort(
989 key=operator.itemgetter('sequence'))
993 @openerpweb.jsonrequest
994 def action(self, req, menu_id):
995 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
996 [('ir.ui.menu', menu_id)], False)
997 return {"action": actions}
999 class DataSet(openerpweb.Controller):
1000 _cp_path = "/web/dataset"
1002 @openerpweb.jsonrequest
1003 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1004 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1005 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1007 """ Performs a search() followed by a read() (if needed) using the
1008 provided search criteria
1010 :param req: a JSON-RPC request object
1011 :type req: openerpweb.JsonRequest
1012 :param str model: the name of the model to search on
1013 :param fields: a list of the fields to return in the result records
1015 :param int offset: from which index should the results start being returned
1016 :param int limit: the maximum number of records to return
1017 :param list domain: the search domain for the query
1018 :param list sort: sorting directives
1019 :returns: A structure (dict) with two keys: ids (all the ids matching
1020 the (domain, context) pair) and records (paginated records
1021 matching fields selection set)
1024 Model = req.session.model(model)
1026 context, domain = eval_context_and_domain(
1027 req.session, req.context, domain)
1029 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1030 if limit and len(ids) == limit:
1031 length = Model.search_count(domain, context)
1033 length = len(ids) + (offset or 0)
1034 if fields and fields == ['id']:
1035 # shortcut read if we only want the ids
1038 'records': [{'id': id} for id in ids]
1041 records = Model.read(ids, fields or False, context)
1042 records.sort(key=lambda obj: ids.index(obj['id']))
1048 @openerpweb.jsonrequest
1049 def load(self, req, model, id, fields):
1050 m = req.session.model(model)
1052 r = m.read([id], False, req.session.eval_context(req.context))
1055 return {'value': value}
1057 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1058 has_domain = domain_id is not None and domain_id < len(args)
1059 has_context = context_id is not None and context_id < len(args)
1061 domain = args[domain_id] if has_domain else []
1062 context = args[context_id] if has_context else {}
1063 c, d = eval_context_and_domain(req.session, context, domain)
1067 args[context_id] = c
1069 return self._call_kw(req, model, method, args, {})
1071 def _call_kw(self, req, model, method, args, kwargs):
1072 for i in xrange(len(args)):
1073 if isinstance(args[i], common.nonliterals.BaseContext):
1074 args[i] = req.session.eval_context(args[i])
1075 elif isinstance(args[i], common.nonliterals.BaseDomain):
1076 args[i] = req.session.eval_domain(args[i])
1077 for k in kwargs.keys():
1078 if isinstance(kwargs[k], common.nonliterals.BaseContext):
1079 kwargs[k] = req.session.eval_context(kwargs[k])
1080 elif isinstance(kwargs[k], common.nonliterals.BaseDomain):
1081 kwargs[k] = req.session.eval_domain(kwargs[k])
1083 # Temporary implements future display_name special field for model#read()
1084 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1085 if 'display_name' in args[1]:
1086 names = req.session.model(model).name_get(args[0], **kwargs)
1087 args[1].remove('display_name')
1088 r = getattr(req.session.model(model), method)(*args, **kwargs)
1089 for i in range(len(r)):
1090 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1093 return getattr(req.session.model(model), method)(*args, **kwargs)
1095 @openerpweb.jsonrequest
1096 def onchange(self, req, model, method, args, context_id=None):
1097 """ Support method for handling onchange calls: behaves much like call
1098 with the following differences:
1100 * Does not take a domain_id
1101 * Is aware of the return value's structure, and will parse the domains
1102 if needed in order to return either parsed literal domains (in JSON)
1103 or non-literal domain instances, allowing those domains to be used
1107 :type req: web.common.http.JsonRequest
1108 :param str model: object type on which to call the method
1109 :param str method: name of the onchange handler method
1110 :param list args: arguments to call the onchange handler with
1111 :param int context_id: index of the context object in the list of
1113 :return: result of the onchange call with all domains parsed
1115 result = self.call_common(req, model, method, args, context_id=context_id)
1116 if not result or 'domain' not in result:
1119 result['domain'] = dict(
1120 (k, parse_domain(v, req.session))
1121 for k, v in result['domain'].iteritems())
1125 @openerpweb.jsonrequest
1126 def call(self, req, model, method, args, domain_id=None, context_id=None):
1127 return self.call_common(req, model, method, args, domain_id, context_id)
1129 @openerpweb.jsonrequest
1130 def call_kw(self, req, model, method, args, kwargs):
1131 return self._call_kw(req, model, method, args, kwargs)
1133 @openerpweb.jsonrequest
1134 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1135 action = self.call_common(req, model, method, args, domain_id, context_id)
1136 if isinstance(action, dict) and action.get('type') != '':
1137 return {'result': clean_action(req, action)}
1138 return {'result': False}
1140 @openerpweb.jsonrequest
1141 def exec_workflow(self, req, model, id, signal):
1142 return req.session.exec_workflow(model, id, signal)
1144 class DataGroup(openerpweb.Controller):
1145 _cp_path = "/web/group"
1146 @openerpweb.jsonrequest
1147 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1148 Model = req.session.model(model)
1149 context, domain = eval_context_and_domain(req.session, req.context, domain)
1151 return Model.read_group(
1152 domain or [], fields, group_by_fields, 0, False,
1153 dict(context, group_by=group_by_fields), sort or False)
1155 class View(openerpweb.Controller):
1156 _cp_path = "/web/view"
1158 def fields_view_get(self, req, model, view_id, view_type,
1159 transform=True, toolbar=False, submenu=False):
1160 Model = req.session.model(model)
1161 context = req.session.eval_context(req.context)
1162 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1163 # todo fme?: check that we should pass the evaluated context here
1164 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1165 if toolbar and transform:
1166 self.process_toolbar(req, fvg['toolbar'])
1169 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1170 # depending on how it feels, xmlrpclib.ServerProxy can translate
1171 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1172 # enjoy unicode strings which can not be trivially converted to
1173 # strings, and it blows up during parsing.
1175 # So ensure we fix this retardation by converting view xml back to
1177 if isinstance(fvg['arch'], unicode):
1178 arch = fvg['arch'].encode('utf-8')
1181 fvg['arch_string'] = arch
1184 evaluation_context = session.evaluation_context(context or {})
1185 xml = self.transform_view(arch, session, evaluation_context)
1187 xml = ElementTree.fromstring(arch)
1188 fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
1190 if 'id' in fvg['fields']:
1191 # Special case for id's
1192 id_field = fvg['fields']['id']
1193 id_field['original_type'] = id_field['type']
1194 id_field['type'] = 'id'
1196 for field in fvg['fields'].itervalues():
1197 if field.get('views'):
1198 for view in field["views"].itervalues():
1199 self.process_view(session, view, None, transform)
1200 if field.get('domain'):
1201 field["domain"] = parse_domain(field["domain"], session)
1202 if field.get('context'):
1203 field["context"] = parse_context(field["context"], session)
1205 def process_toolbar(self, req, toolbar):
1207 The toolbar is a mapping of section_key: [action_descriptor]
1209 We need to clean all those actions in order to ensure correct
1212 for actions in toolbar.itervalues():
1213 for action in actions:
1214 if 'context' in action:
1215 action['context'] = parse_context(
1216 action['context'], req.session)
1217 if 'domain' in action:
1218 action['domain'] = parse_domain(
1219 action['domain'], req.session)
1221 @openerpweb.jsonrequest
1222 def add_custom(self, req, view_id, arch):
1223 CustomView = req.session.model('ir.ui.view.custom')
1225 'user_id': req.session._uid,
1228 }, req.session.eval_context(req.context))
1229 return {'result': True}
1231 @openerpweb.jsonrequest
1232 def undo_custom(self, req, view_id, reset=False):
1233 CustomView = req.session.model('ir.ui.view.custom')
1234 context = req.session.eval_context(req.context)
1235 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1236 0, False, False, context)
1239 CustomView.unlink(vcustom, context)
1241 CustomView.unlink([vcustom[0]], context)
1242 return {'result': True}
1243 return {'result': False}
1245 def transform_view(self, view_string, session, context=None):
1246 # transform nodes on the fly via iterparse, instead of
1247 # doing it statically on the parsing result
1248 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1250 for event, elem in parser:
1251 if event == "start":
1254 self.parse_domains_and_contexts(elem, session)
1257 def parse_domains_and_contexts(self, elem, session):
1258 """ Converts domains and contexts from the view into Python objects,
1259 either literals if they can be parsed by literal_eval or a special
1260 placeholder object if the domain or context refers to free variables.
1262 :param elem: the current node being parsed
1263 :type param: xml.etree.ElementTree.Element
1264 :param session: OpenERP session object, used to store and retrieve
1266 :type session: openerpweb.openerpweb.OpenERPSession
1268 for el in ['domain', 'filter_domain']:
1269 domain = elem.get(el, '').strip()
1271 elem.set(el, parse_domain(domain, session))
1272 elem.set(el + '_string', domain)
1273 for el in ['context', 'default_get']:
1274 context_string = elem.get(el, '').strip()
1276 elem.set(el, parse_context(context_string, session))
1277 elem.set(el + '_string', context_string)
1279 @openerpweb.jsonrequest
1280 def load(self, req, model, view_id, view_type, toolbar=False):
1281 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1283 class ListView(View):
1284 _cp_path = "/web/listview"
1286 def process_colors(self, view, row, context):
1287 colors = view['arch']['attrs'].get('colors')
1294 for pair in colors.split(';')
1295 if eval(pair.split(':')[1], dict(context, **row))
1300 elif len(color) == 1:
1304 class TreeView(View):
1305 _cp_path = "/web/treeview"
1307 @openerpweb.jsonrequest
1308 def action(self, req, model, id):
1309 return load_actions_from_ir_values(
1310 req,'action', 'tree_but_open',[(model, id)],
1313 class SearchView(View):
1314 _cp_path = "/web/searchview"
1316 @openerpweb.jsonrequest
1317 def load(self, req, model, view_id):
1318 fields_view = self.fields_view_get(req, model, view_id, 'search')
1319 return {'fields_view': fields_view}
1321 @openerpweb.jsonrequest
1322 def fields_get(self, req, model):
1323 Model = req.session.model(model)
1324 fields = Model.fields_get(False, req.session.eval_context(req.context))
1325 for field in fields.values():
1326 # shouldn't convert the views too?
1327 if field.get('domain'):
1328 field["domain"] = parse_domain(field["domain"], req.session)
1329 if field.get('context'):
1330 field["context"] = parse_context(field["context"], req.session)
1331 return {'fields': fields}
1333 @openerpweb.jsonrequest
1334 def get_filters(self, req, model):
1335 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1336 Model = req.session.model("ir.filters")
1337 filters = Model.get_filters(model)
1338 for filter in filters:
1340 parsed_context = parse_context(filter["context"], req.session)
1341 filter["context"] = (parsed_context
1342 if not isinstance(parsed_context, common.nonliterals.BaseContext)
1343 else req.session.eval_context(parsed_context))
1345 parsed_domain = parse_domain(filter["domain"], req.session)
1346 filter["domain"] = (parsed_domain
1347 if not isinstance(parsed_domain, common.nonliterals.BaseDomain)
1348 else req.session.eval_domain(parsed_domain))
1350 logger.exception("Failed to parse custom filter %s in %s",
1351 filter['name'], model)
1352 filter['disabled'] = True
1353 del filter['context']
1354 del filter['domain']
1357 class Binary(openerpweb.Controller):
1358 _cp_path = "/web/binary"
1360 @openerpweb.httprequest
1361 def image(self, req, model, id, field, **kw):
1362 last_update = '__last_update'
1363 Model = req.session.model(model)
1364 context = req.session.eval_context(req.context)
1365 headers = [('Content-Type', 'image/png')]
1366 etag = req.httprequest.headers.get('If-None-Match')
1367 hashed_session = hashlib.md5(req.session_id).hexdigest()
1368 id = None if not id else simplejson.loads(id)
1369 if type(id) is list:
1372 if not id and hashed_session == etag:
1373 return werkzeug.wrappers.Response(status=304)
1375 date = Model.read([id], [last_update], context)[0].get(last_update)
1376 if hashlib.md5(date).hexdigest() == etag:
1377 return werkzeug.wrappers.Response(status=304)
1379 retag = hashed_session
1382 res = Model.default_get([field], context).get(field)
1383 image_data = base64.b64decode(res)
1385 res = Model.read([id], [last_update, field], context)[0]
1386 retag = hashlib.md5(res.get(last_update)).hexdigest()
1387 image_data = base64.b64decode(res.get(field))
1388 except (TypeError, xmlrpclib.Fault):
1389 image_data = self.placeholder(req)
1390 headers.append(('ETag', retag))
1391 headers.append(('Content-Length', len(image_data)))
1393 ncache = int(kw.get('cache'))
1394 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1397 return req.make_response(image_data, headers)
1398 def placeholder(self, req):
1399 addons_path = openerpweb.addons_manifest['web']['addons_path']
1400 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1401 def content_disposition(self, filename, req):
1402 filename = filename.encode('utf8')
1403 escaped = urllib2.quote(filename)
1404 browser = req.httprequest.user_agent.browser
1405 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
1406 if browser == 'msie' and version < 9:
1407 return "attachment; filename=%s" % escaped
1408 elif browser == 'safari':
1409 return "attachment; filename=%s" % filename
1411 return "attachment; filename*=UTF-8''%s" % escaped
1413 @openerpweb.httprequest
1414 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1415 """ Download link for files stored as binary fields.
1417 If the ``id`` parameter is omitted, fetches the default value for the
1418 binary field (via ``default_get``), otherwise fetches the field for
1419 that precise record.
1421 :param req: OpenERP request
1422 :type req: :class:`web.common.http.HttpRequest`
1423 :param str model: name of the model to fetch the binary from
1424 :param str field: binary field
1425 :param str id: id of the record from which to fetch the binary
1426 :param str filename_field: field holding the file's name, if any
1427 :returns: :class:`werkzeug.wrappers.Response`
1429 Model = req.session.model(model)
1430 context = req.session.eval_context(req.context)
1433 fields.append(filename_field)
1435 res = Model.read([int(id)], fields, context)[0]
1437 res = Model.default_get(fields, context)
1438 filecontent = base64.b64decode(res.get(field, ''))
1440 return req.not_found()
1442 filename = '%s_%s' % (model.replace('.', '_'), id)
1444 filename = res.get(filename_field, '') or filename
1445 return req.make_response(filecontent,
1446 [('Content-Type', 'application/octet-stream'),
1447 ('Content-Disposition', self.content_disposition(filename, req))])
1449 @openerpweb.httprequest
1450 def saveas_ajax(self, req, data, token):
1451 jdata = simplejson.loads(data)
1452 model = jdata['model']
1453 field = jdata['field']
1454 id = jdata.get('id', None)
1455 filename_field = jdata.get('filename_field', None)
1456 context = jdata.get('context', dict())
1458 context = req.session.eval_context(context)
1459 Model = req.session.model(model)
1462 fields.append(filename_field)
1464 res = Model.read([int(id)], fields, context)[0]
1466 res = Model.default_get(fields, context)
1467 filecontent = base64.b64decode(res.get(field, ''))
1469 raise ValueError("No content found for field '%s' on '%s:%s'" %
1472 filename = '%s_%s' % (model.replace('.', '_'), id)
1474 filename = res.get(filename_field, '') or filename
1475 return req.make_response(filecontent,
1476 headers=[('Content-Type', 'application/octet-stream'),
1477 ('Content-Disposition', self.content_disposition(filename, req))],
1478 cookies={'fileToken': int(token)})
1480 @openerpweb.httprequest
1481 def upload(self, req, callback, ufile):
1482 # TODO: might be useful to have a configuration flag for max-length file uploads
1484 out = """<script language="javascript" type="text/javascript">
1485 var win = window.top.window;
1486 win.jQuery(win).trigger(%s, %s);
1489 args = [len(data), ufile.filename,
1490 ufile.content_type, base64.b64encode(data)]
1491 except Exception, e:
1492 args = [False, e.message]
1493 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1495 @openerpweb.httprequest
1496 def upload_attachment(self, req, callback, model, id, ufile):
1497 context = req.session.eval_context(req.context)
1498 Model = req.session.model('ir.attachment')
1500 out = """<script language="javascript" type="text/javascript">
1501 var win = window.top.window;
1502 win.jQuery(win).trigger(%s, %s);
1504 attachment_id = Model.create({
1505 'name': ufile.filename,
1506 'datas': base64.encodestring(ufile.read()),
1507 'datas_fname': ufile.filename,
1512 'filename': ufile.filename,
1515 except Exception, e:
1516 args = { 'error': e.message }
1517 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1519 class Action(openerpweb.Controller):
1520 _cp_path = "/web/action"
1522 # For most actions, the type attribute and the model name are the same, but
1523 # there are exceptions. This dict is used to remap action type attributes
1524 # to the "real" model name when they differ.
1526 "ir.actions.act_url": "ir.actions.url",
1529 @openerpweb.jsonrequest
1530 def load(self, req, action_id, do_not_eval=False):
1531 Actions = req.session.model('ir.actions.actions')
1533 context = req.session.eval_context(req.context)
1536 action_id = int(action_id)
1539 module, xmlid = action_id.split('.', 1)
1540 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1541 assert model.startswith('ir.actions.')
1543 action_id = 0 # force failed read
1545 base_action = Actions.read([action_id], ['type'], context)
1548 action_type = base_action[0]['type']
1549 if action_type == 'ir.actions.report.xml':
1550 ctx.update({'bin_size': True})
1552 action_model = self.action_mapping.get(action_type, action_type)
1553 action = req.session.model(action_model).read([action_id], False, ctx)
1555 value = clean_action(req, action[0], do_not_eval)
1556 return {'result': value}
1558 @openerpweb.jsonrequest
1559 def run(self, req, action_id):
1560 return_action = req.session.model('ir.actions.server').run(
1561 [action_id], req.session.eval_context(req.context))
1563 return clean_action(req, return_action)
1568 _cp_path = "/web/export"
1570 @openerpweb.jsonrequest
1571 def formats(self, req):
1572 """ Returns all valid export formats
1574 :returns: for each export format, a pair of identifier and printable name
1575 :rtype: [(str, str)]
1579 for path, controller in openerpweb.controllers_path.iteritems()
1580 if path.startswith(self._cp_path)
1581 if hasattr(controller, 'fmt')
1582 ], key=operator.itemgetter("label"))
1584 def fields_get(self, req, model):
1585 Model = req.session.model(model)
1586 fields = Model.fields_get(False, req.session.eval_context(req.context))
1589 @openerpweb.jsonrequest
1590 def get_fields(self, req, model, prefix='', parent_name= '',
1591 import_compat=True, parent_field_type=None,
1594 if import_compat and parent_field_type == "many2one":
1597 fields = self.fields_get(req, model)
1600 fields.pop('id', None)
1602 fields['.id'] = fields.pop('id', {'string': 'ID'})
1604 fields_sequence = sorted(fields.iteritems(),
1605 key=lambda field: field[1].get('string', ''))
1608 for field_name, field in fields_sequence:
1610 if exclude and field_name in exclude:
1612 if field.get('readonly'):
1613 # If none of the field's states unsets readonly, skip the field
1614 if all(dict(attrs).get('readonly', True)
1615 for attrs in field.get('states', {}).values()):
1618 id = prefix + (prefix and '/'or '') + field_name
1619 name = parent_name + (parent_name and '/' or '') + field['string']
1620 record = {'id': id, 'string': name,
1621 'value': id, 'children': False,
1622 'field_type': field.get('type'),
1623 'required': field.get('required'),
1624 'relation_field': field.get('relation_field')}
1625 records.append(record)
1627 if len(name.split('/')) < 3 and 'relation' in field:
1628 ref = field.pop('relation')
1629 record['value'] += '/id'
1630 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1632 if not import_compat or field['type'] == 'one2many':
1633 # m2m field in import_compat is childless
1634 record['children'] = True
1638 @openerpweb.jsonrequest
1639 def namelist(self,req, model, export_id):
1640 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1641 export = req.session.model("ir.exports").read([export_id])[0]
1642 export_fields_list = req.session.model("ir.exports.line").read(
1643 export['export_fields'])
1645 fields_data = self.fields_info(
1646 req, model, map(operator.itemgetter('name'), export_fields_list))
1649 {'name': field['name'], 'label': fields_data[field['name']]}
1650 for field in export_fields_list
1653 def fields_info(self, req, model, export_fields):
1655 fields = self.fields_get(req, model)
1657 # To make fields retrieval more efficient, fetch all sub-fields of a
1658 # given field at the same time. Because the order in the export list is
1659 # arbitrary, this requires ordering all sub-fields of a given field
1660 # together so they can be fetched at the same time
1662 # Works the following way:
1663 # * sort the list of fields to export, the default sorting order will
1664 # put the field itself (if present, for xmlid) and all of its
1665 # sub-fields right after it
1666 # * then, group on: the first field of the path (which is the same for
1667 # a field and for its subfields and the length of splitting on the
1668 # first '/', which basically means grouping the field on one side and
1669 # all of the subfields on the other. This way, we have the field (for
1670 # the xmlid) with length 1, and all of the subfields with the same
1671 # base but a length "flag" of 2
1672 # * if we have a normal field (length 1), just add it to the info
1673 # mapping (with its string) as-is
1674 # * otherwise, recursively call fields_info via graft_subfields.
1675 # all graft_subfields does is take the result of fields_info (on the
1676 # field's model) and prepend the current base (current field), which
1677 # rebuilds the whole sub-tree for the field
1679 # result: because we're not fetching the fields_get for half the
1680 # database models, fetching a namelist with a dozen fields (including
1681 # relational data) falls from ~6s to ~300ms (on the leads model).
1682 # export lists with no sub-fields (e.g. import_compatible lists with
1683 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1684 # there's a single fields_get to execute)
1685 for (base, length), subfields in itertools.groupby(
1686 sorted(export_fields),
1687 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1688 subfields = list(subfields)
1690 # subfields is a seq of $base/*rest, and not loaded yet
1691 info.update(self.graft_subfields(
1692 req, fields[base]['relation'], base, fields[base]['string'],
1696 info[base] = fields[base]['string']
1700 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1701 export_fields = [field.split('/', 1)[1] for field in fields]
1703 (prefix + '/' + k, prefix_string + '/' + v)
1704 for k, v in self.fields_info(req, model, export_fields).iteritems())
1706 #noinspection PyPropertyDefinition
1708 def content_type(self):
1709 """ Provides the format's content type """
1710 raise NotImplementedError()
1712 def filename(self, base):
1713 """ Creates a valid filename for the format (with extension) from the
1714 provided base name (exension-less)
1716 raise NotImplementedError()
1718 def from_data(self, fields, rows):
1719 """ Conversion method from OpenERP's export data to whatever the
1720 current export class outputs
1722 :params list fields: a list of fields to export
1723 :params list rows: a list of records to export
1727 raise NotImplementedError()
1729 @openerpweb.httprequest
1730 def index(self, req, data, token):
1731 model, fields, ids, domain, import_compat = \
1732 operator.itemgetter('model', 'fields', 'ids', 'domain',
1734 simplejson.loads(data))
1736 context = req.session.eval_context(req.context)
1737 Model = req.session.model(model)
1738 ids = ids or Model.search(domain, 0, False, False, context)
1740 field_names = map(operator.itemgetter('name'), fields)
1741 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1744 columns_headers = field_names
1746 columns_headers = [val['label'].strip() for val in fields]
1749 return req.make_response(self.from_data(columns_headers, import_data),
1750 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1751 ('Content-Type', self.content_type)],
1752 cookies={'fileToken': int(token)})
1754 class CSVExport(Export):
1755 _cp_path = '/web/export/csv'
1756 fmt = {'tag': 'csv', 'label': 'CSV'}
1759 def content_type(self):
1760 return 'text/csv;charset=utf8'
1762 def filename(self, base):
1763 return base + '.csv'
1765 def from_data(self, fields, rows):
1767 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1769 writer.writerow([name.encode('utf-8') for name in fields])
1774 if isinstance(d, basestring):
1775 d = d.replace('\n',' ').replace('\t',' ')
1777 d = d.encode('utf-8')
1778 except UnicodeError:
1780 if d is False: d = None
1782 writer.writerow(row)
1789 class ExcelExport(Export):
1790 _cp_path = '/web/export/xls'
1794 'error': None if xlwt else "XLWT required"
1798 def content_type(self):
1799 return 'application/vnd.ms-excel'
1801 def filename(self, base):
1802 return base + '.xls'
1804 def from_data(self, fields, rows):
1805 workbook = xlwt.Workbook()
1806 worksheet = workbook.add_sheet('Sheet 1')
1808 for i, fieldname in enumerate(fields):
1809 worksheet.write(0, i, fieldname)
1810 worksheet.col(i).width = 8000 # around 220 pixels
1812 style = xlwt.easyxf('align: wrap yes')
1814 for row_index, row in enumerate(rows):
1815 for cell_index, cell_value in enumerate(row):
1816 if isinstance(cell_value, basestring):
1817 cell_value = re.sub("\r", " ", cell_value)
1818 if cell_value is False: cell_value = None
1819 worksheet.write(row_index + 1, cell_index, cell_value, style)
1828 class Reports(View):
1829 _cp_path = "/web/report"
1830 POLLING_DELAY = 0.25
1832 'doc': 'application/vnd.ms-word',
1833 'html': 'text/html',
1834 'odt': 'application/vnd.oasis.opendocument.text',
1835 'pdf': 'application/pdf',
1836 'sxw': 'application/vnd.sun.xml.writer',
1837 'xls': 'application/vnd.ms-excel',
1840 @openerpweb.httprequest
1841 def index(self, req, action, token):
1842 action = simplejson.loads(action)
1844 report_srv = req.session.proxy("report")
1845 context = req.session.eval_context(
1846 common.nonliterals.CompoundContext(
1847 req.context or {}, action[ "context"]))
1850 report_ids = context["active_ids"]
1851 if 'report_type' in action:
1852 report_data['report_type'] = action['report_type']
1853 if 'datas' in action:
1854 if 'ids' in action['datas']:
1855 report_ids = action['datas'].pop('ids')
1856 report_data.update(action['datas'])
1858 report_id = report_srv.report(
1859 req.session._db, req.session._uid, req.session._password,
1860 action["report_name"], report_ids,
1861 report_data, context)
1863 report_struct = None
1865 report_struct = report_srv.report_get(
1866 req.session._db, req.session._uid, req.session._password, report_id)
1867 if report_struct["state"]:
1870 time.sleep(self.POLLING_DELAY)
1872 report = base64.b64decode(report_struct['result'])
1873 if report_struct.get('code') == 'zlib':
1874 report = zlib.decompress(report)
1875 report_mimetype = self.TYPES_MAPPING.get(
1876 report_struct['format'], 'octet-stream')
1878 if 'name' not in action:
1879 reports = req.session.model('ir.actions.report.xml')
1880 res_id = reports.search([('report_name', '=', action['report_name']),],
1881 0, False, False, context)
1883 file_name = reports.read(res_id[0], ['name'], context)['name']
1885 file_name = action['report_name']
1887 return req.make_response(report,
1889 # maybe we should take of what characters can appear in a file name?
1890 ('Content-Disposition', 'attachment; filename="%s.%s"' % (file_name, report_struct['format'])),
1891 ('Content-Type', report_mimetype),
1892 ('Content-Length', len(report))],
1893 cookies={'fileToken': int(token)})
1896 _cp_path = "/web/import"
1898 def fields_get(self, req, model):
1899 Model = req.session.model(model)
1900 fields = Model.fields_get(False, req.session.eval_context(req.context))
1903 @openerpweb.httprequest
1904 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1906 data = list(csv.reader(
1907 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1908 except csv.Error, e:
1910 return '<script>window.top.%s(%s);</script>' % (
1911 jsonp, simplejson.dumps({'error': {
1912 'message': 'Error parsing CSV file: %s' % e,
1913 # decodes each byte to a unicode character, which may or
1914 # may not be printable, but decoding will succeed.
1915 # Otherwise simplejson will try to decode the `str` using
1916 # utf-8, which is very likely to blow up on characters out
1917 # of the ascii range (in range [128, 256))
1918 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1921 return '<script>window.top.%s(%s);</script>' % (
1922 jsonp, simplejson.dumps(
1923 {'records': data[:10]}, encoding=csvcode))
1924 except UnicodeDecodeError:
1925 return '<script>window.top.%s(%s);</script>' % (
1926 jsonp, simplejson.dumps({
1927 'message': u"Failed to decode CSV file using encoding %s, "
1928 u"try switching to a different encoding" % csvcode
1931 @openerpweb.httprequest
1932 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1934 modle_obj = req.session.model(model)
1935 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1936 simplejson.loads(meta))
1939 if not (csvdel and len(csvdel) == 1):
1940 error = u"The CSV delimiter must be a single character"
1942 if not indices and fields:
1943 error = u"You must select at least one field to import"
1946 return '<script>window.top.%s(%s);</script>' % (
1947 jsonp, simplejson.dumps({'error': {'message': error}}))
1949 # skip ignored records (@skip parameter)
1950 # then skip empty lines (not valid csv)
1951 # nb: should these operations be reverted?
1952 rows_to_import = itertools.ifilter(
1955 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1958 # if only one index, itemgetter will return an atom rather than a tuple
1959 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1960 else: mapper = operator.itemgetter(*indices)
1965 # decode each data row
1967 [record.decode(csvcode) for record in row]
1968 for row in itertools.imap(mapper, rows_to_import)
1969 # don't insert completely empty rows (can happen due to fields
1970 # filtering in case of e.g. o2m content rows)
1973 except UnicodeDecodeError:
1974 error = u"Failed to decode CSV file using encoding %s" % csvcode
1975 except csv.Error, e:
1976 error = u"Could not process CSV file: %s" % e
1978 # If the file contains nothing,
1980 error = u"File to import is empty"
1982 return '<script>window.top.%s(%s);</script>' % (
1983 jsonp, simplejson.dumps({'error': {'message': error}}))
1986 (code, record, message, _nope) = modle_obj.import_data(
1987 fields, data, 'init', '', False,
1988 req.session.eval_context(req.context))
1989 except xmlrpclib.Fault, e:
1990 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1991 return '<script>window.top.%s(%s);</script>' % (
1992 jsonp, simplejson.dumps({'error':error}))
1995 return '<script>window.top.%s(%s);</script>' % (
1996 jsonp, simplejson.dumps({'success':True}))
1998 msg = u"Error during import: %s\n\nTrying to import record %r" % (
2000 return '<script>window.top.%s(%s);</script>' % (
2001 jsonp, simplejson.dumps({'error': {'message':msg}}))
2003 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: