diff --git a/addons/web/__init__.py b/addons/web/__init__.py index 5a0289cef6d..71b16f2ea19 100644 --- a/addons/web/__init__.py +++ b/addons/web/__init__.py @@ -6,6 +6,3 @@ sys.modules['openerp.addons.web.http'] = openerp.http http = openerp.http import controllers -import cli - -wsgi_postload = http.wsgi_postload diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py index 72f1610177c..5941a6aa5b0 100644 --- a/addons/web/__openerp__.py +++ b/addons/web/__openerp__.py @@ -11,7 +11,6 @@ This module provides the core of the OpenERP Web Client. """, 'depends': ['base'], 'auto_install': True, - 'post_load': 'wsgi_postload', 'data': [ 'views/webclient_templates.xml', ], @@ -36,7 +35,6 @@ This module provides the core of the OpenERP Web Client. "static/lib/jquery.ui.notify/js/jquery.notify.js", "static/lib/jquery.deferred-queue/jquery.deferred-queue.js", "static/lib/jquery.scrollTo/jquery.scrollTo-min.js", - "static/lib/jquery.tipsy/jquery.tipsy.js", "static/lib/jquery.textext/jquery.textext.js", "static/lib/jquery.timeago/jquery.timeago.js", "static/lib/bootstrap/js/bootstrap.js", @@ -67,7 +65,6 @@ This module provides the core of the OpenERP Web Client. "static/lib/jquery.ui.bootstrap/css/custom-theme/jquery-ui-1.9.0.custom.css", "static/lib/jquery.ui.timepicker/css/jquery-ui-timepicker-addon.css", "static/lib/jquery.ui.notify/css/ui.notify.css", - "static/lib/jquery.tipsy/tipsy.css", "static/lib/jquery.textext/jquery.textext.css", "static/lib/fontawesome/css/font-awesome.css", "static/lib/bootstrap/css/bootstrap.css", @@ -87,7 +84,6 @@ This module provides the core of the OpenERP Web Client. "static/test/data.js", "static/test/list-utils.js", "static/test/formats.js", - "static/test/jsonrpc.js", "static/test/rpc-misordered.js", "static/test/evals.js", "static/test/search.js", diff --git a/addons/web/cli/__init__.py b/addons/web/cli/__init__.py deleted file mode 100644 index 823c140bb79..00000000000 --- a/addons/web/cli/__init__.py +++ /dev/null @@ -1 +0,0 @@ -import test_js diff --git a/addons/web/cli/test_js.py b/addons/web/cli/test_js.py deleted file mode 100644 index b3a8e634b72..00000000000 --- a/addons/web/cli/test_js.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging -import optparse -import sys - -import unittest2 - -import openerp -import openerp.addons.web.tests - -_logger = logging.getLogger(__name__) - -class TestJs(openerp.cli.Command): - def run(self, args): - self.parser = parser = optparse.OptionParser() - parser.add_option("-d", "--database", dest="db_name", default=False, help="specify the database name") - parser.add_option("--xmlrpc-port", dest="xmlrpc_port", default=8069, help="specify the TCP port for the XML-RPC protocol", type="int") - # proably need to add both --superadmin-password and --database-admin-password - self.parser.parse_args(args) - - # test ony uses db_name xmlrpc_port admin_passwd, so use the server one for the actual parsing - - config = openerp.tools.config - config.parse_config(args) - # needed until runbot is fixed - config['db_password'] = config['admin_passwd'] - - # run js tests - openerp.netsvc.init_alternative_logger() - suite = unittest2.TestSuite() - suite.addTests(unittest2.TestLoader().loadTestsFromModule(openerp.addons.web.tests.test_js)) - r = unittest2.TextTestRunner(verbosity=2).run(suite) - if r.errors or r.failures: - sys.exit(1) - -# vim:et:ts=4:sw=4: diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 94357c0131b..a6b5fa63918 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -14,6 +14,7 @@ import hashlib import os import re import simplejson +import sys import time import urllib2 import zlib @@ -33,14 +34,18 @@ import openerp.modules.registry from openerp.tools.translate import _ from openerp import http -from openerp.http import request, serialize_exception as _serialize_exception, LazyResponse +from openerp.http import request, serialize_exception as _serialize_exception _logger = logging.getLogger(__name__) -env = jinja2.Environment( - loader=jinja2.PackageLoader('openerp.addons.web', "views"), - autoescape=True -) +if hasattr(sys, 'frozen'): + # When running on compiled windows binary, we don't have access to package loader. + path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'views')) + loader = jinja2.FileSystemLoader(path) +else: + loader = jinja2.PackageLoader('openerp.addons.web', "views") + +env = jinja2.Environment(loader=loader, autoescape=True) env.filters["json"] = simplejson.dumps #---------------------------------------------------------- @@ -119,6 +124,12 @@ def redirect_with_hash(*args, **kw): """ return http.redirect_with_hash(*args, **kw) +def abort_and_redirect(url): + r = request.httprequest + response = werkzeug.utils.redirect(url, 302) + response = r.app.get_response(r, response, explicit_session=False) + werkzeug.exceptions.abort(response) + def ensure_db(redirect='/web/database/selector'): # This helper should be used in web client auth="none" routes # if those routes needs a db to work with. @@ -141,11 +152,14 @@ def ensure_db(redirect='/web/database/selector'): # Thus, we redirect the user to the same page but with the session cookie set. # This will force using the database route dispatcher... r = request.httprequest - response = werkzeug.utils.redirect(r.url, 302) + url_redirect = r.base_url + if r.query_string: + # Can't use werkzeug.wrappers.BaseRequest.url with encoded hashes: + # https://github.com/amigrave/werkzeug/commit/b4a62433f2f7678c234cdcac6247a869f90a7eb7 + url_redirect += '?' + r.query_string + response = werkzeug.utils.redirect(url_redirect, 302) request.session.db = db - response = r.app.get_response(r, response, explicit_session=False) - werkzeug.exceptions.abort(response) - return + abort_and_redirect(url_redirect) # if db not provided, use the session one if not db: @@ -163,6 +177,7 @@ def ensure_db(redirect='/web/database/selector'): # always switch the session to the computed db if db != request.session.db: request.session.logout() + abort_and_redirect(request.httprequest.url) request.session.db = db @@ -356,7 +371,13 @@ def manifest_glob(extension, addons=None, db=None, include_remotes=False): r.append((None, pattern)) else: for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))): - r.append((path, fs2web(path[len(addons_path):]))) + # Hack for IE, who limit 288Ko, 4095 rules, 31 sheets + # http://support.microsoft.com/kb/262161/en + if pattern == "static/lib/bootstrap/css/bootstrap.css": + if include_remotes: + r.insert(0, (None, fs2web(path[len(addons_path):]))) + else: + r.append((path, fs2web(path[len(addons_path):]))) return r def manifest_list(extension, mods=None, db=None, debug=False): @@ -421,6 +442,15 @@ def set_cookie_and_redirect(redirect_url): redirect.autocorrect_location_header = False return redirect +def login_redirect(): + url = '/web/login?' + if request.debug: + url += 'debug&' + return """ + """ % (url,) + def load_actions_from_ir_values(key, key2, models, meta): Values = request.session.model('ir.values') actions = Values.get(key, key2, models, meta, request.context) @@ -591,12 +621,13 @@ html_template = """ """ -def render_bootstrap_template(db, template, values=None, debug=False, lazy=False, **kw): - if request and request.debug: +def render_bootstrap_template(template, values=None, debug=False, db=None, **kw): + if not db: + db = request.db + if request.debug: debug = True if values is None: values = {} - values.update(kw) values['debug'] = debug values['current_db'] = db try: @@ -612,36 +643,40 @@ def render_bootstrap_template(db, template, values=None, debug=False, lazy=False values['modules'] = module_boot(db=db) values['modules'] = simplejson.dumps(values['modules']) - def callback(template, values): - registry = openerp.modules.registry.RegistryManager.get(db) - with registry.cursor() as cr: - view_obj = registry["ir.ui.view"] - return view_obj.render(cr, openerp.SUPERUSER_ID, template, values) - if lazy: - return LazyResponse(callback, template=template, values=values) - else: - return callback(template, values) + return request.render(template, values, **kw) class Home(http.Controller): @http.route('/', type='http', auth="none") def index(self, s_action=None, db=None, **kw): - return http.local_redirect('/web', query=request.params) + return http.local_redirect('/web', query=request.params, keep_hash=True) @http.route('/web', type='http', auth="none") def web_client(self, s_action=None, **kw): ensure_db() if request.session.uid: - html = render_bootstrap_template(request.session.db, "web.webclient_bootstrap") - return request.make_response(html, {'Cache-Control': 'no-cache', 'Content-Type': 'text/html; charset=utf-8'}) + if kw.get('redirect'): + return werkzeug.utils.redirect(kw.get('redirect'), 303) + + headers = { + 'Cache-Control': 'no-cache', + 'Content-Type': 'text/html; charset=utf-8', + } + return render_bootstrap_template("web.webclient_bootstrap", headers=headers) else: - return http.local_redirect('/web/login', query=request.params) + return login_redirect() @http.route('/web/login', type='http', auth="none") def web_login(self, redirect=None, **kw): ensure_db() + if request.httprequest.method == 'GET' and redirect and request.session.uid: + return http.redirect_with_hash(redirect) + + if not request.uid: + request.uid = openerp.SUPERUSER_ID + values = request.params.copy() if not redirect: redirect = '/web?' + request.httprequest.query_string @@ -651,7 +686,7 @@ class Home(http.Controller): if uid is not False: return http.redirect_with_hash(redirect) values['error'] = "Wrong login/password" - return render_bootstrap_template(request.session.db, 'web.login', values, lazy=True) + return render_bootstrap_template('web.login', values) @http.route('/login', type='http', auth="none") def login(self, db, login, key, redirect="/web", **kw): @@ -928,10 +963,11 @@ class Database(http.Controller): return simplejson.dumps([[],[{'error': openerp.tools.ustr(e), 'title': _('Backup Database')}]]) @http.route('/web/database/restore', type='http', auth="none") - def restore(self, db_file, restore_pwd, new_db): + def restore(self, db_file, restore_pwd, new_db, mode): try: + copy = mode == 'copy' data = base64.b64encode(db_file.read()) - request.session.proxy("db").restore(restore_pwd, new_db, data) + request.session.proxy("db").restore(restore_pwd, new_db, data, copy) return '' except openerp.exceptions.AccessDenied, e: raise Exception("AccessDenied") @@ -1179,11 +1215,14 @@ class DataSet(http.Controller): def _call_kw(self, model, method, args, kwargs): # Temporary implements future display_name special field for model#read() - if method == 'read' and kwargs.get('context', {}).get('future_display_name'): + if method in ('read', 'search_read') and kwargs.get('context', {}).get('future_display_name'): if 'display_name' in args[1]: - names = dict(request.session.model(model).name_get(args[0], **kwargs)) + if method == 'read': + names = dict(request.session.model(model).name_get(args[0], **kwargs)) + else: + names = dict(request.session.model(model).name_search('', args[0], **kwargs)) args[1].remove('display_name') - records = request.session.model(model).read(*args, **kwargs) + records = getattr(request.session.model(model), method)(*args, **kwargs) for record in records: record['display_name'] = \ names.get(record['id']) or "%s#%d" % (model, (record['id'])) @@ -1596,8 +1635,8 @@ class Export(http.Controller): model, map(operator.itemgetter('name'), export_fields_list)) return [ - {'name': field_name, 'label': fields_data[field_name]} - for field_name in fields_data.keys() + {'name': field['name'], 'label': fields_data[field['name']]} + for field in export_fields_list ] def fields_info(self, model, export_fields): @@ -1656,6 +1695,8 @@ class Export(http.Controller): for k, v in self.fields_info(model, export_fields).iteritems()) class ExportFormat(object): + raw_data = False + @property def content_type(self): """ Provides the format's content type """ @@ -1688,7 +1729,7 @@ class ExportFormat(object): ids = ids or Model.search(domain, 0, False, False, request.context) field_names = map(operator.itemgetter('name'), fields) - import_data = Model.export_data(ids, field_names, request.context).get('datas',[]) + import_data = Model.export_data(ids, field_names, self.raw_data, context=request.context).get('datas',[]) if import_compat: columns_headers = field_names @@ -1741,6 +1782,8 @@ class CSVExport(ExportFormat, http.Controller): return data class ExcelExport(ExportFormat, http.Controller): + # Excel needs raw data to correctly handle numbers and date values + raw_data = True @http.route('/web/export/xls', type='http', auth="user") @serialize_exception @@ -1762,14 +1805,20 @@ class ExcelExport(ExportFormat, http.Controller): worksheet.write(0, i, fieldname) worksheet.col(i).width = 8000 # around 220 pixels - style = xlwt.easyxf('align: wrap yes') + base_style = xlwt.easyxf('align: wrap yes') + date_style = xlwt.easyxf('align: wrap yes', num_format_str='YYYY-MM-DD') + datetime_style = xlwt.easyxf('align: wrap yes', num_format_str='YYYY-MM-DD HH:mm:SS') for row_index, row in enumerate(rows): for cell_index, cell_value in enumerate(row): + cell_style = base_style 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) + elif isinstance(cell_value, datetime.datetime): + cell_style = datetime_style + elif isinstance(cell_value, datetime.date): + cell_style = date_style + worksheet.write(row_index + 1, cell_index, cell_value, cell_style) fp = StringIO() workbook.save(fp) diff --git a/addons/web/controllers/testing.py b/addons/web/controllers/testing.py index 598270cfe54..a51b619d49f 100644 --- a/addons/web/controllers/testing.py +++ b/addons/web/controllers/testing.py @@ -52,7 +52,6 @@ TESTING = Template(u"""