summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRomain Derie <rde@odoo.com>2019-03-26 05:56:55 +0000
committerRomain Derie <rde@odoo.com>2019-08-26 16:35:19 +0000
commit269aa594111a152ad4b7714856ea745bfef57155 (patch)
treed5f8f7e86209102d831b2e222038d45bd7fe7626
parentf10f120de3017bc8d0e9a5ac72eeec1280374a1d (diff)
[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>
-rw-r--r--addons/auth_signup/controllers/main.py6
-rw-r--r--addons/http_routing/__manifest__.py1
-rw-r--r--addons/http_routing/models/ir_http.py121
-rw-r--r--addons/http_routing/models/res_lang.py0
-rw-r--r--addons/http_routing/views/res_lang_views.xml24
-rw-r--r--addons/web/controllers/main.py4
-rw-r--r--addons/website/controllers/main.py6
-rw-r--r--addons/website/models/ir_http.py16
-rw-r--r--addons/website/models/ir_qweb.py5
-rw-r--r--addons/website/models/ir_ui_view.py6
-rw-r--r--addons/website/models/res_config_settings.py12
-rw-r--r--addons/website/models/res_lang.py13
-rw-r--r--addons/website/models/website.py22
-rw-r--r--addons/website/models/website_visitor.py3
-rw-r--r--addons/website/static/src/js/content/website_root.js2
-rw-r--r--addons/website/static/src/js/menu/seo.js4
-rw-r--r--addons/website/static/src/js/menu/translate.js7
-rw-r--r--addons/website/static/src/xml/website.seo.xml2
-rw-r--r--addons/website/static/tests/tours/rte.js8
-rw-r--r--addons/website/tests/__init__.py9
-rw-r--r--addons/website/tests/test_lang_url.py34
-rw-r--r--addons/website/tests/test_qweb.py34
-rw-r--r--addons/website/tools.py12
-rw-r--r--addons/website/views/website_templates.xml10
-rw-r--r--addons/website_sale/controllers/main.py2
-rw-r--r--addons/website_sale_stock/models/product_product.py4
-rw-r--r--odoo/addons/base/models/res_lang.py11
-rw-r--r--odoo/addons/base/views/res_lang_views.xml2
-rw-r--r--odoo/addons/base/wizard/base_language_install.py2
-rw-r--r--odoo/http.py8
30 files changed, 238 insertions, 152 deletions
diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py
index 0618cd367f3..6f21118e369 100644
--- a/addons/auth_signup/controllers/main.py
+++ b/addons/auth_signup/controllers/main.py
@@ -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()
diff --git a/addons/http_routing/__manifest__.py b/addons/http_routing/__manifest__.py
index 6305f25310a..f4636332f45 100644
--- a/addons/http_routing/__manifest__.py
+++ b/addons/http_routing/__manifest__.py
@@ -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'],
}
diff --git a/addons/http_routing/models/ir_http.py b/addons/http_routing/models/ir_http.py
index d6a4524eb31..c9d08f3c754 100644
--- a/addons/http_routing/models/ir_http.py
+++ b/addons/http_routing/models/ir_http.py
@@ -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)
-
- 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()]
+ 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
- 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:
@@ -223,10 +240,6 @@ class IrHttp(models.AbstractModel):
)
@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')
if lang_code:
@@ -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()
+
+ 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')
- 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)
+ 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 = context['lang'] = nearest_lang or 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)
diff --git a/addons/http_routing/models/res_lang.py b/addons/http_routing/models/res_lang.py
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/addons/http_routing/models/res_lang.py
diff --git a/addons/http_routing/views/res_lang_views.xml b/addons/http_routing/views/res_lang_views.xml
new file mode 100644
index 00000000000..96d03217f69
--- /dev/null
+++ b/addons/http_routing/views/res_lang_views.xml
@@ -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>
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 591849a8bc0..dfe575b8f6f 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -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:
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index a1cca57919f..f91870adfaf 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -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)
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index 58c15a8128c..ecacdc62bac 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -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
@@ -133,18 +133,6 @@ class Http(models.AbstractModel):
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):
return request.website.default_lang_id
@@ -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)
diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py
index 0e93876be45..d3483fcb1b4 100644
--- a/addons/website/models/ir_qweb.py
+++ b/addons/website/models/ir_qweb.py
@@ -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'])
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index 6949f173a46..6b1efe5a717 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -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()
diff --git a/addons/website/models/res_config_settings.py b/addons/website/models/res_config_settings.py
index 6bcae68b61f..0fff2f6eb3b 100644
--- a/addons/website/models/res_config_settings.py
+++ b/addons/website/models/res_config_settings.py
@@ -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')
diff --git a/addons/website/models/res_lang.py b/addons/website/models/res_lang.py
index c84a212ed5a..9fc3f69e998 100644
--- a/addons/website/models/res_lang.py
+++ b/addons/website/models/res_lang.py
@@ -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()
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index 8ce3d46568a..29398c35872 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -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 = {
diff --git a/addons/website/models/website_visitor.py b/addons/website/models/website_visitor.py
index 9b262c26031..307ba256214 100644
--- a/addons/website/models/website_visitor.py
+++ b/addons/website/models/website_visitor.py
@@ -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
}
diff --git a/addons/website/static/src/js/content/website_root.js b/addons/website/static/src/js/content/website_root.js
index 4b61a33d779..a8e208f1408 100644
--- a/addons/website/static/src/js/content/website_root.js
+++ b/addons/website/static/src/js/content/website_root.js
@@ -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)
};
diff --git a/addons/website/static/src/js/menu/seo.js b/addons/website/static/src/js/menu/seo.js
index 3844a1d4731..2ac6d7fcded 100644
--- a/addons/website/static/src/js/menu/seo.js
+++ b/addons/website/static/src/js/menu/seo.js
@@ -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,
diff --git a/addons/website/static/src/js/menu/translate.js b/addons/website/static/src/js/menu/translate.js
index 043a900fff6..cab37bf3473 100644
--- a/addons/website/static/src/js/menu/translate.js
+++ b/addons/website/static/src/js/menu/translate.js
@@ -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';
diff --git a/addons/website/static/src/xml/website.seo.xml b/addons/website/static/src/xml/website.seo.xml
index e3c442d8f32..6bafe769f16 100644
--- a/addons/website/static/src/xml/website.seo.xml
+++ b/addons/website/static/src/xml/website.seo.xml
@@ -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>
diff --git a/addons/website/static/tests/tours/rte.js b/addons/website/static/tests/tours/rte.js
index a57b282bd09..a5cd53eaf61 100644
--- a/addons/website/static/tests/tours/rte.js
+++ b/addons/website/static/tests/tours/rte.js
@@ -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)',
diff --git a/addons/website/tests/__init__.py b/addons/website/tests/__init__.py
index dc24bd6e22a..9bb3efd60de 100644
--- a/addons/website/tests/__init__.py
+++ b/addons/website/tests/__init__.py
@@ -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_converter
+from . import test_crawl
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_crawl
from . import test_ui
from . import test_views
-from . import test_menu
-from . import test_page
from . import test_website_favicon
from . import test_website_reset_password
from . import test_website_visitor
diff --git a/addons/website/tests/test_lang_url.py b/addons/website/tests/test_lang_url.py
new file mode 100644
index 00000000000..f4998168074
--- /dev/null
+++ b/addons/website/tests/test_lang_url.py
@@ -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)")
diff --git a/addons/website/tests/test_qweb.py b/addons/website/tests/test_qweb.py
index 9386c9f81e3..9cc199291a9 100644
--- a/addons/website/tests/test_qweb.py
+++ b/addons/website/tests/test_qweb.py
@@ -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:
diff --git a/addons/website/tools.py b/addons/website/tools.py
index e3bb897ec52..e7058d65541 100644
--- a/addons/website/tools.py
+++ b/addons/website/tools.py
@@ -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': {
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index 312d1ce701c..b89cb0721b4 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -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>
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index 97a9e11169e..9412ddce2be 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -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':
diff --git a/addons/website_sale_stock/models/product_product.py b/addons/website_sale_stock/models/product_product.py
index 07a873fa1a7..8c33b40d895 100644
--- a/addons/website_sale_stock/models/product_product.py
+++ b/addons/website_sale_stock/models/product_product.py
@@ -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
diff --git a/odoo/addons/base/models/res_lang.py b/odoo/addons/base/models/res_lang.py
index 22c6221b5c2..eccb6b09c93 100644
--- a/odoo/addons/base/models/res_lang.py
+++ b/odoo/addons/base/models/res_lang.py
@@ -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):
diff --git a/odoo/addons/base/views/res_lang_views.xml b/odoo/addons/base/views/res_lang_views.xml
index 2f7b4990e90..483f2c1879f 100644
--- a/odoo/addons/base/views/res_lang_views.xml
+++ b/odoo/addons/base/views/res_lang_views.xml
@@ -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>
diff --git a/odoo/addons/base/wizard/base_language_install.py b/odoo/addons/base/wizard/base_language_install.py
index 3a41302b40d..3d5c804af34 100644
--- a/odoo/addons/base/wizard/base_language_install.py
+++ b/odoo/addons/base/wizard/base_language_install.py
@@ -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)
diff --git a/odoo/http.py b/odoo/http.py
index 25200ebcecf..146d79f6aa6 100644
--- a/odoo/http.py
+++ b/odoo/http.py
@@ -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.
@@ -270,13 +269,6 @@ class WebRequest(object):
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
current http session