[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:
@@ -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()
|
||||
|
||||
|
||||
@@ -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'],
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'])
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)")
|
||||
@@ -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
@@ -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': {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user