X-Git-Url: http://git.inspyration.org/?a=blobdiff_plain;f=addons%2Fweb%2Fcontrollers%2Fmain.py;h=397a6a0d302e1c5401acd32b93ef21cba495643c;hb=35895bde21813782b58a20a06a7b2f9e033d560d;hp=9d71451a344e13a222a68f24c8575be064955c4e;hpb=ea82b312865db0bac4f6a951a0d023483dd5c0dd;p=odoo%2Fodoo.git diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 9d71451..397a6a0 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import ast import base64 import csv import glob @@ -8,151 +9,193 @@ import operator import os import re import simplejson -import textwrap -import xmlrpclib import time +import xmlrpclib import zlib -import webrelease from xml.etree import ElementTree from cStringIO import StringIO -import web.common.dispatch as openerpweb -import web.common.ast -import web.common.nonliterals -openerpweb.ast = web.common.ast -openerpweb.nonliterals = web.common.nonliterals - -from babel.messages.pofile import read_po - -# Should move to openerpweb.Xml2Json -class Xml2Json: - # xml2json-direct - # Simple and straightforward XML-to-JSON converter in Python - # New BSD Licensed - # - # URL: http://code.google.com/p/xml2json-direct/ - @staticmethod - def convert_to_json(s): - return simplejson.dumps( - Xml2Json.convert_to_structure(s), sort_keys=True, indent=4) - - @staticmethod - def convert_to_structure(s): - root = ElementTree.fromstring(s) - return Xml2Json.convert_element(root) - - @staticmethod - def convert_element(el, skip_whitespaces=True): - res = {} - if el.tag[0] == "{": - ns, name = el.tag.rsplit("}", 1) - res["tag"] = name - res["namespace"] = ns[1:] - else: - res["tag"] = el.tag - res["attrs"] = {} - for k, v in el.items(): - res["attrs"][k] = v - kids = [] - if el.text and (not skip_whitespaces or el.text.strip() != ''): - kids.append(el.text) - for kid in el: - kids.append(Xml2Json.convert_element(kid)) - if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''): - kids.append(kid.tail) - res["children"] = kids - return res +import babel.messages.pofile + +import web.common +openerpweb = web.common.http #---------------------------------------------------------- # OpenERP Web web Controllers #---------------------------------------------------------- -def manifest_glob(addons_path, addons, key): - files = [] - for addon in addons: - globlist = openerpweb.addons_manifest.get(addon, {}).get(key, []) - for pattern in globlist: - for path in glob.glob(os.path.join(addons_path, addon, pattern)): - files.append(path[len(addons_path):]) - return files -def concat_files(addons_path, file_list): - """ Concatenate file content +def concat_xml(file_list): + """Concatenate xml files return (concat,timestamp) concat: concatenation of file content timestamp: max(os.path.getmtime of file_list) """ + root = None + files_timestamp = 0 + for fname in file_list: + ftime = os.path.getmtime(fname) + if ftime > files_timestamp: + files_timestamp = ftime + + xml = ElementTree.parse(fname).getroot() + + if root is None: + root = ElementTree.Element(xml.tag) + #elif root.tag != xml.tag: + # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag)) + + for child in xml.getchildren(): + root.append(child) + return ElementTree.tostring(root, 'utf-8'), files_timestamp + + +def concat_files(file_list, reader=None): + """ Concatenate file content + return (concat,timestamp) + concat: concatenation of file content, read by `reader` + timestamp: max(os.path.getmtime of file_list) + """ + if reader is None: + def reader(f): + with open(f) as fp: + return fp.read() + files_content = [] files_timestamp = 0 - for i in file_list: - fname = os.path.join(addons_path, i[1:]) + for fname in file_list: ftime = os.path.getmtime(fname) if ftime > files_timestamp: files_timestamp = ftime - files_content.append(open(fname).read()) + + files_content.append(reader(fname)) files_concat = "".join(files_content) return files_concat,files_timestamp -home_template = textwrap.dedent(""" +html_template = """ OpenERP %(css)s - %(javascript)s + %(js)s -""") +""" + class WebClient(openerpweb.Controller): _cp_path = "/web/webclient" + def server_wide_modules(self, req): + addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest] + return addons + + def manifest_glob(self, req, addons, key): + if addons is None: + addons = self.server_wide_modules(req) + else: + addons = addons.split(',') + r = [] + for addon in addons: + manifest = openerpweb.addons_manifest.get(addon, None) + if not manifest: + continue + # ensure does not ends with / + addons_path = os.path.join(manifest['addons_path'], '')[:-1] + globlist = manifest.get(key, []) + for pattern in globlist: + for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))): + r.append( (path, path[len(addons_path):])) + return r + + def manifest_list(self, req, mods, extension): + if not req.debug: + path = '/web/webclient/' + extension + if mods is not None: + path += '?mods=' + mods + return [path] + return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in self.manifest_glob(req, mods, extension)] + + @openerpweb.jsonrequest + def csslist(self, req, mods=None): + return self.manifest_list(req, mods, 'css') + @openerpweb.jsonrequest - def csslist(self, req, mods='web'): - return manifest_glob(req.config.addons_path, mods.split(','), 'css') + def jslist(self, req, mods=None): + return self.manifest_list(req, mods, 'js') @openerpweb.jsonrequest - def jslist(self, req, mods='web'): - return manifest_glob(req.config.addons_path, mods.split(','), 'js') + def qweblist(self, req, mods=None): + return self.manifest_list(req, mods, 'qweb') @openerpweb.httprequest - def css(self, req, mods='web'): - files = manifest_glob(req.config.addons_path, mods.split(','), 'css') - content,timestamp = concat_files(req.config.addons_path, files) - # TODO request set the Date of last modif and Etag + def css(self, req, mods=None): + + files = list(self.manifest_glob(req, mods, 'css')) + file_map = dict(files) + + rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U) + rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://)""", re.U) + + + def reader(f): + """read the a css file and absolutify all relative uris""" + with open(f) as fp: + data = fp.read() + + web_path = file_map[f] + web_dir = os.path.dirname(web_path) + + data = re.sub( + rx_import, + r"""@import \1%s/""" % (web_dir,), + data, + ) + + data = re.sub( + rx_url, + r"""url(\1%s/""" % (web_dir,), + data, + ) + return data + + content,timestamp = concat_files((f[0] for f in files), reader) + # TODO use timestamp to set Last mofified date and E-tag return req.make_response(content, [('Content-Type', 'text/css')]) @openerpweb.httprequest - def js(self, req, mods='web'): - files = manifest_glob(req.config.addons_path, mods.split(','), 'js') - content,timestamp = concat_files(req.config.addons_path, files) - # TODO request set the Date of last modif and Etag + def js(self, req, mods=None): + files = [f[0] for f in self.manifest_glob(req, mods, 'js')] + content,timestamp = concat_files(files) + # TODO use timestamp to set Last mofified date and E-tag return req.make_response(content, [('Content-Type', 'application/javascript')]) @openerpweb.httprequest + def qweb(self, req, mods=None): + files = [f[0] for f in self.manifest_glob(req, mods, 'qweb')] + content,timestamp = concat_xml(files) + # TODO use timestamp to set Last mofified date and E-tag + return req.make_response(content, [('Content-Type', 'text/xml')]) + + + @openerpweb.httprequest def home(self, req, s_action=None, **kw): - # script tags - jslist = ['/web/webclient/js'] - if req.debug: - jslist = [i + '?debug=' + str(time.time()) for i in manifest_glob(req.config.addons_path, ['web'], 'js')] - js = "\n ".join([''%i for i in jslist]) - - # css tags - csslist = ['/web/webclient/css'] - if req.debug: - csslist = [i + '?debug=' + str(time.time()) for i in manifest_glob(req.config.addons_path, ['web'], 'css')] - css = "\n ".join([''%i for i in csslist]) - r = home_template % { - 'javascript': js, - 'css': css + js = "\n ".join(''%i for i in self.manifest_list(req, None, 'js')) + css = "\n ".join(''%i for i in self.manifest_list(req, None, 'css')) + + r = html_template % { + 'js': js, + 'css': css, + 'modules': simplejson.dumps(self.server_wide_modules(req)), + 'init': 'new s.web.WebClient("oe").start();', } return r @@ -178,12 +221,13 @@ class WebClient(openerpweb.Controller): transl = {"messages":[]} transs[addon_name] = transl for l in langs: - f_name = os.path.join(req.config.addons_path, addon_name, "po", l + ".po") + addons_path = openerpweb.addons_manifest[addon_name]['addons_path'] + f_name = os.path.join(addons_path, addon_name, "po", l + ".po") if not os.path.exists(f_name): continue try: with open(f_name) as t_file: - po = read_po(t_file) + po = babel.messages.pofile.read_po(t_file) except: continue for x in po: @@ -195,7 +239,7 @@ class WebClient(openerpweb.Controller): @openerpweb.jsonrequest def version_info(self, req): return { - "version": webrelease.version + "version": web.common.release.version } class Database(openerpweb.Controller): @@ -226,13 +270,18 @@ class Database(openerpweb.Controller): params['db_lang'], params['create_admin_pwd'] ) - + try: return req.session.proxy("db").create(*create_attrs) except xmlrpclib.Fault, e: - if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied': - return {'error': e.faultCode, 'title': 'Create Database'} - return {'error': 'Could not create database !', 'title': 'Create Database'} + if e.faultCode and isinstance(e.faultCode, str)\ + and e.faultCode.split(':')[0] == 'AccessDenied': + return {'error': e.faultCode, 'title': 'Database creation error'} + return { + 'error': "Could not create database '%s': %s" % ( + params['db_name'], e.faultString), + 'title': 'Database creation error' + } @openerpweb.jsonrequest def drop(self, req, fields): @@ -265,7 +314,7 @@ class Database(openerpweb.Controller): @openerpweb.httprequest def restore(self, req, db_file, restore_pwd, new_db): try: - data = base64.b64encode(db_file.file.read()) + data = base64.b64encode(db_file.read()) req.session.proxy("db").restore(restore_pwd, new_db, data) return '' except xmlrpclib.Fault, e: @@ -290,13 +339,26 @@ class Session(openerpweb.Controller): @openerpweb.jsonrequest def login(self, req, db, login, password): req.session.login(db, login, password) - ctx = req.session.get_context() + ctx = req.session.get_context() if req.session._uid else {} return { "session_id": req.session_id, "uid": req.session._uid, - "context": ctx + "context": ctx, + "db": req.session._db, + "login": req.session._login } + + @openerpweb.jsonrequest + def get_session_info(self, req): + req.session.assert_valid(force=True) + return { + "uid": req.session._uid, + "context": req.session.get_context() if req.session._uid else False, + "db": req.session._db, + "login": req.session._login + } + @openerpweb.jsonrequest def change_password (self,req,fields): old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')( @@ -312,6 +374,7 @@ class Session(openerpweb.Controller): except: return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'} return {'error': 'Error, password not changed !', 'title': 'Change Password'} + @openerpweb.jsonrequest def sc_list(self, req): return req.session.model('ir.ui.view_sc').get_sc( @@ -329,12 +392,22 @@ class Session(openerpweb.Controller): @openerpweb.jsonrequest def modules(self, req): - # TODO query server for installed web modules - mods = [] - for name, manifest in openerpweb.addons_manifest.items(): - if name != 'web' and manifest.get('active', True): - mods.append(name) - return mods + # Compute available candidates module + loadable = openerpweb.addons_manifest.iterkeys() + loaded = req.config.server_wide_modules + candidates = [mod for mod in loadable if mod not in loaded] + + # Compute active true modules that might be on the web side only + active = set(name for name in candidates + if openerpweb.addons_manifest[name].get('active')) + + # Retrieve database installed modules + Modules = req.session.model('ir.module.module') + installed = set(module['name'] for module in Modules.search_read( + [('state','=','installed'), ('name','in', candidates)], ['name'])) + + # Merge both + return list(active | installed) @openerpweb.jsonrequest def eval_domain_and_context(self, req, contexts, domains, @@ -369,8 +442,8 @@ class Session(openerpweb.Controller): no group by should be performed) """ context, domain = eval_context_and_domain(req.session, - openerpweb.nonliterals.CompoundContext(*(contexts or [])), - openerpweb.nonliterals.CompoundDomain(*(domains or []))) + web.common.nonliterals.CompoundContext(*(contexts or [])), + web.common.nonliterals.CompoundDomain(*(domains or []))) group_by_sequence = [] for candidate in (group_by_seq or []): @@ -449,18 +522,27 @@ def load_actions_from_ir_values(req, key, key2, models, meta): return [(id, name, clean_action(req, action)) for id, name, action in actions] -def clean_action(req, action): +def clean_action(req, action, do_not_eval=False): action.setdefault('flags', {}) context = req.session.eval_context(req.context) eval_ctx = req.session.evaluation_context(context) - # values come from the server, we can just eval them - if isinstance(action.get('context'), basestring): - action['context'] = eval( action['context'], eval_ctx ) or {} + if not do_not_eval: + # values come from the server, we can just eval them + if isinstance(action.get('context'), basestring): + action['context'] = eval( action['context'], eval_ctx ) or {} - if isinstance(action.get('domain'), basestring): - action['domain'] = eval( action['domain'], eval_ctx ) or [] + if isinstance(action.get('domain'), basestring): + action['domain'] = eval( action['domain'], eval_ctx ) or [] + else: + if 'context' in action: + action['context'] = parse_context(action['context'], req.session) + if 'domain' in action: + action['domain'] = parse_domain(action['domain'], req.session) + + if 'type' not in action: + action['type'] = 'ir.actions.act_window_close' if action['type'] == 'ir.actions.act_window': return fix_view_modes(action) @@ -522,10 +604,18 @@ def fix_view_modes(action): :param dict action: an action descriptor :returns: nothing, the action is modified in place """ - if 'views' not in action: + if not action.get('views'): generate_views(action) - if action.pop('view_type') != 'form': + id_form = None + for index, (id, mode) in enumerate(action['views']): + if mode == 'form': + id_form = id + break + if id_form is not None: + action['views'].insert(index + 1, (id_form, 'page')) + + if action.pop('view_type', 'form') != 'form': return action action['views'] = [ @@ -698,12 +788,15 @@ class DataSet(openerpweb.Controller): return Model.unlink(ids, req.session.eval_context(req.context)) def call_common(self, req, model, method, args, domain_id=None, context_id=None): - domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else [] - context = args[context_id] if context_id and len(args) - 1 >= context_id else {} + has_domain = domain_id is not None and domain_id < len(args) + has_context = context_id is not None and context_id < len(args) + + domain = args[domain_id] if has_domain else [] + context = args[context_id] if has_context else {} c, d = eval_context_and_domain(req.session, context, domain) - if domain_id and len(args) - 1 >= domain_id: + if has_domain: args[domain_id] = d - if context_id and len(args) - 1 >= context_id: + if has_context: args[context_id] = c for i in xrange(len(args)): @@ -761,12 +854,12 @@ class View(openerpweb.Controller): context = req.session.eval_context(req.context) fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu) # todo fme?: check that we should pass the evaluated context here - self.process_view(req.session, fvg, context, transform) + self.process_view(req.session, fvg, context, transform, (view_type == 'kanban')) if toolbar and transform: self.process_toolbar(req, fvg['toolbar']) return fvg - def process_view(self, session, fvg, context, transform): + def process_view(self, session, fvg, context, transform, preserve_whitespaces=False): # depending on how it feels, xmlrpclib.ServerProxy can translate # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not # enjoy unicode strings which can not be trivially converted to @@ -784,16 +877,16 @@ class View(openerpweb.Controller): xml = self.transform_view(arch, session, evaluation_context) else: xml = ElementTree.fromstring(arch) - fvg['arch'] = Xml2Json.convert_element(xml) + fvg['arch'] = web.common.xml2json.Xml2Json.convert_element(xml, preserve_whitespaces) for field in fvg['fields'].itervalues(): if field.get('views'): for view in field["views"].itervalues(): self.process_view(session, view, None, transform) if field.get('domain'): - field["domain"] = self.parse_domain(field["domain"], session) + field["domain"] = parse_domain(field["domain"], session) if field.get('context'): - field["context"] = self.parse_context(field["context"], session) + field["context"] = parse_context(field["context"], session) def process_toolbar(self, req, toolbar): """ @@ -805,10 +898,10 @@ class View(openerpweb.Controller): for actions in toolbar.itervalues(): for action in actions: if 'context' in action: - action['context'] = self.parse_context( + action['context'] = parse_context( action['context'], req.session) if 'domain' in action: - action['domain'] = self.parse_domain( + action['domain'] = parse_domain( action['domain'], req.session) @openerpweb.jsonrequest @@ -847,39 +940,6 @@ class View(openerpweb.Controller): self.parse_domains_and_contexts(elem, session) return root - def parse_domain(self, domain, session): - """ Parses an arbitrary string containing a domain, transforms it - to either a literal domain or a :class:`openerpweb.nonliterals.Domain` - - :param domain: the domain to parse, if the domain is not a string it - is assumed to be a literal domain and is returned as-is - :param session: Current OpenERP session - :type session: openerpweb.openerpweb.OpenERPSession - """ - if not isinstance(domain, (str, unicode)): - return domain - try: - return openerpweb.ast.literal_eval(domain) - except ValueError: - # not a literal - return openerpweb.nonliterals.Domain(session, domain) - - def parse_context(self, context, session): - """ Parses an arbitrary string containing a context, transforms it - to either a literal context or a :class:`openerpweb.nonliterals.Context` - - :param context: the context to parse, if the context is not a string it - is assumed to be a literal domain and is returned as-is - :param session: Current OpenERP session - :type session: openerpweb.openerpweb.OpenERPSession - """ - if not isinstance(context, (str, unicode)): - return context - try: - return openerpweb.ast.literal_eval(context) - except ValueError: - return openerpweb.nonliterals.Context(session, context) - def parse_domains_and_contexts(self, elem, session): """ Converts domains and contexts from the view into Python objects, either literals if they can be parsed by literal_eval or a special @@ -894,16 +954,51 @@ class View(openerpweb.Controller): for el in ['domain', 'filter_domain']: domain = elem.get(el, '').strip() if domain: - elem.set(el, self.parse_domain(domain, session)) + elem.set(el, parse_domain(domain, session)) + elem.set(el + '_string', domain) for el in ['context', 'default_get']: context_string = elem.get(el, '').strip() if context_string: - elem.set(el, self.parse_context(context_string, session)) + elem.set(el, parse_context(context_string, session)) + elem.set(el + '_string', context_string) @openerpweb.jsonrequest def load(self, req, model, view_id, view_type, toolbar=False): return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar) +def parse_domain(domain, session): + """ Parses an arbitrary string containing a domain, transforms it + to either a literal domain or a :class:`web.common.nonliterals.Domain` + + :param domain: the domain to parse, if the domain is not a string it + is assumed to be a literal domain and is returned as-is + :param session: Current OpenERP session + :type session: openerpweb.openerpweb.OpenERPSession + """ + if not isinstance(domain, (str, unicode)): + return domain + try: + return ast.literal_eval(domain) + except ValueError: + # not a literal + return web.common.nonliterals.Domain(session, domain) + +def parse_context(context, session): + """ Parses an arbitrary string containing a context, transforms it + to either a literal context or a :class:`web.common.nonliterals.Context` + + :param context: the context to parse, if the context is not a string it + is assumed to be a literal domain and is returned as-is + :param session: Current OpenERP session + :type session: openerpweb.openerpweb.OpenERPSession + """ + if not isinstance(context, (str, unicode)): + return context + try: + return ast.literal_eval(context) + except ValueError: + return web.common.nonliterals.Context(session, context) + class ListView(View): _cp_path = "/web/listview" @@ -949,9 +1044,9 @@ class SearchView(View): for field in fields.values(): # shouldn't convert the views too? if field.get('domain'): - field["domain"] = self.parse_domain(field["domain"], req.session) + field["domain"] = parse_domain(field["domain"], req.session) if field.get('context'): - field["context"] = self.parse_domain(field["context"], req.session) + field["context"] = parse_context(field["context"], req.session) return {'fields': fields} @openerpweb.jsonrequest @@ -959,17 +1054,17 @@ class SearchView(View): Model = req.session.model("ir.filters") filters = Model.get_filters(model) for filter in filters: - filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session)) - filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session)) + filter["context"] = req.session.eval_context(parse_context(filter["context"], req.session)) + filter["domain"] = req.session.eval_domain(parse_domain(filter["domain"], req.session)) return filters @openerpweb.jsonrequest def save_filter(self, req, model, name, context_to_save, domain): Model = req.session.model("ir.filters") - ctx = openerpweb.nonliterals.CompoundContext(context_to_save) + ctx = web.common.nonliterals.CompoundContext(context_to_save) ctx.session = req.session ctx = ctx.evaluate() - domain = openerpweb.nonliterals.CompoundDomain(domain) + domain = web.common.nonliterals.CompoundDomain(domain) domain.session = req.session domain = domain.evaluate() uid = req.session._uid @@ -982,6 +1077,44 @@ class SearchView(View): }, context) return to_return + @openerpweb.jsonrequest + def add_to_dashboard(self, req, menu_id, action_id, context_to_save, domain, view_mode, name=''): + ctx = web.common.nonliterals.CompoundContext(context_to_save) + ctx.session = req.session + ctx = ctx.evaluate() + domain = web.common.nonliterals.CompoundDomain(domain) + domain.session = req.session + domain = domain.evaluate() + + dashboard_action = load_actions_from_ir_values(req, 'action', 'tree_but_open', + [('ir.ui.menu', menu_id)], False) + if dashboard_action: + action = dashboard_action[0][2] + if action['res_model'] == 'board.board' and action['views'][0][1] == 'form': + # Maybe should check the content instead of model board.board ? + view_id = action['views'][0][0] + board = req.session.model(action['res_model']).fields_view_get(view_id, 'form') + if board and 'arch' in board: + xml = ElementTree.fromstring(board['arch']) + column = xml.find('./board/column') + if column: + new_action = ElementTree.Element('action', { + 'name' : str(action_id), + 'string' : name, + 'view_mode' : view_mode, + 'context' : str(ctx), + 'domain' : str(domain) + }) + column.insert(0, new_action) + arch = ElementTree.tostring(xml, 'utf-8') + return req.session.model('ir.ui.view.custom').create({ + 'user_id': req.session._uid, + 'ref_id': view_id, + 'arch': arch + }, req.session.eval_context(req.context)) + + return False + class Binary(openerpweb.Controller): _cp_path = "/web/binary" @@ -992,23 +1125,27 @@ class Binary(openerpweb.Controller): try: if not id: - res = Model.default_get([field], context).get(field, '') + res = Model.default_get([field], context).get(field) else: - res = Model.read([int(id)], [field], context)[0].get(field, '') + res = Model.read([int(id)], [field], context)[0].get(field) image_data = base64.b64decode(res) except (TypeError, xmlrpclib.Fault): image_data = self.placeholder(req) return req.make_response(image_data, [ ('Content-Type', 'image/png'), ('Content-Length', len(image_data))]) def placeholder(self, req): - return open(os.path.join(req.addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read() + addons_path = openerpweb.addons_manifest['web']['addons_path'] + return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read() @openerpweb.httprequest def saveas(self, req, model, id, field, fieldname, **kw): Model = req.session.model(model) context = req.session.eval_context(req.context) - res = Model.read([int(id)], [field, fieldname], context)[0] - filecontent = res.get(field, '') + if id: + res = Model.read([int(id)], [field, fieldname], context)[0] + else: + res = Model.default_get([field, fieldname], context) + filecontent = base64.b64decode(res.get(field, '')) if not filecontent: return req.not_found() else: @@ -1072,7 +1209,7 @@ class Action(openerpweb.Controller): _cp_path = "/web/action" @openerpweb.jsonrequest - def load(self, req, action_id): + def load(self, req, action_id, do_not_eval=False): Actions = req.session.model('ir.actions.actions') value = False context = req.session.eval_context(req.context) @@ -1084,7 +1221,7 @@ class Action(openerpweb.Controller): ctx.update(context) action = req.session.model(action_type[0]['type']).read([action_id], False, ctx) if action: - value = clean_action(req, action[0]) + value = clean_action(req, action[0], do_not_eval) return {'result': value} @openerpweb.jsonrequest @@ -1116,19 +1253,26 @@ class Export(View): @openerpweb.jsonrequest def get_fields(self, req, model, prefix='', parent_name= '', - import_compat=True, parent_field_type=None): + import_compat=True, parent_field_type=None, + exclude=None): if import_compat and parent_field_type == "many2one": fields = {} else: fields = self.fields_get(req, model) - fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'} + + if import_compat: + fields.pop('id', None) + else: + fields['.id'] = fields.pop('id', {'string': 'ID'}) fields_sequence = sorted(fields.iteritems(), key=lambda field: field[1].get('string', '')) records = [] for field_name, field in fields_sequence: + if import_compat and (exclude and field_name in exclude): + continue if import_compat and field.get('readonly'): # If none of the field's states unsets readonly, skip the field if all(dict(attrs).get('readonly', True) @@ -1140,7 +1284,8 @@ class Export(View): record = {'id': id, 'string': name, 'value': id, 'children': False, 'field_type': field.get('type'), - 'required': field.get('required')} + 'required': field.get('required'), + 'relation_field': field.get('relation_field')} records.append(record) if len(name.split('/')) < 3 and 'relation' in field: @@ -1172,7 +1317,6 @@ class Export(View): def fields_info(self, req, model, export_fields): info = {} fields = self.fields_get(req, model) - fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'} # To make fields retrieval more efficient, fetch all sub-fields of a # given field at the same time. Because the order in the export list is @@ -1255,7 +1399,7 @@ class Export(View): context = req.session.eval_context(req.context) Model = req.session.model(model) - ids = ids or Model.search(domain, context=context) + ids = ids or Model.search(domain, 0, False, False, context) field_names = map(operator.itemgetter('name'), fields) import_data = Model.export_data(ids, field_names, context).get('datas',[]) @@ -1333,6 +1477,7 @@ class ExcelExport(Export): for cell_index, cell_value in enumerate(row): if isinstance(cell_value, basestring): cell_value = re.sub("\r", " ", cell_value) + if cell_value is False: cell_value = None worksheet.write(row_index + 1, cell_index, cell_value, style) fp = StringIO() @@ -1360,15 +1505,21 @@ class Reports(View): report_srv = req.session.proxy("report") context = req.session.eval_context( - openerpweb.nonliterals.CompoundContext( + web.common.nonliterals.CompoundContext( req.context or {}, action[ "context"])) - report_data = {"id": context["active_id"], "model": context["active_model"]} + report_data = {} + report_ids = context["active_ids"] if 'report_type' in action: report_data['report_type'] = action['report_type'] + if 'datas' in action: + if 'ids' in action['datas']: + report_ids = action['datas'].pop('ids') + report_data.update(action['datas']) + report_id = report_srv.report( req.session._db, req.session._uid, req.session._password, - action["report_name"], context["active_ids"], + action["report_name"], report_ids, report_data, context) report_struct = None @@ -1392,7 +1543,6 @@ class Reports(View): ('Content-Length', len(report))], cookies={'fileToken': int(token)}) - class Import(View): _cp_path = "/web/import" @@ -1402,181 +1552,97 @@ class Import(View): return fields @openerpweb.httprequest - def detect_data(self, req, model, csvfile, csvsep, csvdel, csvcode, csvskip, - jsonp): - - _fields = {} - _fields_invert = {} - req_field = [] - error = None - fields = req.session.model(model).fields_get(False, req.session.eval_context(req.context)) - fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}}) - - for field in fields: - value = fields[field] - if value.get('required'): - req_field.append(field) - - def model_populate(fields, prefix_node='', prefix=None, prefix_value='', level=2): - def str_comp(x,y): - if xy: return -1 - else: return 0 - - fields_order = fields.keys() - fields_order.sort(lambda x,y: str_comp(fields[x].get('string', ''), fields[y].get('string', ''))) - for field in fields_order: - if (fields[field].get('type','') not in ('reference',))\ - and (not fields[field].get('readonly')\ - or not dict(fields[field].get('states', {}).get( - 'draft', [('readonly', True)])).get('readonly',True)): - - st_name = prefix_value+fields[field]['string'] or field - _fields[prefix_node+field] = st_name - _fields_invert[st_name] = prefix_node+field - - if fields[field].get('type')=='one2many' and level>0: - fields2 = self.fields_get(req, fields[field]['relation']) - model_populate(fields2, prefix_node+field+'/', None, st_name+'/', level-1) - - if fields[field].get('relation',False) and level>0: - model_populate({'/id': {'type': 'char', 'string': 'ID'}, '.id': {'type': 'char', 'string': 'Database ID'}}, - prefix_node+field, None, st_name+'/', level-1) - fields.update({'id':{'string':'ID'},'.id':{'string':'Database ID'}}) - model_populate(fields) - all_fields = fields.keys() - all_fields.sort() - + def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'): try: - data = csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)) - except: - error={'message': 'error opening .CSV file. Input Error.'} + data = list(csv.reader( + csvfile, quotechar=str(csvdel), delimiter=str(csvsep))) + except csv.Error, e: + csvfile.seek(0) return '' % ( - jsonp, simplejson.dumps({'error':error})) - - records = [] - count = 0 - header_fields = [] - word='' + jsonp, simplejson.dumps({'error': { + 'message': 'Error parsing CSV file: %s' % e, + # decodes each byte to a unicode character, which may or + # may not be printable, but decoding will succeed. + # Otherwise simplejson will try to decode the `str` using + # utf-8, which is very likely to blow up on characters out + # of the ascii range (in range [128, 256)) + 'preview': csvfile.read(200).decode('iso-8859-1')}})) try: - for rec in itertools.islice(data,0,4): - records.append(rec) - - headers = itertools.islice(records,1) - line = headers.next() - - for word in line: - word = str(word.decode(csvcode)) - if word in _fields: - header_fields.append((word, _fields[word])) - elif word in _fields_invert.keys(): - header_fields.append((_fields_invert[word], word)) - else: - count = count + 1 - header_fields.append((word, word)) - - if len(line) == count: - error = {'message':"File has not any column header."} - except: - error = {'message':('Error processing the first line of the file. Field "%s" is unknown') % (word,)} - - if error: - csvfile.seek(0) - error=dict(error, preview=csvfile.read(200)) return '' % ( - jsonp, simplejson.dumps({'error':error})) - - return '' % ( - jsonp, simplejson.dumps({'records':records[1:],'header':header_fields,'all_fields':all_fields,'req_field':req_field})) + jsonp, simplejson.dumps( + {'records': data[:10]}, encoding=csvcode)) + except UnicodeDecodeError: + return '' % ( + jsonp, simplejson.dumps({ + 'message': u"Failed to decode CSV file using encoding %s, " + u"try switching to a different encoding" % csvcode + })) @openerpweb.httprequest - def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, csvskip, - jsonp): - - _fields = {} - _fields_invert = {} - prefix_node='' - prefix_value = '' - - context = req.session.eval_context(req.context) + def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp, + meta): modle_obj = req.session.model(model) - res = None - - limit = 0 - data = [] + skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')( + simplejson.loads(meta)) + error = None if not (csvdel and len(csvdel) == 1): - error={'message': "The CSV delimiter must be a single character"} - return '' % ( - jsonp, simplejson.dumps({'error':error})) + error = u"The CSV delimiter must be a single character" - try: - data_record = csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)) - for rec in itertools.islice(data_record,0,None): - data.append(rec) - - headers = itertools.islice(data,1) - fields = headers.next() + if not indices and fields: + error = u"You must select at least one field to import" - except csv.Error, e: - error={'message': str(e),'title': 'File Format Error'} + if error: return '' % ( - jsonp, simplejson.dumps({'error':error})) - - datas = [] - ctx = context + jsonp, simplejson.dumps({'error': {'message': error}})) - if not isinstance(fields, list): - fields = [fields] + # skip ignored records + data_record = itertools.islice( + csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)), + skip, None) - flds = modle_obj.fields_get(False, req.session.eval_context(req.context)) - flds.update({'id':{'string':'ID'},'.id':{'string':'Database ID'}}) - fields_order = flds.keys() - for field in fields_order: - st_name = prefix_value+flds[field]['string'] or field - _fields[prefix_node+field] = st_name - _fields_invert[st_name] = prefix_node+field + # if only one index, itemgetter will return an atom rather than a tuple + if len(indices) == 1: mapper = lambda row: [row[indices[0]]] + else: mapper = operator.itemgetter(*indices) - unmatch_field = [] - for fld in fields: - if ((fld not in _fields) and (fld not in _fields_invert)): - unmatch_field.append(fld) - - if unmatch_field: - error = {'message':("You cannot import the fields '%s',because we cannot auto-detect it." % (unmatch_field))} - return '' % ( - jsonp, simplejson.dumps({'error':error})) - - for line in data[1:]: - try: - datas.append(map(lambda x:x.decode(csvcode).encode('utf-8'), line)) - except: - datas.append(map(lambda x:x.decode('latin').encode('utf-8'), line)) + data = None + error = None + try: + # decode each data row + data = [ + [record.decode(csvcode) for record in row] + for row in itertools.imap(mapper, data_record) + # don't insert completely empty rows (can happen due to fields + # filtering in case of e.g. o2m content rows) + if any(row) + ] + except UnicodeDecodeError: + error = u"Failed to decode CSV file using encoding %s" % csvcode + except csv.Error, e: + error = u"Could not process CSV file: %s" % e # If the file contains nothing, - if not datas: - error = {'message': 'The file is empty !', 'title': 'Importation !'} + if not data: + error = u"File to import is empty" + if error: return '' % ( - jsonp, simplejson.dumps({'error':error})) + jsonp, simplejson.dumps({'error': {'message': error}})) - #Inverting the header into column names try: - res = modle_obj.import_data(fields, datas, 'init', '', False, ctx) + (code, record, message, _nope) = modle_obj.import_data( + fields, data, 'init', '', False, + req.session.eval_context(req.context)) except xmlrpclib.Fault, e: - error = {"message":e.faultCode} + error = {"message": u"%s, %s" % (e.faultCode, e.faultString)} return '' % ( jsonp, simplejson.dumps({'error':error})) - if res[0]>=0: - success={'message':'Imported %d objects' % (res[0],)} + if code != -1: return '' % ( - jsonp, simplejson.dumps({'error':success})) + jsonp, simplejson.dumps({'success':True})) - d = '' - for key,val in res[1].items(): - d+= ('%s: %s' % (str(key),str(val))) - msg = 'Error trying to import this record:%s. ErrorMessage:%s %s' % (d,res[2],res[3]) - error = {'message':str(msg), 'title':'ImportationError'} + msg = u"Error during import: %s\n\nTrying to import record %r" % ( + message, record) return '' % ( - jsonp, simplejson.dumps({'error':error})) + jsonp, simplejson.dumps({'error': {'message':msg}}))