diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-09-13 18:07:59 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-09-13 18:07:59 +0200 |
| commit | 6c948c36bbf72dd896e0bff03aa7fbddadcbb0cd (patch) | |
| tree | f8620905c011735d8a3c340c46d1417a54a75972 /addons/website/models | |
| parent | 48503fcf08eac6fed12e080d4f501f9bbc8d257d (diff) | |
| parent | 197474e5b9ad43ab2d587a98136a1a809ce9a2e7 (diff) | |
[MERGE] forward port branch saas-12.5 up to 197474e5b9a
Diffstat (limited to 'addons/website/models')
| -rw-r--r-- | addons/website/models/ir_http.py | 16 | ||||
| -rw-r--r-- | addons/website/models/ir_qweb.py | 5 | ||||
| -rw-r--r-- | addons/website/models/ir_ui_view.py | 31 | ||||
| -rw-r--r-- | addons/website/models/res_config_settings.py | 12 | ||||
| -rw-r--r-- | addons/website/models/res_lang.py | 13 | ||||
| -rw-r--r-- | addons/website/models/website.py | 117 | ||||
| -rw-r--r-- | addons/website/models/website_menu.py | 50 | ||||
| -rw-r--r-- | addons/website/models/website_visitor.py | 8 |
8 files changed, 156 insertions, 96 deletions
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..a3aca36ced5 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 @@ -336,18 +336,19 @@ class View(models.Model): qcontext['main_object'] = self cur = Website.get_current_website() - qcontext['multi_website_websites_current'] = {'website_id': cur.id, 'name': cur.name, 'domain': cur.domain} - qcontext['multi_website_websites'] = [ - {'website_id': website.id, 'name': website.name, 'domain': website.domain} - for website in Website.search([]) if website != cur - ] - - cur_company = self.env.company - qcontext['multi_website_companies_current'] = {'company_id': cur_company.id, 'name': cur_company.name} - qcontext['multi_website_companies'] = [ - {'company_id': comp.id, 'name': comp.name} - for comp in self.env.user.company_ids if comp != cur_company - ] + if self.env.user.has_group('website.group_website_publisher') and self.env.user.has_group('website.group_multi_website'): + qcontext['multi_website_websites_current'] = {'website_id': cur.id, 'name': cur.name, 'domain': cur.domain} + qcontext['multi_website_websites'] = [ + {'website_id': website.id, 'name': website.name, 'domain': website.domain} + for website in Website.search([]) if website != cur + ] + + cur_company = self.env.company + qcontext['multi_website_companies_current'] = {'company_id': cur_company.id, 'name': cur_company.name} + qcontext['multi_website_companies'] = [ + {'company_id': comp.id, 'name': comp.name} + for comp in self.env.user.company_ids if comp != cur_company + ] qcontext.update(dict( self._context.copy(), @@ -355,7 +356,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 +368,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..0ddca6810e2 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -7,7 +7,9 @@ import logging import hashlib import re + from werkzeug import urls +from werkzeug.datastructures import OrderedMultiDict from werkzeug.exceptions import NotFound from odoo import api, fields, models, tools @@ -56,7 +58,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 +157,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,45 +439,42 @@ class Website(models.Model): # Languages # ---------------------------------------------------------- - def get_languages(self): + def _get_alternate_languages(self, canonical_params): 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] + if not self._is_canonical_url(canonical_params=canonical_params): + # no hreflang on non-canonical pages + return [] + + languages = self.language_ids + if len(languages) <= 1: + # no hreflang if no alternate language + return [] - def get_alternate_languages(self, req=None): langs = [] - if req is None: - req = request.httprequest - default = self.get_current_website().default_lang_code shorts = [] - def get_url_localized(router, lang): - arguments = dict(request.endpoint_arguments) - for key, val in list(arguments.items()): - if isinstance(val, models.BaseModel): - arguments[key] = val.with_context(lang=lang) - 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('_') - shorts.append(lg_codes[0]) - uri = get_url_localized(router, code) if request.endpoint else request.httprequest.path - if req.query_string: - uri += u'?' + req.query_string.decode('utf-8') - lang = { + for lg in languages: + lg_codes = lg.code.split('_') + short = lg_codes[0] + shorts.append(short) + langs.append({ 'hreflang': ('-'.join(lg_codes)).lower(), - 'short': lg_codes[0], - 'href': req.url_root[0:-1] + lg_path + uri, - } - langs.append(lang) + 'short': short, + 'href': self._get_canonical_url_localized(lang=lg, canonical_params=canonical_params), + }) + + # if there is only one region for a language, use only the language code for lang in langs: if shorts.count(lang['short']) == 1: lang['hreflang'] = lang['short'] + + # add the default + langs.append({ + 'hreflang': 'x-default', + 'href': self._get_canonical_url_localized(lang=self.default_lang_id, canonical_params=canonical_params), + }) + return langs # ---------------------------------------------------------- @@ -741,9 +740,6 @@ class Website(models.Model): domain_part, url = rule.build(value, append_unknown=False) if not query_string or query_string.lower() in url.lower(): page = {'loc': url} - for key, val in value.items(): - if key.startswith('__'): - page[key[2:]] = val if url in ('/sitemap.xml',): continue if url in url_set: @@ -767,9 +763,9 @@ class Website(models.Model): for page in pages: record = {'loc': page['url'], 'id': page['id'], 'name': page['name']} if page.view_id and page.view_id.priority != 16: - record['__priority'] = min(round(page.view_id.priority / 32.0, 1), 1) + record['priority'] = min(round(page.view_id.priority / 32.0, 1), 1) if page['write_date']: - record['__lastmod'] = page['write_date'].date() + record['lastmod'] = page['write_date'].date() yield record def get_website_pages(self, domain=[], order='name', limit=None): @@ -835,6 +831,55 @@ class Website(models.Model): self.ensure_one() return self._get_http_domain() or super(BaseModel, self).get_base_url() + def _get_canonical_url_localized(self, lang, canonical_params): + """Returns the canonical URL for the current request with translatable + elements appropriately translated in `lang`. + + If `request.endpoint` is not true, returns the current `path` instead. + + `url_quote_plus` is applied on the returned path. + """ + self.ensure_one() + if request.endpoint: + router = request.httprequest.app.get_db_router(request.db).bind('') + arguments = dict(request.endpoint_arguments) + for key, val in list(arguments.items()): + if isinstance(val, models.BaseModel): + if val.env.context.get('lang') != lang.url_code: + arguments[key] = val.with_context(lang=lang.url_code) + path = router.build(request.endpoint, arguments) + else: + # The build method returns a quoted URL so convert in this case for consistency. + path = urls.url_quote_plus(request.httprequest.path, safe='/') + lang_path = ('/' + lang.url_code) if lang != self.default_lang_id else '' + canonical_query_string = '?%s' % urls.url_encode(canonical_params) if canonical_params else '' + return self.get_base_url() + lang_path + path + canonical_query_string + + def _get_canonical_url(self, canonical_params): + """Returns the canonical URL for the current request.""" + self.ensure_one() + return self._get_canonical_url_localized(lang=request.lang, canonical_params=canonical_params) + + def _is_canonical_url(self, canonical_params): + """Returns whether the current request URL is canonical.""" + self.ensure_one() + # Compare OrderedMultiDict because the order is important, there must be + # only one canonical and not params permutations. + params = request.httprequest.args + canonical_params = canonical_params or OrderedMultiDict() + if params != canonical_params: + return False + # Compare URL at the first rerouting iteration (if available) because + # it's the one with the language in the path. + # It is important to also test the domain of the current URL. + current_url = request.httprequest.url_root[:-1] + (hasattr(request, 'rerouting') and request.rerouting[0] or request.httprequest.path) + canonical_url = self._get_canonical_url_localized(lang=request.lang, canonical_params=None) + # A request path with quotable characters (such as ",") is never + # canonical because request.httprequest.base_url is always unquoted, + # and canonical url is always quoted, so it is never possible to tell + # if the current URL is indeed canonical or not. + return current_url == canonical_url + class BaseModel(models.AbstractModel): _inherit = 'base' diff --git a/addons/website/models/website_menu.py b/addons/website/models/website_menu.py index e710979015f..195942ebc0d 100644 --- a/addons/website/models/website_menu.py +++ b/addons/website/models/website_menu.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models +from odoo.tools.translate import html_translate class Menu(models.Model): @@ -16,6 +17,20 @@ class Menu(models.Model): menu = self.search([], limit=1, order="sequence DESC") return menu.sequence or 0 + def _compute_field_is_mega_menu(self): + for menu in self: + menu.is_mega_menu = bool(menu.mega_menu_content) + + def _set_field_is_mega_menu(self): + for menu in self: + if menu.is_mega_menu: + if not menu.mega_menu_content: + default_content = self.env['ir.ui.view'].render_template('website.s_mega_menu_multi_menus') + menu.mega_menu_content = default_content.decode() + else: + menu.mega_menu_content = False + menu.mega_menu_classes = False + name = fields.Char('Menu', required=True, translate=True) url = fields.Char('Url', default='') page_id = fields.Many2one('website.page', 'Related Page', ondelete='cascade') @@ -28,6 +43,9 @@ class Menu(models.Model): is_visible = fields.Boolean(compute='_compute_visible', string='Is Visible') group_ids = fields.Many2many('res.groups', string='Visible Groups', help="User need to be at least in one of these groups to see the menu") + is_mega_menu = fields.Boolean(compute=_compute_field_is_mega_menu, inverse=_set_field_is_mega_menu) + mega_menu_content = fields.Html(translate=html_translate, sanitize=False) + mega_menu_classes = fields.Char() def name_get(self): if not self._context.get('display_website') and not self.env.user.has_group('website.group_multi_website'): @@ -108,25 +126,25 @@ class Menu(models.Model): @api.model def get_tree(self, website_id, menu_id=None): def make_tree(node): - page_id = node.page_id.id if node.page_id else None - is_homepage = page_id and self.env['website'].browse(website_id).homepage_id.id == page_id - menu_node = dict( - id=node.id, - name=node.name, - url=node.page_id.url if page_id else node.url, - new_window=node.new_window, - sequence=node.sequence, - parent_id=node.parent_id.id, - children=[], - is_homepage=is_homepage, - ) + is_homepage = bool(node.page_id and self.env['website'].browse(website_id).homepage_id.id == node.page_id.id) + menu_node = { + 'fields': { + 'id': node.id, + 'name': node.name, + 'url': node.page_id.url if node.page_id else node.url, + 'new_window': node.new_window, + 'is_mega_menu': node.is_mega_menu, + 'sequence': node.sequence, + 'parent_id': node.parent_id.id, + }, + 'children': [], + 'is_homepage': is_homepage, + } for child in node.child_id: menu_node['children'].append(make_tree(child)) return menu_node - if menu_id: - menu = self.browse(menu_id) - else: - menu = self.env['website'].browse(website_id).menu_id + + menu = menu_id and self.browse(menu_id) or self.env['website'].browse(website_id).menu_id return make_tree(menu) @api.model diff --git a/addons/website/models/website_visitor.py b/addons/website/models/website_visitor.py index 9b262c26031..7f7e50073b4 100644 --- a/addons/website/models/website_visitor.py +++ b/addons/website/models/website_visitor.py @@ -13,7 +13,7 @@ from odoo.http import request class WebsitVisitorPage(models.Model): _name = 'website.visitor.page' _description = 'Visited Pages' - _order = 'visit_datetime ASC' + _order = 'visit_datetime DESC' _log_access = False visitor_id = fields.Many2one('website.visitor', ondelete="cascade", index=True, required=True, readonly=True) @@ -106,7 +106,8 @@ class WebsiteVisitor(models.Model): # If visitor does not exist visitor_sudo = self._create_visitor(website_page.id) sign = visitor_sudo._get_visitor_sign().get(visitor_sudo.id) - response.set_cookie('visitor_id', sign) + expiration_date = datetime.now() + timedelta(days=100*365) # never expire + response.set_cookie('visitor_id', sign, expires=expiration_date.timestamp()) else: # Add page even if already in visitor_page_ids as checks on relations are done in many2many write method vals = { @@ -122,10 +123,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 } |
