From be8fc2296b3822baffe0bc68cb4b7efdfe53fa91 Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Thu, 12 Sep 2019 09:45:11 +0000 Subject: [PATCH] [IMP] base, http_routing, website: allow custom routing rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After this commit, you will be able (in technical mode) to update the url for the python controllers. Eg. You can now rename /shop in /garden and /shop/product/ in /garden/vegetable/ Most of urls will be replaced at fly in the renderd qweb, with the function url_for but all old urls will keep available. So if you access url /shop you will be automatically redirected to /garden (308 Permanent Redirect). As for cdn and other post-process of att, the automatically replacement in the rendered qweb is only done when you will be not website editor. But the new dispatch of URL will be applied in all cases. For developper, since it is Permanent Redirect, don't forget to clear cache or open chrome debug tool (with option 'Disable cache while DevTools is Open) to see your lasts changes. closes odoo/odoo#36555 Signed-off-by: Jérémy Kersten (jke) --- addons/http_routing/models/ir_http.py | 77 +++++++++---- addons/website/__manifest__.py | 1 + addons/website/controllers/main.py | 1 - addons/website/models/__init__.py | 2 +- addons/website/models/ir_http.py | 66 ++++++++++- addons/website/models/website.py | 1 + addons/website/models/website_page.py | 2 +- addons/website/models/website_redirect.py | 18 --- addons/website/models/website_rewrite.py | 104 ++++++++++++++++++ addons/website/security/ir.model.access.csv | 5 +- addons/website/static/src/js/menu/content.js | 2 +- addons/website/tests/test_lang_url.py | 6 +- .../views/res_config_settings_views.xml | 7 -- addons/website/views/website_rewrite.xml | 80 ++++++++++++++ addons/website/views/website_views.xml | 63 ----------- odoo/addons/base/models/ir_http.py | 39 +++++-- odoo/http.py | 18 +-- 17 files changed, 349 insertions(+), 143 deletions(-) delete mode 100644 addons/website/models/website_redirect.py create mode 100644 addons/website/models/website_rewrite.py create mode 100644 addons/website/views/website_rewrite.xml diff --git a/addons/http_routing/models/ir_http.py b/addons/http_routing/models/ir_http.py index 78217881e65..71d62d054db 100644 --- a/addons/http_routing/models/ir_http.py +++ b/addons/http_routing/models/ir_http.py @@ -99,6 +99,7 @@ def slug(value): return str(identifier) return "%s-%d" % (slugname, identifier) + # NOTE: as the pattern is used as it for the ModelConverter (ir_http.py), do not use any flags _UNSLUG_RE = re.compile(r'(?:(\w{1,2}|\w[A-Za-z0-9-_]+?\w)-)?(-?\d+)(?=$|/)') @@ -128,7 +129,7 @@ def unslug_url(s): # Language tools # ------------------------------------------------------------ -def url_for(path_or_uri, lang_code=None): +def url_lang(path_or_uri, lang_code=None): ''' Given a relative URL, make it absolute and add the required lang or remove useless lang. Nothing will be done for absolute URL. @@ -164,10 +165,46 @@ def url_for(path_or_uri, lang_code=None): elif lang_url_code != default_lg.url_code or force_lang: ps.insert(1, lang_url_code) location = u'/'.join(ps) - return location +def url_for(url_from, lang_code=None, no_rewrite=False): + ''' Return the url with the rewriting applied. + Nothing will be done for absolute URL, or short URL from 1 char. + + :param url_from: The URL to convert. + :param lang_code: Must be the lang `code`. It could also be something + else, such as `'[lang]'` (used for url_return). + :param no_rewrite: don't try to match route with website.rewrite. + ''' + new_url = False + + # don't try to match route if we know that no rewrite has been loaded. + if not request.env['ir.http']._rewrite_len.get(request.website_routing): + no_rewrite = True + + # avoid useless check for 1 char URL '/', '#', ... and absolute URL + if not no_rewrite and url_from and (len(url_from) > 1 or not url_from.startswith('http')): + path, _, qs = url_from.partition('?') + req = request.httprequest + router = req.app.get_db_router(request.db).bind('') + try: + _ = router.match(path, method='POST') + except werkzeug.exceptions.MethodNotAllowed as e: + _ = router.match(path, method='GET') + except werkzeug.routing.RequestRedirect as e: + # remove query string from current env + new_url = e.new_url.split('?')[0] + # remove scheme and add query_string from url_from + new_url = new_url[7:] + (qs and '?%s' % qs or '') + except werkzeug.exceptions.NotFound as e: + new_url = url_from + except Exception as e: + raise e + + return url_lang(new_url or url_from, lang_code=lang_code) + + def is_multilang_url(local_url, lang_url_codes=None): ''' Check if the given URL content is supposed to be translated. To be considered as translatable, the URL should either: @@ -374,12 +411,15 @@ class IrHttp(models.AbstractModel): func = None routing_error = None + + # handle // in url + if request.httprequest.method == 'GET' and '//' in request.httprequest.path: + new_url = request.httprequest.path.replace('//', '/') + '?' + request.httprequest.query_string.decode('utf-8') + return werkzeug.utils.redirect(new_url, 301) + # locate the controller method try: - if request.httprequest.method == 'GET' and '//' in request.httprequest.path: - new_url = request.httprequest.path.replace('//', '/') + '?' + request.httprequest.query_string.decode('utf-8') - return werkzeug.utils.redirect(new_url, 301) - rule, arguments = cls._find_handler(return_rule=True) + rule, arguments = cls._match(request.httprequest.path) func = rule.endpoint request.is_frontend = func.routing.get('website', False) except werkzeug.exceptions.NotFound as e: @@ -388,13 +428,7 @@ class IrHttp(models.AbstractModel): request.is_frontend = True routing_error = e - request.is_frontend_multilang = ( - request.is_frontend and - (not func or (func and func.routing.get('multilang', func.routing['type'] == 'http'))) - ) - - cls._geoip_setup_resolver() - cls._geoip_resolve() + request.is_frontend_multilang = not func or (func and request.is_frontend and func.routing.get('multilang', func.routing['type'] == 'http')) # check authentication level try: @@ -405,6 +439,9 @@ class IrHttp(models.AbstractModel): except Exception as e: return cls._handle_exception(e) + cls._geoip_setup_resolver() + cls._geoip_resolve() + # For website routes (only), add website params on `request` if request.is_frontend: request.redirect = lambda url, code=302: werkzeug.utils.redirect(url_for(url), code) @@ -416,19 +453,19 @@ class IrHttp(models.AbstractModel): if request.routing_iteration == 1: is_a_bot = cls.is_a_bot() nearest_lang = not func and cls.get_nearest_lang(request.env['res.lang']._lang_get_code(path[1])) - url_lang = nearest_lang and path[1] + url_lg = nearest_lang and path[1] # The default lang should never be in the URL, and a wrong lang # should never be in the URL. - wrong_url_lang = url_lang and (url_lang != request.lang.url_code or url_lang == default_lg_id.url_code) + wrong_url_lg = url_lg and (url_lg != request.lang.url_code or url_lg == default_lg_id.url_code) # The lang is missing from the URL if multi lang is enabled for # the route and the current lang is not the default lang. # POST requests are excluded from this condition. - missing_url_lang = not url_lang and request.is_frontend_multilang and request.lang != default_lg_id and request.httprequest.method != 'POST' + missing_url_lg = not url_lg and request.is_frontend_multilang and request.lang != default_lg_id and request.httprequest.method != 'POST' # Bots should never be redirected when the lang is missing # because it is the only way for them to index the default lang. - if wrong_url_lang or (missing_url_lang and not is_a_bot): - if url_lang: + if wrong_url_lg or (missing_url_lg and not is_a_bot): + if url_lg: path.pop(1) if request.lang != default_lg_id: path.insert(1, request.lang.url_code) @@ -437,12 +474,12 @@ class IrHttp(models.AbstractModel): redirect = request.redirect(path + '?' + request.httprequest.query_string.decode('utf-8')) redirect.set_cookie('frontend_lang', request.lang.code) return redirect - elif url_lang: + elif url_lg: request.uid = None path.pop(1) routing_error = None return cls.reroute('/'.join(path) or '/') - elif missing_url_lang and is_a_bot: + elif missing_url_lg and is_a_bot: # Ensure that if the URL without lang is not redirected, the # current lang is indeed the default lang, because it is the # lang that bots should index in that case. diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py index 82483b63bd2..3c86cc69a0e 100644 --- a/addons/website/__manifest__.py +++ b/addons/website/__manifest__.py @@ -29,6 +29,7 @@ 'views/website_views.xml', 'views/website_visitor_views.xml', 'views/res_config_settings_views.xml', + 'views/website_rewrite.xml', 'views/ir_actions_views.xml', 'views/ir_attachment_views.xml', 'views/res_partner_views.xml', diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 6ac7ef197f8..e1252e0e23b 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -4,7 +4,6 @@ import base64 import datetime import json import os -import re import logging import pytz import requests diff --git a/addons/website/models/__init__.py b/addons/website/models/__init__.py index 1c32e9d514a..83ef426cacf 100644 --- a/addons/website/models/__init__.py +++ b/addons/website/models/__init__.py @@ -11,7 +11,7 @@ from . import mixins from . import website from . import website_menu from . import website_page -from . import website_redirect +from . import website_rewrite from . import ir_rule from . import ir_ui_view from . import res_company diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index ccfc2c531e4..2ad541d3e33 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -11,6 +11,8 @@ import werkzeug import werkzeug.routing import werkzeug.utils +from functools import partial + import odoo from odoo import api, models, registry from odoo import SUPERUSER_ID @@ -61,6 +63,58 @@ def get_request_website(): class Http(models.AbstractModel): _inherit = 'ir.http' + @classmethod + def routing_map(cls, key=None): + key = key or (request and request.website_routing) + return super(Http, cls).routing_map(key=key) + + @classmethod + def clear_caches(cls): + super(Http, cls)._clear_routing_map() + return super(Http, cls).clear_caches() + + @classmethod + def _slug_matching(cls, adapter, endpoint, **kw): + for arg in kw: + if isinstance(kw[arg], models.BaseModel): + kw[arg] = kw[arg].with_user(request.uid) + qs = request.httprequest.query_string.decode('utf-8') + return adapter.build(endpoint, kw) + (qs and '?%s' % qs or '') + + @classmethod + def _match(cls, path_info, key=None): + key = key or (request and request.website_routing) + return super(Http, cls)._match(path_info, key=key) + + @classmethod + def _generate_routing_rules(cls, modules, converters): + website_id = request.website_routing + logger.debug("_generate_routing_rules for website: %s", website_id) + domain = [('redirect_type', 'in', ('rewrite', 'not_found')), '|', ('website_id', '=', False), ('website_id', '=', website_id)] + + rewrites = dict([(x.url_from, x) for x in request.env['website.rewrite'].sudo().search(domain)]) + cls._rewrite_len[website_id] = len(rewrites) + + for url, endpoint, routing in super(Http, cls)._generate_routing_rules(modules, converters): + routing = dict(routing) + if url in rewrites: + rewrite = rewrites[url] + url_to = rewrite.url_to + if rewrite.redirect_type == 'rewrite': + logger.debug('Add rule %s for %s' % (url_to, website_id)) + yield url_to, endpoint, routing # yield new url + + if url != url_to: + logger.debug('Redirect from %s to %s for website %s' % (url, url_to, website_id)) + _slug_matching = partial(cls._slug_matching, endpoint=endpoint) + routing['redirect_to'] = _slug_matching + yield url, endpoint, routing # yield original redirected to new url + elif rewrite.redirect_type == 'not_found': + logger.debug('Return 404 for %s for website %s' % (url, website_id)) + continue + else: + yield url, endpoint, routing + @classmethod def _get_converters(cls): """ Get the converters list for custom url pattern werkzeug need to @@ -111,7 +165,11 @@ class Http(models.AbstractModel): handling the original request, in which we should create the visitor. We ignore every other rerouting requests. """ is_rerouting = hasattr(request, 'routing_iteration') + + request.website_routing = request.env['website'].get_current_website().id + response = super(Http, cls)._dispatch() + if not is_rerouting: cls._register_website_track(response) return response @@ -186,8 +244,12 @@ class Http(models.AbstractModel): @classmethod def _serve_redirect(cls): req_page = request.httprequest.path - domain = [('url_from', '=', req_page)] + request.website.website_domain() - return request.env['website.redirect'].search(domain, limit=1) + domain = [ + ('redirect_type', 'in', ('redirect_301', 'redirect_302')), + ('url_from', '=', req_page) + ] + domain += request.website.website_domain() + return request.env['website.rewrite'].sudo().search(domain, limit=1) @classmethod def _serve_fallback(cls, exception): diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 0ddca6810e2..f8d8d652e72 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -714,6 +714,7 @@ class Website(models.Model): converters = rule._converters or {} if query_string and not converters and (query_string not in rule.build([{}], append_unknown=False)[1]): continue + values = [{}] # converters with a domain are processed after the other ones convitems = sorted( diff --git a/addons/website/models/website_page.py b/addons/website/models/website_page.py index 5473efeffe6..1d3d72b68a6 100644 --- a/addons/website/models/website_page.py +++ b/addons/website/models/website_page.py @@ -122,7 +122,7 @@ class Page(models.Model): # Create redirect if needed if data['create_redirect']: - self.env['website.redirect'].create({ + self.env['website.rewrite'].create({ 'redirect_type': data['redirect_type'], 'url_from': original_url, 'url_to': url, diff --git a/addons/website/models/website_redirect.py b/addons/website/models/website_redirect.py deleted file mode 100644 index a0fbda884b0..00000000000 --- a/addons/website/models/website_redirect.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import fields, models - - -class WebsiteRedirect(models.Model): - _name = "website.redirect" - _description = "Website Redirect" - _order = "sequence, id" - _rec_name = 'url_from' - - redirect_type = fields.Selection([('301', 'Moved permanently (301)'), ('302', 'Moved temporarily (302)')], string='Redirection Type', required=True, default="301") - url_from = fields.Char('Redirect From', required=True) - url_to = fields.Char('Redirect To', required=True) - website_id = fields.Many2one('website', 'Website', ondelete='cascade') - active = fields.Boolean(default=True) - sequence = fields.Integer() diff --git a/addons/website/models/website_rewrite.py b/addons/website/models/website_rewrite.py new file mode 100644 index 00000000000..77acdb8c120 --- /dev/null +++ b/addons/website/models/website_rewrite.py @@ -0,0 +1,104 @@ +from odoo import models, fields, api +from odoo.exceptions import AccessDenied + +import logging +_logger = logging.getLogger(__name__) + + +class WebsiteRoute(models.Model): + _rec_name = 'path' + _name = 'website.route' + _description = "All Website Route" + _order = 'path' + + path = fields.Char('Route') + + @api.model + def _name_search(self, name='', args=None, operator='ilike', limit=100, name_get_uid=None): + res = super(WebsiteRoute, self)._name_search(name=name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid) + if not len(res): + self._refresh() + return super(WebsiteRoute, self)._name_search(name=name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid) + return res + + def _refresh(self): + _logger.debug("Refreshing website.route") + ir_http = self.env['ir.http'] + tocreate = [] + paths = {rec.path: rec for rec in self.search([])} + for url, _, routing in ir_http._generate_routing_rules(self.pool._init_modules, converters=ir_http._get_converters()): + if 'GET' in (routing.get('methods') or ['GET']): + if paths.get(url): + paths.pop(url) + else: + tocreate.append({'path': url}) + + if tocreate: + _logger.info("Add %d website.route" % len(tocreate)) + self.create(tocreate) + + if paths: + find = self.search([('path', 'in', list(paths.keys()))]) + _logger.info("Delete %d website.route" % len(find)) + find.unlink() + + +class WebsiteRewrite(models.Model): + _name = 'website.rewrite' + _description = "Website rewrite" + + name = fields.Char('Name', required=True) + website_id = fields.Many2one('website', string="Website", ondelete='cascade', index=True) + active = fields.Boolean(default=True) + url_from = fields.Char('URL from', index=True) + route_id = fields.Many2one('website.route') + url_to = fields.Char("URL to") + redirect_type = fields.Selection([ + ('not_found', '404 Not Found'), + ('redirect_301', '301 Moved permanently'), + ('redirect_302', '302 Moved temporarily'), + ('rewrite', '308 Redirect / Rewrite'), + ], string='Action', default="redirect_302", + help='''Type of redirect/Rewrite:\n + 301 Moved permanently: The browser will keep in cache the new url. + 302 Moved temporarily: The browser will not keep in cache the new url and ask again the next time the new url. + 404 Not Found: If you want remove a specific page/controller (e.g. Ecommerce is installed, but you don't want /shop on a specific website) + 308 Redirect / Rewrite: If you want rename a controller with a new url. (Eg: /shop -> /garden - Both url will be accessible but /shop will automatically be redirected to /garden) + ''') + + sequence = fields.Integer() + + @api.onchange('route_id') + def _onchange_route_id(self): + self.url_from = self.route_id.path + self.url_to = self.route_id.path + + def name_get(self): + result = [] + for rewrite in self: + name = rewrite.redirect_type + ' - ' + rewrite.name + result.append((rewrite.id, name)) + return result + + @api.model + def create(self, vals): + res = super(WebsiteRewrite, self).create(vals) + self._invalidate_routing() + return res + + def write(self, vals): + res = super(WebsiteRewrite, self).write(vals) + self._invalidate_routing() + return res + + def unlink(self): + res = super(WebsiteRewrite, self).unlink() + self._invalidate_routing() + return res + + def _invalidate_routing(self): + # call clear_caches on this worker to reload routing table + self.env['ir.http'].clear_caches() + + def refresh_routes(self): + self.env['website.route']._refresh() diff --git a/addons/website/security/ir.model.access.csv b/addons/website/security/ir.model.access.csv index 624735ca4a4..242c3bd2ea5 100644 --- a/addons/website/security/ir.model.access.csv +++ b/addons/website/security/ir.model.access.csv @@ -3,8 +3,8 @@ access_website_public,website,website.model_website,,1,0,0,0 access_website_designer,website,website.model_website,group_website_designer,1,1,1,1 access_website_menu,access_website_menu,model_website_menu,,1,0,0,0 access_website_menu_designer,Web Menu Manager,model_website_menu,group_website_designer,1,1,1,1 -access_website_redirect,access_website_redirect,model_website_redirect,,1,0,0,0 -access_website_redirect_designer,Web Redirect Manager,model_website_redirect,group_website_designer,1,1,1,1 +access_website_rewrite,access_website_rewrite,model_website_rewrite,,0,0,0,0 +access_website_rewrite_designer,Web Rewrite Manager,model_website_rewrite,group_website_designer,1,1,1,1 access_website_page,access_website_page,model_website_page,,1,0,0,0 access_website_page_designer,Web Page Manager,model_website_page,group_website_designer,1,1,1,1 access_website,web menu manager,website.model_website,group_website_designer,1,1,1,1 @@ -18,3 +18,4 @@ access_website_visitor_lastconnection_designer,access_website_visitor_lastconnec access_website_visitor_lastconnection_system,access_website_visitor_lastconnection_system,model_website_visitor_lastconnection,base.group_system,1,1,1,1 access_website_track_designer,access_website_track_designer,model_website_track,website.group_website_designer,1,1,1,1 access_website_track_system,access_website_track_system,model_website_track,base.group_system,1,1,1,1 +access_website_route_designer,access_website_designer_route,model_website_route,group_website_designer,1,1,1,1 \ No newline at end of file diff --git a/addons/website/static/src/js/menu/content.js b/addons/website/static/src/js/menu/content.js index c0daa93925f..6aeeb9d5085 100644 --- a/addons/website/static/src/js/menu/content.js +++ b/addons/website/static/src/js/menu/content.js @@ -86,7 +86,7 @@ var PagePropertiesDialog = weWidgets.Dialog.extend({ })); defs.push(this._rpc({ - model: 'website.redirect', + model: 'website.rewrite', method: 'fields_get', }).then(function (fields) { self.fields = fields; diff --git a/addons/website/tests/test_lang_url.py b/addons/website/tests/test_lang_url.py index f4998168074..871248c2778 100644 --- a/addons/website/tests/test_lang_url.py +++ b/addons/website/tests/test_lang_url.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.addons.http_routing.models.ir_http import url_for +from odoo.addons.http_routing.models.ir_http import url_lang from odoo.addons.website.tools import MockRequest from odoo.tests import HttpCase, tagged @@ -18,9 +18,9 @@ class TestLangUrl(HttpCase): self.website.language_ids = self.env.ref('base.lang_en') + lang_fr self.website.default_lang_id = self.env.ref('base.lang_en') - def test_01_url_for(self): + def test_01_url_lang(self): with MockRequest(self.env, website=self.website): - self.assertEqual(url_for('', '[lang]'), '/[lang]/hello/', "`[lang]` is used to be replaced in the url_return after installing a language, it should not be replaced or removed.") + self.assertEqual(url_lang('', '[lang]'), '/[lang]/hello/', "`[lang]` is used to be replaced in the url_return after installing a language, it should not be replaced or removed.") def test_02_url_redirect(self): url = '/fr_WHATEVER/contactus' diff --git a/addons/website/views/res_config_settings_views.xml b/addons/website/views/res_config_settings_views.xml index ff0ac73f2b5..4c3f7e6c296 100644 --- a/addons/website/views/res_config_settings_views.xml +++ b/addons/website/views/res_config_settings_views.xml @@ -380,13 +380,6 @@ sequence="30" /> - - + + + + website.rewrite + +
+
+
+ + + + + + + + + + + + + + + + +
+
+
+ + + website.rewrite.list + website.rewrite + + + + + + + + + + + + + + + + Rewrite + ir.actions.act_window + website.rewrite + + + + + + + website.rewrite.search + website.rewrite + + + + + + + + + + + + +
diff --git a/addons/website/views/website_views.xml b/addons/website/views/website_views.xml index 92841cb2063..cac1ba89b28 100644 --- a/addons/website/views/website_views.xml +++ b/addons/website/views/website_views.xml @@ -62,23 +62,6 @@ - - - website.redirect.search - website.redirect - - - - - - - - - - - - - Websites website @@ -87,52 +70,6 @@ current - - - website.redirect.form - website.redirect - -
- - - - - - - - - - - - - -
-
-
- - - website.redirect.list - website.redirect - - - - - - - - - - - - - - Website Redirects - website.redirect - tree,form - - current - - diff --git a/odoo/addons/base/models/ir_http.py b/odoo/addons/base/models/ir_http.py index cc76a8d0ff9..f2c87db5fa4 100644 --- a/odoo/addons/base/models/ir_http.py +++ b/odoo/addons/base/models/ir_http.py @@ -87,8 +87,12 @@ class IrHttp(models.AbstractModel): return {'model': ModelConverter, 'models': ModelsConverter, 'int': SignedIntConverter} @classmethod - def _find_handler(cls, return_rule=False): - return cls.routing_map().bind_to_environ(request.httprequest.environ).match(return_rule=return_rule) + def _match(cls, path_info, key=None): + return cls.routing_map(key=key).bind('').match( + return_rule=True, + method=request.httprequest.method, + path_info=path_info + ) @classmethod def _auth_method_user(cls): @@ -208,7 +212,7 @@ class IrHttp(models.AbstractModel): # locate the controller method try: - rule, arguments = cls._find_handler(return_rule=True) + rule, arguments = cls._match(request.httprequest.path) func = rule.endpoint except werkzeug.exceptions.NotFound as e: return cls._handle_exception(e) @@ -245,24 +249,37 @@ class IrHttp(models.AbstractModel): return cls._handle_exception(werkzeug.exceptions.NotFound()) @classmethod - def routing_map(cls): + def _generate_routing_rules(cls, modules, converters): + return http._generate_routing_rules(modules, False, converters) + + @classmethod + def routing_map(cls, key=None): if not hasattr(cls, '_routing_map'): - _logger.info("Generating routing map") - installed = request.registry._init_modules - {'web'} + cls._routing_map = {} + cls._rewrite_len = {} + if key not in cls._routing_map: + _logger.info("Generating routing map for key %s" % str(key)) + installed = request.registry._init_modules | set(odoo.conf.server_wide_modules) if tools.config['test_enable'] and odoo.modules.module.current_test: installed.add(odoo.modules.module.current_test) - mods = [''] + odoo.conf.server_wide_modules + sorted(installed) + mods = sorted(installed) # Note : when routing map is generated, we put it on the class `cls` # to make it available for all instance. Since `env` create an new instance # of the model, each instance will regenared its own routing map and thus # regenerate its EndPoint. The routing map should be static. - cls._routing_map = http.routing_map(mods, False, converters=cls._get_converters()) - return cls._routing_map + routing_map = werkzeug.routing.Map(strict_slashes=False, converters=cls._get_converters()) + for url, endpoint, routing in cls._generate_routing_rules(mods, converters=cls._get_converters()): + xtra_keys = 'defaults subdomain build_only strict_slashes redirect_to alias host'.split() + kw = {k: routing[k] for k in xtra_keys if k in routing} + routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods'], **kw)) + cls._routing_map[key] = routing_map + return cls._routing_map[key] @classmethod def _clear_routing_map(cls): if hasattr(cls, '_routing_map'): - del cls._routing_map + cls._routing_map = {} + _logger.debug("Clear routing map") #------------------------------------------------------ # Binary server @@ -337,8 +354,6 @@ class IrHttp(models.AbstractModel): status = 301 content = record.url - - return status, content, filename, mimetype, filehash def _binary_record_content( diff --git a/odoo/http.py b/odoo/http.py index 94445401801..721746ee7ce 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -908,9 +908,8 @@ class EndPoint(object): def __call__(self, *args, **kw): return self.method(*args, **kw) -def routing_map(modules, nodb_only, converters=None): - routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters) +def _generate_routing_rules(modules, nodb_only, converters=None): def get_subclasses(klass): def valid(c): return c.__module__.startswith('odoo.addons.') and c.__module__.split(".")[2] in modules @@ -949,16 +948,8 @@ def routing_map(modules, nodb_only, converters=None): assert routing['routes'], "Method %r has not route defined" % mv endpoint = EndPoint(mv, routing) for url in routing['routes']: - if routing.get("combine", False): - # deprecated v7 declaration - url = o._cp_path.rstrip('/') + '/' + url.lstrip('/') - if url.endswith("/") and len(url) > 1: - url = url[: -1] + yield (url, endpoint, routing) - xtra_keys = 'defaults subdomain build_only strict_slashes redirect_to alias host'.split() - kw = {k: routing[k] for k in xtra_keys if k in routing} - routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods'], **kw)) - return routing_map #---------------------------------------------------------- # HTTP Sessions @@ -1276,7 +1267,10 @@ class Root(object): @lazy_property def nodb_routing_map(self): _logger.info("Generating nondb routing") - return routing_map([''] + odoo.conf.server_wide_modules, True) + routing_map = werkzeug.routing.Map(strict_slashes=False, converters=None) + for url, endpoint, routing in odoo.http._generate_routing_rules([''] + odoo.conf.server_wide_modules, True): + routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods'])) + return routing_map def __call__(self, environ, start_response): """ Handle a WSGI request