[IMP] http_routing, website: allow to customize the lang in URL

With this commit it is now possible to change the lang displayed in the URL.
Eg, you could use `/fr` instead of `/fr_BE`, or even a fancier `/french`.

Task-32838

Courtesy of pla@odoo.com

closes odoo/odoo#35135

Signed-off-by: Romain Derie (rde) <rde@odoo.com>
This commit is contained in:
Romain Derie
2019-08-26 16:35:19 +00:00
parent f10f120de3
commit 269aa59411
30 changed files with 240 additions and 154 deletions
+3 -3
View File
@@ -123,9 +123,9 @@ class AuthSignupHome(Home):
raise UserError(_("The form was not properly filled in."))
if values.get('password') != qcontext.get('confirm_password'):
raise UserError(_("Passwords do not match; please retype them."))
supported_langs = [code for code, _ in request.env['res.lang'].get_installed()]
if request.lang in supported_langs:
values['lang'] = request.lang
supported_lang_codes = [code for code, _ in request.env['res.lang'].get_installed()]
if request.lang.code in supported_lang_codes:
values['lang'] = request.lang.code
self._signup_with_values(qcontext.get('token'), values)
request.env.cr.commit()
+1
View File
@@ -11,6 +11,7 @@ Proposes advanced routing options not available in web or base to keep
base modules simple.
""",
'data': [
'views/res_lang_views.xml',
],
'depends': ['web'],
}
+75 -48
View File
@@ -128,41 +128,58 @@ def unslug_url(s):
# Language tools
# ------------------------------------------------------------
def url_for(path_or_uri, lang=None):
current_path = request.httprequest.path # should already be text
def url_for(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.
If there is only one language installed, the lang will not be handled
unless forced with `lang` parameter.
:param lang_code: Must be the lang `code`. It could also be something
else, such as `'[lang]'` (used for url_return).
'''
Lang = request.env['res.lang']
location = pycompat.to_text(path_or_uri).strip()
force_lang = lang is not None
force_lang = lang_code is not None
url = werkzeug.urls.url_parse(location)
# relative URL with either a path or a force_lang
if not url.netloc and not url.scheme and (url.path or force_lang):
location = werkzeug.urls.url_join(current_path, location)
location = werkzeug.urls.url_join(request.httprequest.path, location)
lang_url_codes = [url_code for _, url_code, _ in Lang.get_available()]
lang_code = pycompat.to_text(lang_code or request.context['lang'])
lang_url_code = Lang._lang_get(lang_code).url_code
lang_url_code = lang_url_code if lang_url_code in lang_url_codes else lang_code
lang = pycompat.to_text(lang or request.context.get('lang') or 'en_US')
langs = [lg[0] for lg in request.env['ir.http']._get_language_codes()]
if (len(langs) > 1 or force_lang) and is_multilang_url(location, langs):
if (len(lang_url_codes) > 1 or force_lang) and is_multilang_url(location, lang_url_codes):
ps = location.split(u'/')
if ps[1] in langs:
default_lg = request.env['ir.http']._get_default_lang()
if ps[1] in lang_url_codes:
# Replace the language only if we explicitly provide a language to url_for
if force_lang:
ps[1] = lang
ps[1] = lang_url_code
# Remove the default language unless it's explicitly provided
elif ps[1] == request.env['ir.http']._get_default_lang().code:
elif ps[1] == default_lg.url_code:
ps.pop(1)
# Insert the context language or the provided language
elif lang != request.env['ir.http']._get_default_lang().code or force_lang:
ps.insert(1, lang)
elif lang_url_code != default_lg.url_code or force_lang:
ps.insert(1, lang_url_code)
location = u'/'.join(ps)
return location
def is_multilang_url(local_url, langs=None):
if not langs:
langs = [lg[0] for lg in request.env['ir.http']._get_language_codes()]
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:
1. Match a POST (non-GET actually) controller that is `website=True` and
either `multilang` specified to True or if not specified, with `type='http'`.
2. If not matching 1., everything not under /static/ will be translatable
'''
if not lang_url_codes:
lang_url_codes = [url_code for _, url_code, _ in request.env['res.lang'].get_available()]
spath = local_url.split('/')
# if a language is already in the path, remove it
if spath[1] in langs:
if spath[1] in lang_url_codes:
spath.pop(1)
local_url = '/'.join(spath)
try:
@@ -222,10 +239,6 @@ class IrHttp(models.AbstractModel):
model=ModelConverter,
)
@classmethod
def _get_language_codes(cls):
return request.env['res.lang'].get_installed()
@classmethod
def _get_default_lang(cls):
lang_code = request.env['ir.default'].sudo().get('res.partner', 'lang')
@@ -285,13 +298,17 @@ class IrHttp(models.AbstractModel):
return any(bot in user_agent.encode('ascii', 'ignore') for bot in cls.bots)
@classmethod
def get_nearest_lang(cls, lang):
# Try to find a similar lang. Eg: fr_BE and fr_FR
short = lang.partition('_')[0]
def get_nearest_lang(cls, lang_code):
""" Try to find a similar lang. Eg: fr_BE and fr_FR
:param lang_code: the lang `code` (en_US)
"""
short_match = False
for code, dummy in cls._get_language_codes():
if code == lang:
return lang
if not lang_code:
return short_match
short = lang_code.partition('_')[0]
for code in [code for code, _, _ in request.env['res.lang'].get_available()]:
if code == lang_code:
return code
if not short_match and code.startswith(short):
short_match = code
return short_match
@@ -317,19 +334,28 @@ class IrHttp(models.AbstractModel):
@classmethod
def _add_dispatch_parameters(cls, func):
Lang = request.env['res.lang']
# only called for is_frontend request
if request.routing_iteration == 1:
context = dict(request.context)
path = request.httprequest.path.split('/')
langs = [code for code, _ in request.env['res.lang'].get_installed()]
is_a_bot = cls.is_a_bot()
cook_lang = request.httprequest.cookies.get('frontend_lang')
nearest_lang = not func and cls.get_nearest_lang(path[1])
preferred_lang = ((cook_lang if cook_lang in langs else False)
or (not is_a_bot and cls.get_nearest_lang(request.lang))
or cls._get_default_lang().code)
request.lang = context['lang'] = nearest_lang or preferred_lang
available_langs = Lang.get_available()
lang_codes = [code for code, _, _ in available_langs]
nearest_lang = not func and cls.get_nearest_lang(Lang._lang_get_code(path[1]))
cook_lang = request.httprequest.cookies.get('frontend_lang')
cook_lang = cook_lang in lang_codes and cook_lang
if nearest_lang:
lang = Lang._lang_get(nearest_lang)
else:
nearest_ctx_lg = not is_a_bot and cls.get_nearest_lang(request.env.context['lang'])
preferred_lang = Lang._lang_get(cook_lang or nearest_ctx_lg) or cls._get_default_lang()
lang = preferred_lang
request.lang = lang
context['lang'] = lang.code
# bind modified context
request.context = context
@@ -380,34 +406,34 @@ class IrHttp(models.AbstractModel):
return cls._handle_exception(e)
# For website routes (only), add website params on `request`
cook_lang = request.httprequest.cookies.get('frontend_lang')
if request.is_frontend:
request.redirect = lambda url, code=302: werkzeug.utils.redirect(url_for(url), code)
cls._add_dispatch_parameters(func)
path = request.httprequest.path.split('/')
default_lg_id = cls._get_default_lang()
if request.routing_iteration == 1:
is_a_bot = cls.is_a_bot()
nearest_lang = not func and cls.get_nearest_lang(path[1])
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]
# if lang in url but not the displayed or default language --> change or remove
# or no lang in url, and lang to dispay not the default language --> add lang
# and not a POST request
# and not a bot or bot but default lang in url
if ((url_lang and (url_lang != request.lang or url_lang == cls._get_default_lang().code))
or (not url_lang and request.is_frontend_multilang and request.lang != cls._get_default_lang().code)
if ((url_lang and (url_lang != request.lang.url_code or url_lang == default_lg_id.url_code))
or (not url_lang and request.is_frontend_multilang and request.lang != default_lg_id)
and request.httprequest.method != 'POST') \
and (not is_a_bot or (url_lang and url_lang == cls._get_default_lang().code)):
and (not is_a_bot or (url_lang and url_lang == default_lg_id.url_code)):
if url_lang:
path.pop(1)
if request.lang != cls._get_default_lang().code:
path.insert(1, request.lang)
if request.lang != default_lg_id:
path.insert(1, request.lang.url_code)
path = '/'.join(path) or '/'
routing_error = None
redirect = request.redirect(path + '?' + request.httprequest.query_string.decode('utf-8'))
redirect.set_cookie('frontend_lang', request.lang)
redirect.set_cookie('frontend_lang', request.lang.code)
return redirect
elif url_lang:
request.uid = None
@@ -415,7 +441,7 @@ class IrHttp(models.AbstractModel):
routing_error = None
return cls.reroute('/'.join(path) or '/')
if request.lang == cls._get_default_lang().code:
if request.lang == default_lg_id:
context = dict(request.context)
context['edit_translations'] = False
request.context = context
@@ -426,8 +452,9 @@ class IrHttp(models.AbstractModel):
# removed cache for auth public
result = super(IrHttp, cls)._dispatch()
if request.is_frontend and cook_lang != request.lang and hasattr(result, 'set_cookie'):
result.set_cookie('frontend_lang', request.lang)
cook_lang = request.httprequest.cookies.get('frontend_lang')
if request.is_frontend and cook_lang != request.lang.code and hasattr(result, 'set_cookie'):
result.set_cookie('frontend_lang', request.lang.code)
return result
@@ -463,8 +490,8 @@ class IrHttp(models.AbstractModel):
generated_path = werkzeug.url_unquote_plus(path)
current_path = werkzeug.url_unquote_plus(request.httprequest.path)
if generated_path != current_path:
if request.lang != cls._get_default_lang().code:
path = '/' + request.lang + path
if request.lang != cls._get_default_lang():
path = '/' + request.lang.url_code + path
if request.httprequest.query_string:
path += '?' + request.httprequest.query_string.decode('utf-8')
return werkzeug.utils.redirect(path, code=301)
@@ -0,0 +1,24 @@
<?xml version="1.0"?>
<odoo>
<record id="res_lang_form_inherit_model" model="ir.ui.view">
<field name="name">res.lang.form.http_routing.inherit</field>
<field name="model">res.lang</field>
<field name="inherit_id" ref="base.res_lang_form"/>
<field name="arch" type="xml">
<field name="url_code" position="attributes">
<attribute name="invisible">0</attribute>
</field>
</field>
</record>
<record id="res_lang_tree_inherit_model" model="ir.ui.view">
<field name="name">res.lang.tree.model.inherit</field>
<field name="model">res.lang</field>
<field name="inherit_id" ref="base.res_lang_tree"/>
<field name="arch" type="xml">
<field name="url_code" position="attributes">
<attribute name="invisible">0</attribute>
</field>
</field>
</record>
</odoo>
+3 -1
View File
@@ -762,7 +762,9 @@ class WebClient(http.Controller):
# For performance reasons we only load a single translation, so for
# sub-languages (that should only be partially translated) we load the
# main language PO instead - that should be enough for the login screen.
lang = request.lang.split('_')[0]
context = dict(request.context)
request.session._fix_lang(context)
lang = context['lang'].split('_')[0]
translations_per_module = {}
for addon_name in mods:
+5 -1
View File
@@ -107,10 +107,14 @@ class Website(Home):
# Business
# ------------------------------------------------------
@http.route('/website/get_languages', type='json', auth="user", website=True)
def website_languages(self, **kwargs):
return [(lg.code, lg.url_code, lg.name) for lg in request.website.language_ids]
@http.route('/website/lang/<lang>', type='http', auth="public", website=True, multilang=False)
def change_lang(self, lang, r='/', **kwargs):
if lang == 'default':
lang = request.website.default_lang_code
lang = request.website.default_lang_id.url_code
r = '/%s%s' % (lang, r or '/')
redirect = werkzeug.utils.redirect(r or ('/%s' % lang), 303)
redirect.set_cookie('frontend_lang', lang)
+2 -14
View File
@@ -15,7 +15,7 @@ import odoo
from odoo import api, models, registry
from odoo import SUPERUSER_ID
from odoo.http import request
from odoo.tools import config
from odoo.tools import config, ormcache
from odoo.tools.safe_eval import safe_eval
from odoo.osv.expression import FALSE_DOMAIN, OR
@@ -132,18 +132,6 @@ class Http(models.AbstractModel):
if request.routing_iteration == 1:
request.website = request.website.with_context(request.context)
@classmethod
def _get_languages(cls):
if getattr(request, 'website', False):
return request.website.language_ids
return super(Http, cls)._get_languages()
@classmethod
def _get_language_codes(cls):
if getattr(request, 'website', False):
return request.website._get_languages()
return super(Http, cls)._get_language_codes()
@classmethod
def _get_default_lang(cls):
if getattr(request, 'website', False):
@@ -205,7 +193,7 @@ class Http(models.AbstractModel):
@classmethod
def _handle_exception(cls, exception):
code = 500 # default code
is_website_request = bool(getattr(request, 'is_frontend', False) and getattr(request, 'website', False))
is_website_request = bool(getattr(request, 'is_frontend', False) and get_request_website())
if not is_website_request:
# Don't touch non website requests exception handling
return super(Http, cls)._handle_exception(exception)
+3 -2
View File
@@ -4,9 +4,10 @@ import re
from collections import OrderedDict
from odoo import models
from odoo.addons.http_routing.models.ir_http import url_for
from odoo.http import request
from odoo.addons.base.models.assetsbundle import AssetsBundle
from odoo.addons.http_routing.models.ir_http import url_for
from odoo.addons.website.models import ir_http
from odoo.tools import html_escape as escape
re_background_image = re.compile(r"(background-image\s*:\s*url\(\s*['\"]?\s*)([^)'\"]+)")
@@ -47,7 +48,7 @@ class QWeb(models.AbstractModel):
options.get('edit_translations') or options.get('debug') or (request and request.session.debug):
return atts
website = request and getattr(request, 'website', None)
website = ir_http.get_request_website()
if not website and options.get('website_id'):
website = self.env['website'].browse(options['website_id'])
+3 -3
View File
@@ -302,7 +302,7 @@ class View(models.Model):
if request and getattr(request, 'is_frontend', False):
editable = request.website.is_publisher()
translatable = editable and self._context.get('lang') != request.website.default_lang_code
translatable = editable and self._context.get('lang') != request.website.default_lang_id.code
editable = not translatable and editable
# in edit mode ir.ui.view will tag nodes
@@ -355,7 +355,7 @@ class View(models.Model):
url_for=url_for,
res_company=request.website.company_id.sudo(),
default_lang_code=request.env['ir.http']._get_default_lang().code,
languages=request.env['ir.http']._get_language_codes(),
languages=request.env['res.lang'].get_available(),
translatable=translatable,
editable=editable,
menu_data=self.env['ir.ui.menu'].load_menus_root() if request.website.is_user() else None,
@@ -367,7 +367,7 @@ class View(models.Model):
def get_default_lang_code(self):
website_id = self.env.context.get('website_id')
if website_id:
lang_code = self.env['website'].browse(website_id).default_lang_code
lang_code = self.env['website'].browse(website_id).default_lang_id.code
return lang_code
else:
return super(View, self).get_default_lang_code()
+4 -8
View File
@@ -19,15 +19,11 @@ class ResConfigSettings(models.TransientModel):
website_country_group_ids = fields.Many2many(related='website_id.country_group_ids', readonly=False)
website_company_id = fields.Many2one(related='website_id.company_id', string='Website Company', readonly=False)
website_logo = fields.Binary(related='website_id.logo', readonly=False)
language_ids = fields.Many2many(related='website_id.language_ids', relation='res.lang',
readonly=False)
language_ids = fields.Many2many(related='website_id.language_ids', relation='res.lang', readonly=False)
website_language_count = fields.Integer(string='Number of languages', compute='_compute_website_language_count', readonly=True)
website_default_lang_id = fields.Many2one(
string='Default language', related='website_id.default_lang_id', readonly=False,
relation='res.lang')
website_default_lang_code = fields.Char(
'Default language code', related='website_id.default_lang_code', readonly=False,
)
website_default_lang_id = fields.Many2one(string='Default language', related='website_id.default_lang_id',
readonly=False, relation='res.lang')
website_default_lang_code = fields.Char('Default language code', related='website_id.default_lang_id.code', readonly=False)
specific_user_account = fields.Boolean(related='website_id.specific_user_account', readonly=False,
help='Are newly created user accounts website specific')
+12 -1
View File
@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, _
from odoo import api, models, tools, _
from odoo.addons.website.models import ir_http
from odoo.exceptions import UserError
from odoo.http import request
class Lang(models.Model):
@@ -13,3 +15,12 @@ class Lang(models.Model):
if self.env['website'].search([('language_ids', 'in', self._ids)]):
raise UserError(_("Cannot deactivate a language that is currently used on a website."))
return super(Lang, self).write(vals)
@api.model
@tools.ormcache_context(keys=("website_id",))
def get_available(self):
""" Return the available languages as a list of (code, name) sorted by name. """
website = ir_http.get_request_website()
if website:
return sorted([(lang.code, lang.url_code, lang.name) for lang in request.website.language_ids])
return super(Lang, self).get_available()
+7 -15
View File
@@ -56,7 +56,6 @@ class Website(models.Model):
company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.company, required=True)
language_ids = fields.Many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages', default=_active_languages)
default_lang_id = fields.Many2one('res.lang', string="Default Language", default=_default_language, required=True)
default_lang_code = fields.Char("Default language code", related='default_lang_id.code', store=True, readonly=False)
auto_redirect_lang = fields.Boolean('Autoredirect Language', default=True, help="Should users be redirected to their browser's language")
def _default_social_facebook(self):
@@ -156,7 +155,8 @@ class Website(models.Model):
public_user_to_change_websites = self.env['website']
self._handle_favicon(values)
self._get_languages.clear_cache(self)
self.clear_caches()
if 'company_id' in values and 'user_id' not in values:
public_user_to_change_websites = self.filtered(lambda w: w.sudo().user_id.company_id.id != values['company_id'])
if public_user_to_change_websites:
@@ -437,19 +437,11 @@ class Website(models.Model):
# Languages
# ----------------------------------------------------------
def get_languages(self):
self.ensure_one()
return self._get_languages()
@tools.cache('self.id')
def _get_languages(self):
return [(lg.code, lg.name) for lg in self.language_ids]
def get_alternate_languages(self, req=None):
langs = []
if req is None:
req = request.httprequest
default = self.get_current_website().default_lang_code
default = self.get_current_website().default_lang_id.url_code
shorts = []
def get_url_localized(router, lang):
@@ -460,11 +452,11 @@ class Website(models.Model):
return router.build(request.endpoint, arguments)
router = request.httprequest.app.get_db_router(request.db).bind('')
for code, dummy in self.get_languages():
lg_path = ('/' + code) if code != default else ''
lg_codes = code.split('_')
for lg in self.language_ids:
lg_path = ('/' + lg.url_code) if lg.url_code != default else ''
lg_codes = lg.code.split('_')
shorts.append(lg_codes[0])
uri = get_url_localized(router, code) if request.endpoint else request.httprequest.path
uri = get_url_localized(router, lg.url_code) if request.endpoint else request.httprequest.path
if req.query_string:
uri += u'?' + req.query_string.decode('utf-8')
lang = {
+1 -2
View File
@@ -122,10 +122,9 @@ class WebsiteVisitor(models.Model):
def _create_visitor(self, website_page_id=False):
country_code = request.session.get('geoip', {}).get('country_code', False)
country_id = request.env['res.country'].sudo().search([('code', '=', country_code)], limit=1).id if country_code else False
lang_id = request.env['res.lang'].sudo().search([('code', '=', request.lang)], limit=1).id
vals = {
'last_connection_datetime': datetime.now(),
'lang_id': lang_id,
'lang_id': request.lang.id,
'country_id': country_id,
'website_id': request.website.id
}
@@ -105,7 +105,7 @@ var WebsiteRoot = publicRootData.PublicRoot.extend({
var $target = $(ev.target);
// retrieve the hash before the redirect
var redirect = {
lang: $target.data('lang'),
lang: $target.data('url_code'),
url: encodeURIComponent($target.attr('href').replace(/[&?]edit_translations[^&?]+/, '')),
hash: encodeURIComponent(window.location.hash)
};
+1 -3
View File
@@ -497,9 +497,7 @@ var MetaKeywords = Widget.extend({
},
});
this._rpc({
model: 'website',
method: 'get_languages',
args: [[context.website_id]],
route: '/website/get_languages',
}).then(function (data) {
self.$('#language-box').html(core.qweb.render('Configurator.language_promote', {
'language': data,
@@ -46,14 +46,11 @@ var TranslatePageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
* @returns {Promise}
*/
_goToMasterPage: function () {
var lang = '/' + utils.get_cookie('frontend_lang');
var current = document.createElement('a');
current.href = window.location.toString();
current.search += (current.search ? '&' : '?') + 'enable_editor=1';
if (current.pathname.indexOf(lang) === 0) {
current.pathname = current.pathname.replace(lang, '');
}
// we are in translate mode, the pathname starts with '/<url_code/'
current.pathname = current.pathname.substr(current.pathname.indexOf('/', 1));
var link = document.createElement('a');
link.href = '/website/lang/default';
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="Configurator.language_promote">
<t t-foreach="language" t-as="lang">
<option t-att-value="lang[0]" t-att-selected="lang[0] == def_lang ? 'selected' : null"><t t-esc="lang[1]" /></option>
<option t-att-value="lang[0]" t-att-selected="lang[0] == def_lang ? 'selected' : null"><t t-esc="lang[2]" /></option>
</t>
</t>
+4 -4
View File
@@ -32,7 +32,7 @@ tour.register('rte_translator', {
timeout: 60000,
}, {
content: "go to english version",
trigger: '.js_language_selector a[data-lang="en_US"]',
trigger: '.js_language_selector a[data-url_code="en"]',
extra_trigger: 'html[lang*="fr"]',
}, {
content: "Open new page menu",
@@ -73,7 +73,7 @@ tour.register('rte_translator', {
extra_trigger: 'body:not(.o_wait_reload):not(:has(.note-editor))',
}, {
content: "click on french version",
trigger: '.js_language_selector a[data-lang="fr_BE"]',
trigger: '.js_language_selector a[data-url_code="fr_BE"]',
extra_trigger: 'html[lang*="en"]:not(:has(button[data-action=save]))',
}, {
content: "translate",
@@ -137,7 +137,7 @@ tour.register('rte_translator', {
extra_trigger: 'html[lang*="fr"]:not(:has(#wrap p span))',
}, {
content: "return to english version",
trigger: '.js_language_selector a[data-lang="en_US"]',
trigger: '.js_language_selector a[data-url_code="en"]',
}, {
content: "edit english version",
trigger: 'a[data-action=edit]',
@@ -170,7 +170,7 @@ tour.register('rte_translator', {
trigger : '.js_language_selector .dropdown-toggle',
}, {
content: "return in french",
trigger : 'html[lang="en-US"] .js_language_selector .js_change_lang[data-lang="fr_BE"]',
trigger : 'html[lang="en-US"] .js_language_selector .js_change_lang[data-url_code="fr_BE"]',
}, {
content: "check bis: content is translated",
trigger: '#wrap p font:first:contains(translated french text)',
+6 -5
View File
@@ -1,15 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_converter
from . import test_base_url
from . import test_get_current_website
from . import test_qweb
from . import test_converter
from . import test_crawl
from . import test_ui
from . import test_views
from . import test_get_current_website
from . import test_lang_url
from . import test_menu
from . import test_page
from . import test_qweb
from . import test_ui
from . import test_views
from . import test_website_favicon
from . import test_website_reset_password
from . import test_website_visitor
+34
View File
@@ -0,0 +1,34 @@
# -*- 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.website.tools import MockRequest
from odoo.tests import HttpCase, tagged
@tagged('-at_install', 'post_install')
class TestLangUrl(HttpCase):
def setUp(self):
super(TestLangUrl, self).setUp()
# Simulate multi lang without loading translations
self.website = self.env['website'].browse(1)
lang_fr = self.env.ref('base.lang_fr')
lang_fr.write({'active': True, 'url_code': 'fr'})
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):
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.")
def test_02_url_redirect(self):
url = '/fr_WHATEVER/contactus'
r = self.url_open(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(r.url.endswith('/fr/contactus'), "fr_WHATEVER should be forwarded to 'fr_FR' lang as closest match")
url = '/fr_FR/contactus'
r = self.url_open(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(r.url.endswith('/fr/contactus'), "lang in url should use url_code ('fr' in this case)")
+17 -17
View File
@@ -77,7 +77,7 @@ class TestQwebProcessAtt(TransactionCase):
self.website.default_lang_id = self.env.ref('base.lang_en')
self.website.cdn_activated = True
self.website.cdn_url = "http://test.cdn"
self.website.cdn_filters = "\n".join(["^(/[a-z]{2}_[A-Z]{2})?/a$", "^/b$"])
self.website.cdn_filters = "\n".join(["^(/[a-z]{2}_[A-Z]{2})?/a$", "^(/[a-z]{2})?/a$", "^/b$"])
def _test_att(self, url, expect, tag='a', attribute='href'):
self.assertEqual(
@@ -88,8 +88,8 @@ class TestQwebProcessAtt(TransactionCase):
def test_process_att_no_request(self):
# no request so no URL rewriting
self._test_att('/', {'href': '/'})
self._test_att('/en_US/', {'href': '/en_US/'})
self._test_att('/fr_FR/', {'href': '/fr_FR/'})
self._test_att('/en/', {'href': '/en/'})
self._test_att('/fr/', {'href': '/fr/'})
# no URL rewritting for CDN
self._test_att('/a', {'href': '/a'})
@@ -97,8 +97,8 @@ class TestQwebProcessAtt(TransactionCase):
with MockRequest(self.env):
# no website so URL rewriting
self._test_att('/', {'href': '/'})
self._test_att('/en_US/', {'href': '/en_US/'})
self._test_att('/fr_FR/', {'href': '/fr_FR/'})
self._test_att('/en/', {'href': '/en/'})
self._test_att('/fr/', {'href': '/fr/'})
# no URL rewritting for CDN
self._test_att('/a', {'href': '/a'})
@@ -106,37 +106,37 @@ class TestQwebProcessAtt(TransactionCase):
with MockRequest(self.env, website=self.website, multilang=False):
# lang not changed in URL but CDN enabled
self._test_att('/a', {'href': 'http://test.cdn/a'})
self._test_att('/en_US/a', {'href': 'http://test.cdn/en_US/a'})
self._test_att('/en/a', {'href': 'http://test.cdn/en/a'})
self._test_att('/b', {'href': 'http://test.cdn/b'})
self._test_att('/en_US/b', {'href': '/en_US/b'})
self._test_att('/en/b', {'href': '/en/b'})
def test_process_att_no_request_lang(self):
with MockRequest(self.env, website=self.website):
self._test_att('/', {'href': '/'})
self._test_att('/en_US/', {'href': '/'})
self._test_att('/fr_FR/', {'href': '/fr_FR/'})
self._test_att('/en/', {'href': '/'})
self._test_att('/fr/', {'href': '/fr/'})
def test_process_att_with_request_lang(self):
with MockRequest(self.env, website=self.website, context={'lang': 'fr_FR'}):
self._test_att('/', {'href': '/fr_FR/'})
self._test_att('/en_US/', {'href': '/'})
self._test_att('/fr_FR/', {'href': '/fr_FR/'})
self._test_att('/', {'href': '/fr/'})
self._test_att('/en/', {'href': '/'})
self._test_att('/fr/', {'href': '/fr/'})
def test_process_att_matching_cdn_and_lang(self):
with MockRequest(self.env, website=self.website):
# lang prefix is added before CDN
self._test_att('/a', {'href': 'http://test.cdn/a'})
self._test_att('/en_US/a', {'href': 'http://test.cdn/a'})
self._test_att('/fr_FR/a', {'href': 'http://test.cdn/fr_FR/a'})
self._test_att('/en/a', {'href': 'http://test.cdn/a'})
self._test_att('/fr/a', {'href': 'http://test.cdn/fr/a'})
self._test_att('/b', {'href': 'http://test.cdn/b'})
self._test_att('/en_US/b', {'href': 'http://test.cdn/b'})
self._test_att('/fr_FR/b', {'href': '/fr_FR/b'})
self._test_att('/en/b', {'href': 'http://test.cdn/b'})
self._test_att('/fr/b', {'href': '/fr/b'})
def test_process_att_no_route(self):
with MockRequest(self.env, website=self.website, context={'lang': 'fr_FR'}, routing=False):
# default on multilang=True if route is not /{module}/static/
self._test_att('/web/static/hi', {'href': '/web/static/hi'})
self._test_att('/my-page', {'href': '/fr_FR/my-page'})
self._test_att('/my-page', {'href': '/fr/my-page'})
def test_process_att_url_crap(self):
with MockRequest(self.env, website=self.website) as request:
+11 -1
View File
@@ -84,14 +84,24 @@ class MockRequest(object):
app.get_db_router = app.bind = app.match = app
if not kw.get('routing', True):
app.match = werkzeugRaiseNotFound
lang = kw.get('lang')
if not lang:
lang_code = kw.get('context', {}).get('lang', env.context.get('lang', 'en_US'))
lang = env['res.lang']._lang_get(lang_code)
context = kw.get('context', {})
context.setdefault('lang', lang_code)
self.request = DotDict({
'context': kw.get('context', {}),
'context': context,
'db': None,
'env': env,
'httprequest': {
'path': '/hello/',
'app': app,
},
'lang': lang,
'redirect': werkzeug.utils.redirect,
'session': {
'geoip': {
+4 -6
View File
@@ -227,7 +227,6 @@
</t>
</t>
<t t-set="languages" t-value="website.get_languages() if website else None"/>
<t t-if="request and request.is_frontend_multilang and website">
<t t-foreach="website.get_alternate_languages(request.httprequest)" t-as="lg">
<link rel="alternate" t-att-hreflang="lg['hreflang']" t-att-href="lg['href']"/>
@@ -395,16 +394,15 @@
<li class="list-inline-item">
<div class="dropup">
<button class="btn btn-sm btn-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span t-esc="list(filter(lambda lg : lg[0] == lang, languages))[0][1].split('/').pop()"/>
<span t-esc="list(filter(lambda lg : lg[0] == lang, languages))[0][2].split('/').pop()"/>
<span class="caret ml4"/>
</button>
<div class="dropdown-menu" role="menu">
<t t-foreach="languages" t-as="lg">
<a t-att-href="url_for(request.httprequest.path + '?' + keep_query(), lang=lg[0])"
<a t-att-href="url_for(request.httprequest.path + '?' + keep_query(), lang_code=lg[0])"
class="dropdown-item js_change_lang"
t-att-data-default-lang="(editable or translatable) and 'true' if website and lg[0] == website.default_lang_code else None"
t-att-data-lang="lg[0]">
<t t-esc="lg[1].split('/').pop()"/>
t-att-data-url_code="lg[1]">
<t t-esc="lg[2].split('/').pop()"/>
</a>
</t>
</div>
+1 -1
View File
@@ -649,7 +649,7 @@ class WebsiteSale(http.Controller):
if mode[0] == 'new':
new_values['company_id'] = request.website.company_id.id
lang = request.lang if request.lang in request.website.mapped('language_ids.code') else None
lang = request.lang.code if request.lang.code in request.website.mapped('language_ids.code') else None
if lang:
new_values['lang'] = lang
if mode == ('edit', 'billing') and order.partner_id.type == 'contact':
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo.http import request
from odoo.addons.website.models import ir_http
class ProductProduct(models.Model):
@@ -11,7 +11,7 @@ class ProductProduct(models.Model):
cart_qty = fields.Integer(compute='_compute_cart_qty')
def _compute_cart_qty(self):
website = request and getattr(request, 'website', None)
website = ir_http.get_request_website()
if not website:
self.cart_qty = 0
return
+10 -1
View File
@@ -28,6 +28,7 @@ class Lang(models.Model):
name = fields.Char(required=True)
code = fields.Char(string='Locale Code', required=True, help='This field is used to set/get locales for user')
iso_code = fields.Char(string='ISO code', help='This ISO code is the name of po files to use for translations')
url_code = fields.Char('URL Code', required=True, help='The Lang Code displayed in the URL')
active = fields.Boolean()
direction = fields.Selection([('ltr', 'Left-to-Right'), ('rtl', 'Right-to-Left')], required=True, default='ltr')
date_format = fields.Char(string='Date Format', required=True, default=DEFAULT_DATE_FORMAT)
@@ -50,6 +51,7 @@ class Lang(models.Model):
_sql_constraints = [
('name_uniq', 'unique(name)', 'The name of the language must be unique !'),
('code_uniq', 'unique(code)', 'The code of the language must be unique !'),
('url_code_uniq', 'unique(url_code)', 'The URL code of the language must be unique !'),
]
@api.constrains('active')
@@ -181,6 +183,10 @@ class Lang(models.Model):
def _lang_get_id(self, code):
return self.with_context(active_test=True).search([('code', '=', code)]).id
@tools.ormcache('url_code')
def _lang_get_code(self, url_code):
return self.with_context(active_test=True).search([('url_code', '=', url_code)]).code or url_code
def _lang_get(self, code):
""" Return the language using this code if it is active """
return self.browse(self._lang_get_id(code))
@@ -198,7 +204,7 @@ class Lang(models.Model):
def get_available(self):
""" Return the available languages as a list of (code, name) sorted by name. """
langs = self.with_context(active_test=False).search([])
return sorted([(lang.code, lang.name) for lang in langs], key=itemgetter(1))
return sorted([(lang.code, lang.url_code, lang.name) for lang in langs], key=itemgetter(2))
@api.model
@tools.ormcache()
@@ -218,6 +224,9 @@ class Lang(models.Model):
@api.model_create_multi
def create(self, vals_list):
self.clear_caches()
for vals in vals_list:
if not vals.get('url_code'):
vals['url_code'] = vals.get('iso_code') or vals['code']
return super(Lang, self).create(vals_list)
def write(self, vals):
@@ -9,6 +9,7 @@
<field name="name"/>
<field name="code" groups="base.group_no_one"/>
<field name="iso_code" groups="base.group_no_one"/>
<field name="url_code" groups="base.group_no_one" invisible="1"/>
<field name="direction" groups="base.group_no_one"/>
<field name="active"/>
<button name="%(base.action_view_base_language_install)d"
@@ -40,6 +41,7 @@
<group>
<field name="code"/>
<field name="iso_code"/>
<field name="url_code" invisible="1" required="0"/>
<field name="active"/>
</group>
<group>
@@ -20,7 +20,7 @@ class BaseLanguageInstall(models.TransientModel):
@api.model
def _get_languages(self):
return self.env['res.lang'].get_available()
return [[code, name] for code, _, name in self.env['res.lang'].get_available()]
lang = fields.Selection(_get_languages, string='Language', required=True,
default=_default_language)
-8
View File
@@ -84,7 +84,6 @@ ALLOWED_DEBUG_MODES = ['', '1', 'assets', 'tests']
#----------------------------------------------------------
# Thread local global request object
_request_stack = werkzeug.local.LocalStack()
request = _request_stack()
"""
A global proxy that always redirect to the current request object.
@@ -269,13 +268,6 @@ class WebRequest(object):
self._env = odoo.api.Environment(self.cr, self.uid, self.context)
return self._env
@lazy_property
def lang(self):
context = dict(self.context)
self.session._fix_lang(context)
self.context = context
return context["lang"]
@lazy_property
def session(self):
""" :class:`OpenERPSession` holding the HTTP session data for the