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 #----------------------------------------------------------
34 # OpenERP Web web Controllers
35 #----------------------------------------------------------
38 def concat_xml(file_list):
39 """Concatenate xml files
41 :param list(str) file_list: list of files to check
42 :returns: (concatenation_result, checksum)
45 checksum = hashlib.new('sha1')
47 return '', checksum.hexdigest()
50 for fname in file_list:
51 with open(fname, 'rb') as fp:
53 checksum.update(contents)
55 xml = ElementTree.parse(fp).getroot()
58 root = ElementTree.Element(xml.tag)
59 #elif root.tag != xml.tag:
60 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
62 for child in xml.getchildren():
64 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
67 def concat_files(file_list, reader=None, intersperse=""):
68 """ Concatenates contents of all provided files
70 :param list(str) file_list: list of files to check
71 :param function reader: reading procedure for each file
72 :param str intersperse: string to intersperse between file contents
73 :returns: (concatenation_result, checksum)
76 checksum = hashlib.new('sha1')
78 return '', checksum.hexdigest()
82 with open(f, 'rb') as fp:
86 for fname in file_list:
87 contents = reader(fname)
88 checksum.update(contents)
89 files_content.append(contents)
91 files_concat = intersperse.join(files_content)
92 return files_concat, checksum.hexdigest()
94 html_template = """<!DOCTYPE html>
95 <html style="height: 100%%">
97 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
98 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
99 <title>OpenERP</title>
100 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
101 <link rel="stylesheet" href="/web/static/src/css/full.css" />
104 <script type="text/javascript">
106 var s = new openerp.init(%(modules)s);
116 # Validated by diff -u of sass2scss against:
117 # sass-convert -F sass -T scss openerp.sass openerp.scss
120 reComment = re.compile(r'//.*$')
121 reIndent = re.compile(r'^\s+')
122 reIgnore = re.compile(r'^\s*(//.*)?$')
123 reFixes = { re.compile(r'\(\((.*)\)\)') : r'(\1)', }
126 for l in src.split('\n'):
128 if reIgnore.search(l): continue
129 l = reComment.sub('', l)
131 indent = reIndent.match(l)
132 level = indent.end() if indent else 0
135 prevBlocks[lastLevel] = block
137 block[-1] = (block[-1], newBlock)
139 elif level<lastLevel:
140 block = prevBlocks[level]
144 for ereg, repl in reFixes.items():
145 l = ereg.sub(repl if type(repl)==str else repl(), l)
148 def write(sass, level=-1):
151 if type(sass)==tuple:
153 out += indent+sass[0]+" {\n"
155 out += write(e, level+1)
157 out = out.rstrip(" \n")
162 out += indent+sass+";\n"
166 def server_wide_modules(req):
167 addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest]
170 def manifest_glob(req, addons, key):
172 addons = server_wide_modules(req)
174 addons = addons.split(',')
177 manifest = openerpweb.addons_manifest.get(addon, None)
180 # ensure does not ends with /
181 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
182 globlist = manifest.get(key, [])
183 for pattern in globlist:
184 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
185 r.append((path, path[len(addons_path):]))
188 def manifest_list(req, mods, extension):
190 path = '/web/webclient/' + extension
192 path += '?mods=' + mods
194 files = manifest_glob(req, mods, extension)
195 i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
196 req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
198 return [wp for _fp, wp in files]
200 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
202 def get_last_modified(files):
203 """ Returns the modification time of the most recently modified
206 :param list(str) files: names of files to check
207 :return: most recent modification time amongst the fileset
208 :rtype: datetime.datetime
212 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
214 return datetime.datetime(1970, 1, 1)
216 def make_conditional(req, response, last_modified=None, etag=None):
217 """ Makes the provided response conditional based upon the request,
218 and mandates revalidation from clients
220 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
221 setting ``last_modified`` and ``etag`` correctly on the response object
223 :param req: OpenERP request
224 :type req: web.common.http.WebRequest
225 :param response: Werkzeug response
226 :type response: werkzeug.wrappers.Response
227 :param datetime.datetime last_modified: last modification date of the response content
228 :param str etag: some sort of checksum of the content (deep etag)
229 :return: the response object provided
230 :rtype: werkzeug.wrappers.Response
232 response.cache_control.must_revalidate = True
233 response.cache_control.max_age = 0
235 response.last_modified = last_modified
237 response.set_etag(etag)
238 return response.make_conditional(req.httprequest)
240 class Home(openerpweb.Controller):
243 @openerpweb.httprequest
244 def index(self, req, s_action=None, **kw):
245 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, None, 'js'))
246 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, None, 'css'))
248 r = html_template % {
251 'modules': simplejson.dumps(server_wide_modules(req)),
252 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
256 @openerpweb.httprequest
257 def login(self, req, db, login, key):
258 return self._login(req, db, login, key)
260 def _login(self, req, db, login, key, redirect_url='/'):
261 req.session.authenticate(db, login, key, {})
262 redirect = werkzeug.utils.redirect(redirect_url, 303)
263 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
264 redirect.set_cookie('instance0|session_id', cookie_val)
267 class WebClient(openerpweb.Controller):
268 _cp_path = "/web/webclient"
270 @openerpweb.jsonrequest
271 def csslist(self, req, mods=None):
272 return manifest_list(req, mods, 'css')
274 @openerpweb.jsonrequest
275 def jslist(self, req, mods=None):
276 return manifest_list(req, mods, 'js')
278 @openerpweb.jsonrequest
279 def qweblist(self, req, mods=None):
280 return manifest_list(req, mods, 'qweb')
282 @openerpweb.httprequest
283 def css(self, req, mods=None):
284 files = list(manifest_glob(req, mods, 'css'))
285 last_modified = get_last_modified(f[0] for f in files)
286 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
287 return werkzeug.wrappers.Response(status=304)
289 file_map = dict(files)
291 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
292 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
295 """read the a css file and absolutify all relative uris"""
296 with open(f, 'rb') as fp:
297 data = fp.read().decode('utf-8')
300 # convert FS path into web path
301 web_dir = '/'.join(os.path.dirname(path).split(os.path.sep))
305 r"""@import \1%s/""" % (web_dir,),
311 r"""url(\1%s/""" % (web_dir,),
314 return data.encode('utf-8')
316 content, checksum = concat_files((f[0] for f in files), reader)
318 return make_conditional(
319 req, req.make_response(content, [('Content-Type', 'text/css')]),
320 last_modified, checksum)
322 @openerpweb.httprequest
323 def js(self, req, mods=None):
324 files = [f[0] for f in manifest_glob(req, mods, 'js')]
325 last_modified = get_last_modified(files)
326 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
327 return werkzeug.wrappers.Response(status=304)
329 content, checksum = concat_files(files, intersperse=';')
331 return make_conditional(
332 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
333 last_modified, checksum)
335 @openerpweb.httprequest
336 def qweb(self, req, mods=None):
337 files = [f[0] for f in manifest_glob(req, mods, 'qweb')]
338 last_modified = get_last_modified(files)
339 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
340 return werkzeug.wrappers.Response(status=304)
342 content, checksum = concat_xml(files)
344 return make_conditional(
345 req, req.make_response(content, [('Content-Type', 'text/xml')]),
346 last_modified, checksum)
348 @openerpweb.jsonrequest
349 def translations(self, req, mods, lang):
350 lang_model = req.session.model('res.lang')
351 ids = lang_model.search([("code", "=", lang)])
353 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
354 "grouping", "decimal_point", "thousands_sep"])
362 langs = lang.split(separator)
363 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
366 for addon_name in mods:
367 transl = {"messages":[]}
368 transs[addon_name] = transl
369 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
371 f_name = os.path.join(addons_path, addon_name, "i18n", l + ".po")
372 if not os.path.exists(f_name):
375 with open(f_name) as t_file:
376 po = babel.messages.pofile.read_po(t_file)
380 if x.id and x.string and "openerp-web" in x.auto_comments:
381 transl["messages"].append({'id': x.id, 'string': x.string})
382 return {"modules": transs,
383 "lang_parameters": lang_obj}
385 @openerpweb.jsonrequest
386 def version_info(self, req):
388 "version": common.release.version
391 class Proxy(openerpweb.Controller):
392 _cp_path = '/web/proxy'
394 @openerpweb.jsonrequest
395 def load(self, req, path):
396 """ Proxies an HTTP request through a JSON request.
398 It is strongly recommended to not request binary files through this,
399 as the result will be a binary data blob as well.
401 :param req: OpenERP request
402 :param path: actual request path
403 :return: file content
405 from werkzeug.test import Client
406 from werkzeug.wrappers import BaseResponse
408 return Client(req.httprequest.app, BaseResponse).get(path).data
410 class Database(openerpweb.Controller):
411 _cp_path = "/web/database"
413 @openerpweb.jsonrequest
414 def get_list(self, req):
415 proxy = req.session.proxy("db")
417 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
419 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
420 dbs = [i for i in dbs if re.match(r, i)]
421 return {"db_list": dbs}
423 @openerpweb.jsonrequest
424 def create(self, req, fields):
425 params = dict(map(operator.itemgetter('name', 'value'), fields))
427 params['super_admin_pwd'],
429 bool(params.get('demo_data')),
431 params['create_admin_pwd']
434 return req.session.proxy("db").create_database(*create_attrs)
436 @openerpweb.jsonrequest
437 def drop(self, req, fields):
438 password, db = operator.itemgetter(
439 'drop_pwd', 'drop_db')(
440 dict(map(operator.itemgetter('name', 'value'), fields)))
443 return req.session.proxy("db").drop(password, db)
444 except xmlrpclib.Fault, e:
445 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
446 return {'error': e.faultCode, 'title': 'Drop Database'}
447 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
449 @openerpweb.httprequest
450 def backup(self, req, backup_db, backup_pwd, token):
452 db_dump = base64.b64decode(
453 req.session.proxy("db").dump(backup_pwd, backup_db))
454 filename = "%(db)s_%(timestamp)s.dump" % {
456 'timestamp': datetime.datetime.utcnow().strftime(
457 "%Y-%m-%d_%H-%M-%SZ")
459 return req.make_response(db_dump,
460 [('Content-Type', 'application/octet-stream; charset=binary'),
461 ('Content-Disposition', 'attachment; filename="' + filename + '"')],
462 {'fileToken': int(token)}
464 except xmlrpclib.Fault, e:
465 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
467 @openerpweb.httprequest
468 def restore(self, req, db_file, restore_pwd, new_db):
470 data = base64.b64encode(db_file.read())
471 req.session.proxy("db").restore(restore_pwd, new_db, data)
473 except xmlrpclib.Fault, e:
474 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
475 raise Exception("AccessDenied")
477 @openerpweb.jsonrequest
478 def change_password(self, req, fields):
479 old_password, new_password = operator.itemgetter(
480 'old_pwd', 'new_pwd')(
481 dict(map(operator.itemgetter('name', 'value'), fields)))
483 return req.session.proxy("db").change_admin_password(old_password, new_password)
484 except xmlrpclib.Fault, e:
485 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
486 return {'error': e.faultCode, 'title': 'Change Password'}
487 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
489 def topological_sort(modules):
490 """ Return a list of module names sorted so that their dependencies of the
491 modules are listed before the module itself
493 modules is a dict of {module_name: dependencies}
495 :param modules: modules to sort
500 dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
501 # incoming edge: dependency on other module (if a depends on b, a has an
502 # incoming edge from b, aka there's an edge from b to a)
503 # outgoing edge: other module depending on this one
505 # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
506 #L ← Empty list that will contain the sorted nodes
508 #S ← Set of all nodes with no outgoing edges (modules on which no other
510 S = set(module for module in modules if module not in dependencies)
513 #function visit(node n)
515 #if n has not been visited yet then
519 #change: n not web module, can not be resolved, ignore
520 if n not in modules: return
521 #for each node m with an edge from m to n do (dependencies of n)
527 #for each node n in S do
533 class Session(openerpweb.Controller):
534 _cp_path = "/web/session"
536 def session_info(self, req):
537 req.session.ensure_valid()
539 "session_id": req.session_id,
540 "uid": req.session._uid,
541 "context": req.session.get_context() if req.session._uid else {},
542 "db": req.session._db,
543 "login": req.session._login,
544 "openerp_entreprise": req.session.openerp_entreprise(),
547 @openerpweb.jsonrequest
548 def get_session_info(self, req):
549 return self.session_info(req)
551 @openerpweb.jsonrequest
552 def authenticate(self, req, db, login, password, base_location=None):
553 wsgienv = req.httprequest.environ
554 release = common.release
556 base_location=base_location,
557 HTTP_HOST=wsgienv['HTTP_HOST'],
558 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
559 user_agent="%s / %s" % (release.name, release.version),
561 req.session.authenticate(db, login, password, env)
563 return self.session_info(req)
565 @openerpweb.jsonrequest
566 def change_password (self,req,fields):
567 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
568 dict(map(operator.itemgetter('name', 'value'), fields)))
569 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
570 return {'error':'All passwords have to be filled.','title': 'Change Password'}
571 if new_password != confirm_password:
572 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
574 if req.session.model('res.users').change_password(
575 old_password, new_password):
576 return {'new_password':new_password}
578 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
579 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
581 @openerpweb.jsonrequest
582 def sc_list(self, req):
583 return req.session.model('ir.ui.view_sc').get_sc(
584 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
586 @openerpweb.jsonrequest
587 def get_lang_list(self, req):
590 'lang_list': (req.session.proxy("db").list_lang() or []),
594 return {"error": e, "title": "Languages"}
596 @openerpweb.jsonrequest
597 def modules(self, req):
598 # Compute available candidates module
599 loadable = openerpweb.addons_manifest
600 loaded = set(req.config.server_wide_modules)
601 candidates = [mod for mod in loadable if mod not in loaded]
603 # already installed modules have no dependencies
604 modules = dict.fromkeys(loaded, [])
606 # Compute auto_install modules that might be on the web side only
607 modules.update((name, openerpweb.addons_manifest[name].get('depends', []))
608 for name in candidates
609 if openerpweb.addons_manifest[name].get('auto_install'))
611 # Retrieve database installed modules
612 Modules = req.session.model('ir.module.module')
613 for module in Modules.search_read(
614 [('state','=','installed'), ('name','in', candidates)],
615 ['name', 'dependencies_id']):
616 deps = module.get('dependencies_id')
619 operator.itemgetter('name'),
620 req.session.model('ir.module.module.dependency').read(deps, ['name']))
621 modules[module['name']] = list(
622 set(modules.get(module['name'], []) + dependencies))
624 sorted_modules = topological_sort(modules)
625 return [module for module in sorted_modules if module not in loaded]
627 @openerpweb.jsonrequest
628 def eval_domain_and_context(self, req, contexts, domains,
630 """ Evaluates sequences of domains and contexts, composing them into
631 a single context, domain or group_by sequence.
633 :param list contexts: list of contexts to merge together. Contexts are
634 evaluated in sequence, all previous contexts
635 are part of their own evaluation context
636 (starting at the session context).
637 :param list domains: list of domains to merge together. Domains are
638 evaluated in sequence and appended to one another
639 (implicit AND), their evaluation domain is the
640 result of merging all contexts.
641 :param list group_by_seq: list of domains (which may be in a different
642 order than the ``contexts`` parameter),
643 evaluated in sequence, their ``'group_by'``
644 key is extracted if they have one.
649 the global context created by merging all of
653 the concatenation of all domains
656 a list of fields to group by, potentially empty (in which case
657 no group by should be performed)
659 context, domain = eval_context_and_domain(req.session,
660 common.nonliterals.CompoundContext(*(contexts or [])),
661 common.nonliterals.CompoundDomain(*(domains or [])))
663 group_by_sequence = []
664 for candidate in (group_by_seq or []):
665 ctx = req.session.eval_context(candidate, context)
666 group_by = ctx.get('group_by')
669 elif isinstance(group_by, basestring):
670 group_by_sequence.append(group_by)
672 group_by_sequence.extend(group_by)
677 'group_by': group_by_sequence
680 @openerpweb.jsonrequest
681 def save_session_action(self, req, the_action):
683 This method store an action object in the session object and returns an integer
684 identifying that action. The method get_session_action() can be used to get
687 :param the_action: The action to save in the session.
688 :type the_action: anything
689 :return: A key identifying the saved action.
692 saved_actions = req.httpsession.get('saved_actions')
693 if not saved_actions:
694 saved_actions = {"next":0, "actions":{}}
695 req.httpsession['saved_actions'] = saved_actions
696 # we don't allow more than 10 stored actions
697 if len(saved_actions["actions"]) >= 10:
698 del saved_actions["actions"][min(saved_actions["actions"])]
699 key = saved_actions["next"]
700 saved_actions["actions"][key] = the_action
701 saved_actions["next"] = key + 1
704 @openerpweb.jsonrequest
705 def get_session_action(self, req, key):
707 Gets back a previously saved action. This method can return None if the action
708 was saved since too much time (this case should be handled in a smart way).
710 :param key: The key given by save_session_action()
712 :return: The saved action or None.
715 saved_actions = req.httpsession.get('saved_actions')
716 if not saved_actions:
718 return saved_actions["actions"].get(key)
720 @openerpweb.jsonrequest
721 def check(self, req):
722 req.session.assert_valid()
725 @openerpweb.jsonrequest
726 def destroy(self, req):
727 req.session._suicide = True
729 def eval_context_and_domain(session, context, domain=None):
730 e_context = session.eval_context(context)
731 # should we give the evaluated context as an evaluation context to the domain?
732 e_domain = session.eval_domain(domain or [])
734 return e_context, e_domain
736 def load_actions_from_ir_values(req, key, key2, models, meta):
737 context = req.session.eval_context(req.context)
738 Values = req.session.model('ir.values')
739 actions = Values.get(key, key2, models, meta, context)
741 return [(id, name, clean_action(req, action))
742 for id, name, action in actions]
744 def clean_action(req, action, do_not_eval=False):
745 action.setdefault('flags', {})
747 context = req.session.eval_context(req.context)
748 eval_ctx = req.session.evaluation_context(context)
751 # values come from the server, we can just eval them
752 if action.get('context') and isinstance(action.get('context'), basestring):
753 action['context'] = eval( action['context'], eval_ctx ) or {}
755 if action.get('domain') and isinstance(action.get('domain'), basestring):
756 action['domain'] = eval( action['domain'], eval_ctx ) or []
758 if 'context' in action:
759 action['context'] = parse_context(action['context'], req.session)
760 if 'domain' in action:
761 action['domain'] = parse_domain(action['domain'], req.session)
763 action_type = action.setdefault('type', 'ir.actions.act_window_close')
764 if action_type == 'ir.actions.act_window':
765 return fix_view_modes(action)
768 # I think generate_views,fix_view_modes should go into js ActionManager
769 def generate_views(action):
771 While the server generates a sequence called "views" computing dependencies
772 between a bunch of stuff for views coming directly from the database
773 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
774 to return custom view dictionaries generated on the fly.
776 In that case, there is no ``views`` key available on the action.
778 Since the web client relies on ``action['views']``, generate it here from
779 ``view_mode`` and ``view_id``.
781 Currently handles two different cases:
783 * no view_id, multiple view_mode
784 * single view_id, single view_mode
786 :param dict action: action descriptor dictionary to generate a views key for
788 view_id = action.get('view_id') or False
789 if isinstance(view_id, (list, tuple)):
792 # providing at least one view mode is a requirement, not an option
793 view_modes = action['view_mode'].split(',')
795 if len(view_modes) > 1:
797 raise ValueError('Non-db action dictionaries should provide '
798 'either multiple view modes or a single view '
799 'mode and an optional view id.\n\n Got view '
800 'modes %r and view id %r for action %r' % (
801 view_modes, view_id, action))
802 action['views'] = [(False, mode) for mode in view_modes]
804 action['views'] = [(view_id, view_modes[0])]
806 def fix_view_modes(action):
807 """ For historical reasons, OpenERP has weird dealings in relation to
808 view_mode and the view_type attribute (on window actions):
810 * one of the view modes is ``tree``, which stands for both list views
812 * the choice is made by checking ``view_type``, which is either
813 ``form`` for a list view or ``tree`` for an actual tree view
815 This methods simply folds the view_type into view_mode by adding a
816 new view mode ``list`` which is the result of the ``tree`` view_mode
817 in conjunction with the ``form`` view_type.
819 TODO: this should go into the doc, some kind of "peculiarities" section
821 :param dict action: an action descriptor
822 :returns: nothing, the action is modified in place
824 if not action.get('views'):
825 generate_views(action)
828 for index, (id, mode) in enumerate(action['views']):
833 if action.pop('view_type', 'form') != 'form':
837 [id, mode if mode != 'tree' else 'list']
838 for id, mode in action['views']
843 class Menu(openerpweb.Controller):
844 _cp_path = "/web/menu"
846 @openerpweb.jsonrequest
848 return {'data': self.do_load(req)}
850 @openerpweb.jsonrequest
851 def get_user_roots(self, req):
852 return self.do_get_user_roots(req)
854 def do_get_user_roots(self, req):
855 """ Return all root menu ids visible for the session user.
857 :param req: A request object, with an OpenERP session attribute
858 :type req: < session -> OpenERPSession >
859 :return: the root menu ids
863 context = s.eval_context(req.context)
864 Menus = s.model('ir.ui.menu')
865 # If a menu action is defined use its domain to get the root menu items
866 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
868 menu_domain = [('parent_id', '=', False)]
870 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
872 menu_domain = ast.literal_eval(domain_string)
874 return Menus.search(menu_domain, 0, False, False, context)
876 def do_load(self, req):
877 """ Loads all menu items (all applications and their sub-menus).
879 :param req: A request object, with an OpenERP session attribute
880 :type req: < session -> OpenERPSession >
881 :return: the menu root
882 :rtype: dict('children': menu_nodes)
884 context = req.session.eval_context(req.context)
885 Menus = req.session.model('ir.ui.menu')
887 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
888 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
890 # menus are loaded fully unlike a regular tree view, cause there are a
891 # limited number of items (752 when all 6.1 addons are installed)
892 menu_ids = Menus.search([], 0, False, False, context)
893 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
894 # adds roots at the end of the sequence, so that they will overwrite
895 # equivalent menu items from full menu read when put into id:item
896 # mapping, resulting in children being correctly set on the roots.
897 menu_items.extend(menu_roots)
899 # make a tree using parent_id
900 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
901 for menu_item in menu_items:
902 if menu_item['parent_id']:
903 parent = menu_item['parent_id'][0]
906 if parent in menu_items_map:
907 menu_items_map[parent].setdefault(
908 'children', []).append(menu_item)
910 # sort by sequence a tree using parent_id
911 for menu_item in menu_items:
912 menu_item.setdefault('children', []).sort(
913 key=operator.itemgetter('sequence'))
917 @openerpweb.jsonrequest
918 def action(self, req, menu_id):
919 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
920 [('ir.ui.menu', menu_id)], False)
921 return {"action": actions}
923 class DataSet(openerpweb.Controller):
924 _cp_path = "/web/dataset"
926 @openerpweb.jsonrequest
927 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
928 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
929 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
931 """ Performs a search() followed by a read() (if needed) using the
932 provided search criteria
934 :param req: a JSON-RPC request object
935 :type req: openerpweb.JsonRequest
936 :param str model: the name of the model to search on
937 :param fields: a list of the fields to return in the result records
939 :param int offset: from which index should the results start being returned
940 :param int limit: the maximum number of records to return
941 :param list domain: the search domain for the query
942 :param list sort: sorting directives
943 :returns: A structure (dict) with two keys: ids (all the ids matching
944 the (domain, context) pair) and records (paginated records
945 matching fields selection set)
948 Model = req.session.model(model)
950 context, domain = eval_context_and_domain(
951 req.session, req.context, domain)
953 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
954 if limit and len(ids) == limit:
955 length = Model.search_count(domain, context)
957 length = len(ids) + (offset or 0)
958 if fields and fields == ['id']:
959 # shortcut read if we only want the ids
962 'records': [{'id': id} for id in ids]
965 records = Model.read(ids, fields or False, context)
966 records.sort(key=lambda obj: ids.index(obj['id']))
972 @openerpweb.jsonrequest
973 def load(self, req, model, id, fields):
974 m = req.session.model(model)
976 r = m.read([id], False, req.session.eval_context(req.context))
979 return {'value': value}
981 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
982 has_domain = domain_id is not None and domain_id < len(args)
983 has_context = context_id is not None and context_id < len(args)
985 domain = args[domain_id] if has_domain else []
986 context = args[context_id] if has_context else {}
987 c, d = eval_context_and_domain(req.session, context, domain)
993 return self._call_kw(req, model, method, args, {})
995 def _call_kw(self, req, model, method, args, kwargs):
996 for i in xrange(len(args)):
997 if isinstance(args[i], common.nonliterals.BaseContext):
998 args[i] = req.session.eval_context(args[i])
999 elif isinstance(args[i], common.nonliterals.BaseDomain):
1000 args[i] = req.session.eval_domain(args[i])
1001 for k in kwargs.keys():
1002 if isinstance(kwargs[k], common.nonliterals.BaseContext):
1003 kwargs[k] = req.session.eval_context(kwargs[k])
1004 elif isinstance(kwargs[k], common.nonliterals.BaseDomain):
1005 kwargs[k] = req.session.eval_domain(kwargs[k])
1007 # Temporary implements future display_name special field for model#read()
1008 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1009 if 'display_name' in args[1]:
1010 names = req.session.model(model).name_get(args[0], **kwargs)
1011 args[1].remove('display_name')
1012 r = getattr(req.session.model(model), method)(*args, **kwargs)
1013 for i in range(len(r)):
1014 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1017 return getattr(req.session.model(model), method)(*args, **kwargs)
1019 @openerpweb.jsonrequest
1020 def onchange(self, req, model, method, args, context_id=None):
1021 """ Support method for handling onchange calls: behaves much like call
1022 with the following differences:
1024 * Does not take a domain_id
1025 * Is aware of the return value's structure, and will parse the domains
1026 if needed in order to return either parsed literal domains (in JSON)
1027 or non-literal domain instances, allowing those domains to be used
1031 :type req: web.common.http.JsonRequest
1032 :param str model: object type on which to call the method
1033 :param str method: name of the onchange handler method
1034 :param list args: arguments to call the onchange handler with
1035 :param int context_id: index of the context object in the list of
1037 :return: result of the onchange call with all domains parsed
1039 result = self.call_common(req, model, method, args, context_id=context_id)
1040 if not result or 'domain' not in result:
1043 result['domain'] = dict(
1044 (k, parse_domain(v, req.session))
1045 for k, v in result['domain'].iteritems())
1049 @openerpweb.jsonrequest
1050 def call(self, req, model, method, args, domain_id=None, context_id=None):
1051 return self.call_common(req, model, method, args, domain_id, context_id)
1053 @openerpweb.jsonrequest
1054 def call_kw(self, req, model, method, args, kwargs):
1055 return self._call_kw(req, model, method, args, kwargs)
1057 @openerpweb.jsonrequest
1058 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1059 action = self.call_common(req, model, method, args, domain_id, context_id)
1060 if isinstance(action, dict) and action.get('type') != '':
1061 return {'result': clean_action(req, action)}
1062 return {'result': False}
1064 @openerpweb.jsonrequest
1065 def exec_workflow(self, req, model, id, signal):
1066 return req.session.exec_workflow(model, id, signal)
1068 class DataGroup(openerpweb.Controller):
1069 _cp_path = "/web/group"
1070 @openerpweb.jsonrequest
1071 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1072 Model = req.session.model(model)
1073 context, domain = eval_context_and_domain(req.session, req.context, domain)
1075 return Model.read_group(
1076 domain or [], fields, group_by_fields, 0, False,
1077 dict(context, group_by=group_by_fields), sort or False)
1079 class View(openerpweb.Controller):
1080 _cp_path = "/web/view"
1082 def fields_view_get(self, req, model, view_id, view_type,
1083 transform=True, toolbar=False, submenu=False):
1084 Model = req.session.model(model)
1085 context = req.session.eval_context(req.context)
1086 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1087 # todo fme?: check that we should pass the evaluated context here
1088 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1089 if toolbar and transform:
1090 self.process_toolbar(req, fvg['toolbar'])
1093 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1094 # depending on how it feels, xmlrpclib.ServerProxy can translate
1095 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1096 # enjoy unicode strings which can not be trivially converted to
1097 # strings, and it blows up during parsing.
1099 # So ensure we fix this retardation by converting view xml back to
1101 if isinstance(fvg['arch'], unicode):
1102 arch = fvg['arch'].encode('utf-8')
1105 fvg['arch_string'] = arch
1108 evaluation_context = session.evaluation_context(context or {})
1109 xml = self.transform_view(arch, session, evaluation_context)
1111 xml = ElementTree.fromstring(arch)
1112 fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
1114 if 'id' in fvg['fields']:
1115 # Special case for id's
1116 id_field = fvg['fields']['id']
1117 id_field['original_type'] = id_field['type']
1118 id_field['type'] = 'id'
1120 for field in fvg['fields'].itervalues():
1121 if field.get('views'):
1122 for view in field["views"].itervalues():
1123 self.process_view(session, view, None, transform)
1124 if field.get('domain'):
1125 field["domain"] = parse_domain(field["domain"], session)
1126 if field.get('context'):
1127 field["context"] = parse_context(field["context"], session)
1129 def process_toolbar(self, req, toolbar):
1131 The toolbar is a mapping of section_key: [action_descriptor]
1133 We need to clean all those actions in order to ensure correct
1136 for actions in toolbar.itervalues():
1137 for action in actions:
1138 if 'context' in action:
1139 action['context'] = parse_context(
1140 action['context'], req.session)
1141 if 'domain' in action:
1142 action['domain'] = parse_domain(
1143 action['domain'], req.session)
1145 @openerpweb.jsonrequest
1146 def add_custom(self, req, view_id, arch):
1147 CustomView = req.session.model('ir.ui.view.custom')
1149 'user_id': req.session._uid,
1152 }, req.session.eval_context(req.context))
1153 return {'result': True}
1155 @openerpweb.jsonrequest
1156 def undo_custom(self, req, view_id, reset=False):
1157 CustomView = req.session.model('ir.ui.view.custom')
1158 context = req.session.eval_context(req.context)
1159 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1160 0, False, False, context)
1163 CustomView.unlink(vcustom, context)
1165 CustomView.unlink([vcustom[0]], context)
1166 return {'result': True}
1167 return {'result': False}
1169 def transform_view(self, view_string, session, context=None):
1170 # transform nodes on the fly via iterparse, instead of
1171 # doing it statically on the parsing result
1172 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1174 for event, elem in parser:
1175 if event == "start":
1178 self.parse_domains_and_contexts(elem, session)
1181 def parse_domains_and_contexts(self, elem, session):
1182 """ Converts domains and contexts from the view into Python objects,
1183 either literals if they can be parsed by literal_eval or a special
1184 placeholder object if the domain or context refers to free variables.
1186 :param elem: the current node being parsed
1187 :type param: xml.etree.ElementTree.Element
1188 :param session: OpenERP session object, used to store and retrieve
1190 :type session: openerpweb.openerpweb.OpenERPSession
1192 for el in ['domain', 'filter_domain']:
1193 domain = elem.get(el, '').strip()
1195 elem.set(el, parse_domain(domain, session))
1196 elem.set(el + '_string', domain)
1197 for el in ['context', 'default_get']:
1198 context_string = elem.get(el, '').strip()
1200 elem.set(el, parse_context(context_string, session))
1201 elem.set(el + '_string', context_string)
1203 @openerpweb.jsonrequest
1204 def load(self, req, model, view_id, view_type, toolbar=False):
1205 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1207 def parse_domain(domain, session):
1208 """ Parses an arbitrary string containing a domain, transforms it
1209 to either a literal domain or a :class:`common.nonliterals.Domain`
1211 :param domain: the domain to parse, if the domain is not a string it
1212 is assumed to be a literal domain and is returned as-is
1213 :param session: Current OpenERP session
1214 :type session: openerpweb.openerpweb.OpenERPSession
1216 if not isinstance(domain, basestring):
1219 return ast.literal_eval(domain)
1222 return common.nonliterals.Domain(session, domain)
1224 def parse_context(context, session):
1225 """ Parses an arbitrary string containing a context, transforms it
1226 to either a literal context or a :class:`common.nonliterals.Context`
1228 :param context: the context to parse, if the context is not a string it
1229 is assumed to be a literal domain and is returned as-is
1230 :param session: Current OpenERP session
1231 :type session: openerpweb.openerpweb.OpenERPSession
1233 if not isinstance(context, basestring):
1236 return ast.literal_eval(context)
1238 return common.nonliterals.Context(session, context)
1240 class ListView(View):
1241 _cp_path = "/web/listview"
1243 def process_colors(self, view, row, context):
1244 colors = view['arch']['attrs'].get('colors')
1251 for pair in colors.split(';')
1252 if eval(pair.split(':')[1], dict(context, **row))
1257 elif len(color) == 1:
1261 class TreeView(View):
1262 _cp_path = "/web/treeview"
1264 @openerpweb.jsonrequest
1265 def action(self, req, model, id):
1266 return load_actions_from_ir_values(
1267 req,'action', 'tree_but_open',[(model, id)],
1270 class SearchView(View):
1271 _cp_path = "/web/searchview"
1273 @openerpweb.jsonrequest
1274 def load(self, req, model, view_id):
1275 fields_view = self.fields_view_get(req, model, view_id, 'search')
1276 return {'fields_view': fields_view}
1278 @openerpweb.jsonrequest
1279 def fields_get(self, req, model):
1280 Model = req.session.model(model)
1281 fields = Model.fields_get(False, req.session.eval_context(req.context))
1282 for field in fields.values():
1283 # shouldn't convert the views too?
1284 if field.get('domain'):
1285 field["domain"] = parse_domain(field["domain"], req.session)
1286 if field.get('context'):
1287 field["context"] = parse_context(field["context"], req.session)
1288 return {'fields': fields}
1290 @openerpweb.jsonrequest
1291 def get_filters(self, req, model):
1292 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1293 Model = req.session.model("ir.filters")
1294 filters = Model.get_filters(model)
1295 for filter in filters:
1297 parsed_context = parse_context(filter["context"], req.session)
1298 filter["context"] = (parsed_context
1299 if not isinstance(parsed_context, common.nonliterals.BaseContext)
1300 else req.session.eval_context(parsed_context))
1302 parsed_domain = parse_domain(filter["domain"], req.session)
1303 filter["domain"] = (parsed_domain
1304 if not isinstance(parsed_domain, common.nonliterals.BaseDomain)
1305 else req.session.eval_domain(parsed_domain))
1307 logger.exception("Failed to parse custom filter %s in %s",
1308 filter['name'], model)
1309 filter['disabled'] = True
1310 del filter['context']
1311 del filter['domain']
1315 @openerpweb.jsonrequest
1316 def add_to_dashboard(self, req, menu_id, action_id, context_to_save, domain, view_mode, name=''):
1317 to_eval = common.nonliterals.CompoundContext(context_to_save)
1318 to_eval.session = req.session
1319 ctx = dict((k, v) for k, v in to_eval.evaluate().iteritems()
1320 if not k.startswith('search_default_'))
1321 ctx['dashboard_merge_domains_contexts'] = False # TODO: replace this 6.1 workaround by attribute on <action/>
1322 domain = common.nonliterals.CompoundDomain(domain)
1323 domain.session = req.session
1324 domain = domain.evaluate()
1326 dashboard_action = load_actions_from_ir_values(req, 'action', 'tree_but_open',
1327 [('ir.ui.menu', menu_id)], False)
1328 if dashboard_action:
1329 action = dashboard_action[0][2]
1330 if action['res_model'] == 'board.board' and action['views'][0][1] == 'form':
1331 # Maybe should check the content instead of model board.board ?
1332 view_id = action['views'][0][0]
1333 board = req.session.model(action['res_model']).fields_view_get(view_id, 'form')
1334 if board and 'arch' in board:
1335 xml = ElementTree.fromstring(board['arch'])
1336 column = xml.find('./board/column')
1337 if column is not None:
1338 new_action = ElementTree.Element('action', {
1339 'name' : str(action_id),
1341 'view_mode' : view_mode,
1342 'context' : str(ctx),
1343 'domain' : str(domain)
1345 column.insert(0, new_action)
1346 arch = ElementTree.tostring(xml, 'utf-8')
1347 return req.session.model('ir.ui.view.custom').create({
1348 'user_id': req.session._uid,
1351 }, req.session.eval_context(req.context))
1355 class Binary(openerpweb.Controller):
1356 _cp_path = "/web/binary"
1358 @openerpweb.httprequest
1359 def image(self, req, model, id, field, **kw):
1360 last_update = '__last_update'
1361 Model = req.session.model(model)
1362 context = req.session.eval_context(req.context)
1363 headers = [('Content-Type', 'image/png')]
1364 etag = req.httprequest.headers.get('If-None-Match')
1365 hashed_session = hashlib.md5(req.session_id).hexdigest()
1367 if not id and hashed_session == etag:
1368 return werkzeug.wrappers.Response(status=304)
1370 date = Model.read([int(id)], [last_update], context)[0].get(last_update)
1371 if hashlib.md5(date).hexdigest() == etag:
1372 return werkzeug.wrappers.Response(status=304)
1374 retag = hashed_session
1377 res = Model.default_get([field], context).get(field)
1378 image_data = base64.b64decode(res)
1382 except (ValueError):
1383 # objects might use virtual ids as string
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}}))