[IMP] base, http_routing, website: allow custom routing rule

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) <jke@openerp.com>
This commit is contained in:
Jeremy Kersten
2019-09-30 13:58:14 +00:00
parent b6b42ef73f
commit be8fc2296b
17 changed files with 349 additions and 143 deletions
+1
View File
@@ -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',
-1
View File
@@ -4,7 +4,6 @@ import base64
import datetime
import json
import os
import re
import logging
import pytz
import requests
+1 -1
View File
@@ -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
+64 -2
View File
@@ -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):
+1
View File
@@ -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(
+1 -1
View File
@@ -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,
-18
View File
@@ -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()
+104
View File
@@ -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()
+3 -2
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
3 access_website_designer website website.model_website group_website_designer 1 1 1 1
4 access_website_menu access_website_menu model_website_menu 1 0 0 0
5 access_website_menu_designer Web Menu Manager model_website_menu group_website_designer 1 1 1 1
6 access_website_redirect access_website_rewrite access_website_redirect access_website_rewrite model_website_redirect model_website_rewrite 1 0 0 0 0
7 access_website_redirect_designer access_website_rewrite_designer Web Redirect Manager Web Rewrite Manager model_website_redirect model_website_rewrite group_website_designer 1 1 1 1
8 access_website_page access_website_page model_website_page 1 0 0 0
9 access_website_page_designer Web Page Manager model_website_page group_website_designer 1 1 1 1
10 access_website web menu manager website.model_website group_website_designer 1 1 1 1
18 access_website_visitor_lastconnection_system access_website_visitor_lastconnection_system model_website_visitor_lastconnection base.group_system 1 1 1 1
19 access_website_track_designer access_website_track_designer model_website_track website.group_website_designer 1 1 1 1
20 access_website_track_system access_website_track_system model_website_track base.group_system 1 1 1 1
21 access_website_route_designer access_website_designer_route model_website_route group_website_designer 1 1 1 1
+1 -1
View File
@@ -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;
+3 -3
View File
@@ -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'
@@ -380,13 +380,6 @@
sequence="30"
/>
<menuitem name="Redirects"
id="menu_website_redirect_list"
action="action_website_redirect_list"
parent="menu_website_global_configuration"
sequence="40"
/>
<menuitem name="Menus"
id="menu_website_menu_list"
action="action_website_menu"
+80
View File
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Attachment -->
<record id="view_website_rewrite_form" model="ir.ui.view">
<field name="model">website.rewrite</field>
<field name="arch" type="xml">
<form string="Website rewrite Settings">
<header>
<button name="refresh_routes" string="Refresh route's list" type="object"
class="btn-primary"
attrs="{'invisible':[('redirect_type', '!=', 'rewrite')]}"
/>
</header>
<sheet>
<group>
<group>
<field name="name"/>
<field name="redirect_type"/>
<field name="url_from" attrs="{'invisible': [('redirect_type', '=', 'rewrite')]}"/>
<field name="route_id" string="URL from" options="{'no_create': True, 'no_open': True}" attrs="{'invisible': [('redirect_type', '!=', 'rewrite')]}"/>
<field name="url_to" attrs="{'invisible': [('redirect_type', '=', 'not_found')]}"/>
</group>
<group>
<field name="website_id" options="{'no_create': True}" groups="website.group_multi_website"/>
<field name="active"/>
<field name="sequence" groups="base.group_no_one"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_website_rewrite_tree" model="ir.ui.view">
<field name="name">website.rewrite.list</field>
<field name="model">website.rewrite</field>
<field name="arch" type="xml">
<tree string="Website rewrites">
<field name="redirect_type"/>
<field name="name"/>
<field name="url_from"/>
<field name="url_to"/>
<field name="website_id" options="{'no_create': True}" groups="website.group_multi_website"/>
<field name="active"/>
<field name="sequence" widget="handle" />
</tree>
</field>
</record>
<record id="action_website_rewrite_list" model="ir.actions.act_window">
<field name="name">Rewrite</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">website.rewrite</field>
<field name="view_id" eval="False"/>
</record>
<menuitem name="Redirects"
id="menu_website_rewrite"
action="action_website_rewrite_list"
parent="menu_website_global_configuration"
sequence="30"
groups="base.group_no_one"/>
<record id="view_rewrite_search" model="ir.ui.view">
<field name="name">website.rewrite.search</field>
<field name="model">website.rewrite</field>
<field name="arch" type="xml">
<search string="Search Redirect">
<field name="url_from"/>
<field name="url_to"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<group string="Group By">
<filter string="Redirection Type" name="group_by_type" domain="[]" context="{'group_by': 'redirect_type'}"/>
</group>
</search>
</field>
</record>
</odoo>
-63
View File
@@ -62,23 +62,6 @@
</field>
</record>
<record id="view_redirect_search" model="ir.ui.view">
<field name="name">website.redirect.search</field>
<field name="model">website.redirect</field>
<field name="arch" type="xml">
<search string="Search Redirect">
<field name="url_from"/>
<field name="url_to"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<group string="Group By">
<filter string="Redirection Type" name="group_by_type" domain="[]" context="{'group_by': 'redirect_type'}"/>
</group>
</search>
</field>
</record>
<record id="action_website_list" model="ir.actions.act_window">
<field name="name">Websites</field>
<field name="res_model">website</field>
@@ -87,52 +70,6 @@
<field name="target">current</field>
</record>
<!-- website.redirect views -->
<record id="website_redirect_form_view" model="ir.ui.view">
<field name="name">website.redirect.form</field>
<field name="model">website.redirect</field>
<field name="arch" type="xml">
<form string="Website Redirect Settings">
<sheet>
<group>
<group>
<field name="redirect_type"/>
<field name="url_from"/>
<field name="url_to"/>
</group>
<group>
<field name="website_id" options="{'no_create': True}" groups="website.group_multi_website"/>
<field name="active"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="website_redirect_tree_view" model="ir.ui.view">
<field name="name">website.redirect.list</field>
<field name="model">website.redirect</field>
<field name="arch" type="xml">
<tree string="Website Redirects" editable="bottom">
<field name="redirect_type"/>
<field name="url_from"/>
<field name="url_to"/>
<field name="website_id" options="{'no_create': True}" groups="website.group_multi_website"/>
<field name="active"/>
<field name="sequence" widget="handle" />
</tree>
</field>
</record>
<record id="action_website_redirect_list" model="ir.actions.act_window">
<field name="name">Website Redirects</field>
<field name="res_model">website.redirect</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="website_redirect_tree_view"/>
<field name="target">current</field>
<field name="search_view_id" ref="view_redirect_search"/>
</record>
<!-- website.page views -->
<record id="website_pages_form_view" model="ir.ui.view">