summaryrefslogtreecommitdiff
path: root/addons/website/models/website.py
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/website/models/website.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/website/models/website.py')
-rw-r--r--addons/website/models/website.py1053
1 files changed, 1053 insertions, 0 deletions
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
new file mode 100644
index 00000000..54719351
--- /dev/null
+++ b/addons/website/models/website.py
@@ -0,0 +1,1053 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import base64
+import inspect
+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
+from odoo.addons.http_routing.models.ir_http import slugify, _guess_mimetype, url_for
+from odoo.addons.website.models.ir_http import sitemap_qs2dom
+from odoo.addons.portal.controllers.portal import pager
+from odoo.exceptions import UserError
+from odoo.http import request
+from odoo.modules.module import get_resource_path
+from odoo.osv.expression import FALSE_DOMAIN
+from odoo.tools.translate import _
+
+logger = logging.getLogger(__name__)
+
+
+DEFAULT_CDN_FILTERS = [
+ "^/[^/]+/static/",
+ "^/web/(css|js)/",
+ "^/web/image",
+ "^/web/content",
+ # retrocompatibility
+ "^/website/image/",
+]
+
+
+class Website(models.Model):
+
+ _name = "website"
+ _description = "Website"
+
+ @api.model
+ def website_domain(self, website_id=False):
+ return [('website_id', 'in', (False, website_id or self.id))]
+
+ def _active_languages(self):
+ return self.env['res.lang'].search([]).ids
+
+ def _default_language(self):
+ lang_code = self.env['ir.default'].get('res.partner', 'lang')
+ def_lang_id = self.env['res.lang']._lang_get_id(lang_code)
+ return def_lang_id or self._active_languages()[0]
+
+ name = fields.Char('Website Name', required=True)
+ domain = fields.Char('Website Domain',
+ help='Will be prefixed by http in canonical URLs if no scheme is specified')
+ country_group_ids = fields.Many2many('res.country.group', 'website_country_group_rel', 'website_id', 'country_group_id',
+ string='Country Groups', help='Used when multiple websites have the same domain.')
+ 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)
+ auto_redirect_lang = fields.Boolean('Autoredirect Language', default=True, help="Should users be redirected to their browser's language")
+ cookies_bar = fields.Boolean('Cookies Bar', help="Display a customizable cookies bar on your website.")
+
+ def _default_social_facebook(self):
+ return self.env.ref('base.main_company').social_facebook
+
+ def _default_social_github(self):
+ return self.env.ref('base.main_company').social_github
+
+ def _default_social_linkedin(self):
+ return self.env.ref('base.main_company').social_linkedin
+
+ def _default_social_youtube(self):
+ return self.env.ref('base.main_company').social_youtube
+
+ def _default_social_instagram(self):
+ return self.env.ref('base.main_company').social_instagram
+
+ def _default_social_twitter(self):
+ return self.env.ref('base.main_company').social_twitter
+
+ def _default_logo(self):
+ image_path = get_resource_path('website', 'static/src/img', 'website_logo.png')
+ with tools.file_open(image_path, 'rb') as f:
+ return base64.b64encode(f.read())
+
+ logo = fields.Binary('Website Logo', default=_default_logo, help="Display this logo on the website.")
+ social_twitter = fields.Char('Twitter Account', default=_default_social_twitter)
+ social_facebook = fields.Char('Facebook Account', default=_default_social_facebook)
+ social_github = fields.Char('GitHub Account', default=_default_social_github)
+ social_linkedin = fields.Char('LinkedIn Account', default=_default_social_linkedin)
+ social_youtube = fields.Char('Youtube Account', default=_default_social_youtube)
+ social_instagram = fields.Char('Instagram Account', default=_default_social_instagram)
+ social_default_image = fields.Binary(string="Default Social Share Image", help="If set, replaces the website logo as the default social share image.")
+ has_social_default_image = fields.Boolean(compute='_compute_has_social_default_image', store=True)
+
+ google_analytics_key = fields.Char('Google Analytics Key')
+ google_management_client_id = fields.Char('Google Client ID')
+ google_management_client_secret = fields.Char('Google Client Secret')
+ google_search_console = fields.Char(help='Google key, or Enable to access first reply')
+
+ google_maps_api_key = fields.Char('Google Maps API Key')
+
+ user_id = fields.Many2one('res.users', string='Public User', required=True)
+ cdn_activated = fields.Boolean('Content Delivery Network (CDN)')
+ cdn_url = fields.Char('CDN Base URL', default='')
+ cdn_filters = fields.Text('CDN Filters', default=lambda s: '\n'.join(DEFAULT_CDN_FILTERS), help="URL matching those filters will be rewritten using the CDN Base URL")
+ partner_id = fields.Many2one(related='user_id.partner_id', string='Public Partner', readonly=False)
+ menu_id = fields.Many2one('website.menu', compute='_compute_menu', string='Main Menu')
+ homepage_id = fields.Many2one('website.page', string='Homepage')
+ custom_code_head = fields.Text('Custom <head> code')
+ custom_code_footer = fields.Text('Custom end of <body> code')
+
+ robots_txt = fields.Text('Robots.txt', translate=False, groups='website.group_website_designer')
+
+ def _default_favicon(self):
+ img_path = get_resource_path('web', 'static/src/img/favicon.ico')
+ with tools.file_open(img_path, 'rb') as f:
+ return base64.b64encode(f.read())
+
+ favicon = fields.Binary(string="Website Favicon", help="This field holds the image used to display a favicon on the website.", default=_default_favicon)
+ theme_id = fields.Many2one('ir.module.module', help='Installed theme')
+
+ specific_user_account = fields.Boolean('Specific User Account', help='If True, new accounts will be associated to the current website')
+ auth_signup_uninvited = fields.Selection([
+ ('b2b', 'On invitation'),
+ ('b2c', 'Free sign up'),
+ ], string='Customer Account', default='b2b')
+
+ @api.onchange('language_ids')
+ def _onchange_language_ids(self):
+ language_ids = self.language_ids._origin
+ if language_ids and self.default_lang_id not in language_ids:
+ self.default_lang_id = language_ids[0]
+
+ @api.depends('social_default_image')
+ def _compute_has_social_default_image(self):
+ for website in self:
+ website.has_social_default_image = bool(website.social_default_image)
+
+ def _compute_menu(self):
+ for website in self:
+ menus = self.env['website.menu'].browse(website._get_menu_ids())
+
+ # use field parent_id (1 query) to determine field child_id (2 queries by level)"
+ for menu in menus:
+ menu._cache['child_id'] = ()
+ for menu in menus:
+ # don't add child menu if parent is forbidden
+ if menu.parent_id and menu.parent_id in menus:
+ menu.parent_id._cache['child_id'] += (menu.id,)
+
+ # prefetch every website.page and ir.ui.view at once
+ menus.mapped('is_visible')
+
+ top_menus = menus.filtered(lambda m: not m.parent_id)
+ website.menu_id = top_menus and top_menus[0].id or False
+
+ # self.env.uid for ir.rule groups on menu
+ @tools.ormcache('self.env.uid', 'self.id')
+ def _get_menu_ids(self):
+ return self.env['website.menu'].search([('website_id', '=', self.id)]).ids
+
+ def _bootstrap_snippet_filters(self):
+ ir_filter = self.env.ref('website.dynamic_snippet_country_filter', raise_if_not_found=False)
+ if ir_filter:
+ self.env['website.snippet.filter'].create({
+ 'field_names': 'name,code,image_url:image,phone_code:char',
+ 'filter_id': ir_filter.id,
+ 'limit': 16,
+ 'name': _('Countries'),
+ 'website_id': self.id,
+ })
+
+ @api.model
+ def create(self, vals):
+ self._handle_favicon(vals)
+
+ if 'user_id' not in vals:
+ company = self.env['res.company'].browse(vals.get('company_id'))
+ vals['user_id'] = company._get_public_user().id if company else self.env.ref('base.public_user').id
+
+ res = super(Website, self).create(vals)
+ res._bootstrap_homepage()
+ res._bootstrap_snippet_filters()
+
+ if not self.env.user.has_group('website.group_multi_website') and self.search_count([]) > 1:
+ all_user_groups = 'base.group_portal,base.group_user,base.group_public'
+ groups = self.env['res.groups'].concat(*(self.env.ref(it) for it in all_user_groups.split(',')))
+ groups.write({'implied_ids': [(4, self.env.ref('website.group_multi_website').id)]})
+
+ return res
+
+ def write(self, values):
+ public_user_to_change_websites = self.env['website']
+ self._handle_favicon(values)
+
+ 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:
+ company = self.env['res.company'].browse(values['company_id'])
+ super(Website, public_user_to_change_websites).write(dict(values, user_id=company and company._get_public_user().id))
+
+ result = super(Website, self - public_user_to_change_websites).write(values)
+ if 'cdn_activated' in values or 'cdn_url' in values or 'cdn_filters' in values:
+ # invalidate the caches from static node at compile time
+ self.env['ir.qweb'].clear_caches()
+
+ if 'cookies_bar' in values:
+ existing_policy_page = self.env['website.page'].search([
+ ('website_id', '=', self.id),
+ ('url', '=', '/cookie-policy'),
+ ])
+ if not values['cookies_bar']:
+ existing_policy_page.unlink()
+ elif not existing_policy_page:
+ cookies_view = self.env.ref('website.cookie_policy', raise_if_not_found=False)
+ if cookies_view:
+ cookies_view.with_context(website_id=self.id).write({'website_id': self.id})
+ specific_cook_view = self.with_context(website_id=self.id).viewref('website.cookie_policy')
+ self.env['website.page'].create({
+ 'is_published': True,
+ 'website_indexed': False,
+ 'url': '/cookie-policy',
+ 'website_id': self.id,
+ 'view_id': specific_cook_view.id,
+ })
+
+ return result
+
+ @api.model
+ def _handle_favicon(self, vals):
+ if 'favicon' in vals:
+ vals['favicon'] = tools.image_process(vals['favicon'], size=(256, 256), crop='center', output_format='ICO')
+
+ def unlink(self):
+ website = self.search([('id', 'not in', self.ids)], limit=1)
+ if not website:
+ raise UserError(_('You must keep at least one website.'))
+ # Do not delete invoices, delete what's strictly necessary
+ attachments_to_unlink = self.env['ir.attachment'].search([
+ ('website_id', 'in', self.ids),
+ '|', '|',
+ ('key', '!=', False), # theme attachment
+ ('url', 'ilike', '.custom.'), # customized theme attachment
+ ('url', 'ilike', '.assets\\_'),
+ ])
+ attachments_to_unlink.unlink()
+ return super(Website, self).unlink()
+
+ def create_and_redirect_to_theme(self):
+ self._force()
+ action = self.env.ref('website.theme_install_kanban_action')
+ return action.read()[0]
+
+ # ----------------------------------------------------------
+ # Page Management
+ # ----------------------------------------------------------
+ def _bootstrap_homepage(self):
+ Page = self.env['website.page']
+ standard_homepage = self.env.ref('website.homepage', raise_if_not_found=False)
+ if not standard_homepage:
+ return
+
+ new_homepage_view = '''<t name="Homepage" t-name="website.homepage%s">
+ <t t-call="website.layout">
+ <t t-set="pageName" t-value="'homepage'"/>
+ <div id="wrap" class="oe_structure oe_empty"/>
+ </t>
+ </t>''' % (self.id)
+ standard_homepage.with_context(website_id=self.id).arch_db = new_homepage_view
+
+ homepage_page = Page.search([
+ ('website_id', '=', self.id),
+ ('key', '=', standard_homepage.key),
+ ], limit=1)
+ if not homepage_page:
+ homepage_page = Page.create({
+ 'website_published': True,
+ 'url': '/',
+ 'view_id': self.with_context(website_id=self.id).viewref('website.homepage').id,
+ })
+ # prevent /-1 as homepage URL
+ homepage_page.url = '/'
+ self.homepage_id = homepage_page
+
+ # Bootstrap default menu hierarchy, create a new minimalist one if no default
+ default_menu = self.env.ref('website.main_menu')
+ self.copy_menu_hierarchy(default_menu)
+ home_menu = self.env['website.menu'].search([('website_id', '=', self.id), ('url', '=', '/')])
+ home_menu.page_id = self.homepage_id
+
+ def copy_menu_hierarchy(self, top_menu):
+ def copy_menu(menu, t_menu):
+ new_menu = menu.copy({
+ 'parent_id': t_menu.id,
+ 'website_id': self.id,
+ })
+ for submenu in menu.child_id:
+ copy_menu(submenu, new_menu)
+ for website in self:
+ new_top_menu = top_menu.copy({
+ 'name': _('Top Menu for Website %s', website.id),
+ 'website_id': website.id,
+ })
+ for submenu in top_menu.child_id:
+ copy_menu(submenu, new_top_menu)
+
+ @api.model
+ def new_page(self, name=False, add_menu=False, template='website.default_page', ispage=True, namespace=None):
+ """ Create a new website page, and assign it a xmlid based on the given one
+ :param name : the name of the page
+ :param template : potential xml_id of the page to create
+ :param namespace : module part of the xml_id if none, the template module name is used
+ """
+ if namespace:
+ template_module = namespace
+ else:
+ template_module, _ = template.split('.')
+ page_url = '/' + slugify(name, max_length=1024, path=True)
+ page_url = self.get_unique_path(page_url)
+ page_key = slugify(name)
+ result = dict({'url': page_url, 'view_id': False})
+
+ if not name:
+ name = 'Home'
+ page_key = 'home'
+
+ template_record = self.env.ref(template)
+ website_id = self._context.get('website_id')
+ key = self.get_unique_key(page_key, template_module)
+ view = template_record.copy({'website_id': website_id, 'key': key})
+
+ view.with_context(lang=None).write({
+ 'arch': template_record.arch.replace(template, key),
+ 'name': name,
+ })
+
+ if view.arch_fs:
+ view.arch_fs = False
+
+ website = self.get_current_website()
+ if ispage:
+ page = self.env['website.page'].create({
+ 'url': page_url,
+ 'website_id': website.id, # remove it if only one website or not?
+ 'view_id': view.id,
+ 'track': True,
+ })
+ result['view_id'] = view.id
+ if add_menu:
+ self.env['website.menu'].create({
+ 'name': name,
+ 'url': page_url,
+ 'parent_id': website.menu_id.id,
+ 'page_id': page.id,
+ 'website_id': website.id,
+ })
+ return result
+
+ @api.model
+ def guess_mimetype(self):
+ return _guess_mimetype()
+
+ def get_unique_path(self, page_url):
+ """ Given an url, return that url suffixed by counter if it already exists
+ :param page_url : the url to be checked for uniqueness
+ """
+ inc = 0
+ # we only want a unique_path for website specific.
+ # we need to be able to have /url for website=False, and /url for website=1
+ # in case of duplicate, page manager will allow you to manage this case
+ domain_static = [('website_id', '=', self.get_current_website().id)] # .website_domain()
+ page_temp = page_url
+ while self.env['website.page'].with_context(active_test=False).sudo().search([('url', '=', page_temp)] + domain_static):
+ inc += 1
+ page_temp = page_url + (inc and "-%s" % inc or "")
+ return page_temp
+
+ def get_unique_key(self, string, template_module=False):
+ """ Given a string, return an unique key including module prefix.
+ It will be suffixed by a counter if it already exists to garantee uniqueness.
+ :param string : the key to be checked for uniqueness, you can pass it with 'website.' or not
+ :param template_module : the module to be prefixed on the key, if not set, we will use website
+ """
+ if template_module:
+ string = template_module + '.' + string
+ else:
+ if not string.startswith('website.'):
+ string = 'website.' + string
+
+ # Look for unique key
+ key_copy = string
+ inc = 0
+ domain_static = self.get_current_website().website_domain()
+ while self.env['website.page'].with_context(active_test=False).sudo().search([('key', '=', key_copy)] + domain_static):
+ inc += 1
+ key_copy = string + (inc and "-%s" % inc or "")
+ return key_copy
+
+ @api.model
+ def page_search_dependencies(self, page_id=False):
+ """ Search dependencies just for information. It will not catch 100%
+ of dependencies and False positive is more than possible
+ Each module could add dependences in this dict
+ :returns a dictionnary where key is the 'categorie' of object related to the given
+ view, and the value is the list of text and link to the resource using given page
+ """
+ dependencies = {}
+ if not page_id:
+ return dependencies
+
+ page = self.env['website.page'].browse(int(page_id))
+ website = self.env['website'].browse(self._context.get('website_id'))
+ url = page.url
+
+ # search for website_page with link
+ website_page_search_dom = [('view_id.arch_db', 'ilike', url)] + website.website_domain()
+ pages = self.env['website.page'].search(website_page_search_dom)
+ page_key = _('Page')
+ if len(pages) > 1:
+ page_key = _('Pages')
+ page_view_ids = []
+ for page in pages:
+ dependencies.setdefault(page_key, [])
+ dependencies[page_key].append({
+ 'text': _('Page <b>%s</b> contains a link to this page', page.url),
+ 'item': page.name,
+ 'link': page.url,
+ })
+ page_view_ids.append(page.view_id.id)
+
+ # search for ir_ui_view (not from a website_page) with link
+ page_search_dom = [('arch_db', 'ilike', url), ('id', 'not in', page_view_ids)] + website.website_domain()
+ views = self.env['ir.ui.view'].search(page_search_dom)
+ view_key = _('Template')
+ if len(views) > 1:
+ view_key = _('Templates')
+ for view in views:
+ dependencies.setdefault(view_key, [])
+ dependencies[view_key].append({
+ 'text': _('Template <b>%s (id:%s)</b> contains a link to this page') % (view.key or view.name, view.id),
+ 'link': '/web#id=%s&view_type=form&model=ir.ui.view' % view.id,
+ 'item': _('%s (id:%s)') % (view.key or view.name, view.id),
+ })
+ # search for menu with link
+ menu_search_dom = [('url', 'ilike', '%s' % url)] + website.website_domain()
+
+ menus = self.env['website.menu'].search(menu_search_dom)
+ menu_key = _('Menu')
+ if len(menus) > 1:
+ menu_key = _('Menus')
+ for menu in menus:
+ dependencies.setdefault(menu_key, []).append({
+ 'text': _('This page is in the menu <b>%s</b>', menu.name),
+ 'link': '/web#id=%s&view_type=form&model=website.menu' % menu.id,
+ 'item': menu.name,
+ })
+
+ return dependencies
+
+ @api.model
+ def page_search_key_dependencies(self, page_id=False):
+ """ Search dependencies just for information. It will not catch 100%
+ of dependencies and False positive is more than possible
+ Each module could add dependences in this dict
+ :returns a dictionnary where key is the 'categorie' of object related to the given
+ view, and the value is the list of text and link to the resource using given page
+ """
+ dependencies = {}
+ if not page_id:
+ return dependencies
+
+ page = self.env['website.page'].browse(int(page_id))
+ website = self.env['website'].browse(self._context.get('website_id'))
+ key = page.key
+
+ # search for website_page with link
+ website_page_search_dom = [
+ ('view_id.arch_db', 'ilike', key),
+ ('id', '!=', page.id)
+ ] + website.website_domain()
+ pages = self.env['website.page'].search(website_page_search_dom)
+ page_key = _('Page')
+ if len(pages) > 1:
+ page_key = _('Pages')
+ page_view_ids = []
+ for p in pages:
+ dependencies.setdefault(page_key, [])
+ dependencies[page_key].append({
+ 'text': _('Page <b>%s</b> is calling this file', p.url),
+ 'item': p.name,
+ 'link': p.url,
+ })
+ page_view_ids.append(p.view_id.id)
+
+ # search for ir_ui_view (not from a website_page) with link
+ page_search_dom = [
+ ('arch_db', 'ilike', key), ('id', 'not in', page_view_ids),
+ ('id', '!=', page.view_id.id),
+ ] + website.website_domain()
+ views = self.env['ir.ui.view'].search(page_search_dom)
+ view_key = _('Template')
+ if len(views) > 1:
+ view_key = _('Templates')
+ for view in views:
+ dependencies.setdefault(view_key, [])
+ dependencies[view_key].append({
+ 'text': _('Template <b>%s (id:%s)</b> is calling this file') % (view.key or view.name, view.id),
+ 'item': _('%s (id:%s)') % (view.key or view.name, view.id),
+ 'link': '/web#id=%s&view_type=form&model=ir.ui.view' % view.id,
+ })
+
+ return dependencies
+
+ # ----------------------------------------------------------
+ # Languages
+ # ----------------------------------------------------------
+
+ def _get_alternate_languages(self, canonical_params):
+ self.ensure_one()
+
+ 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 []
+
+ langs = []
+ shorts = []
+
+ for lg in languages:
+ lg_codes = lg.code.split('_')
+ short = lg_codes[0]
+ shorts.append(short)
+ langs.append({
+ 'hreflang': ('-'.join(lg_codes)).lower(),
+ '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
+
+ # ----------------------------------------------------------
+ # Utilities
+ # ----------------------------------------------------------
+
+ @api.model
+ def get_current_website(self, fallback=True):
+ if request and request.session.get('force_website_id'):
+ website_id = self.browse(request.session['force_website_id']).exists()
+ if not website_id:
+ # Don't crash is session website got deleted
+ request.session.pop('force_website_id')
+ else:
+ return website_id
+
+ website_id = self.env.context.get('website_id')
+ if website_id:
+ return self.browse(website_id)
+
+ # The format of `httprequest.host` is `domain:port`
+ domain_name = request and request.httprequest.host or ''
+
+ country = request.session.geoip.get('country_code') if request and request.session.geoip else False
+ country_id = False
+ if country:
+ country_id = self.env['res.country'].search([('code', '=', country)], limit=1).id
+
+ website_id = self._get_current_website_id(domain_name, country_id, fallback=fallback)
+ return self.browse(website_id)
+
+ @tools.cache('domain_name', 'country_id', 'fallback')
+ @api.model
+ def _get_current_website_id(self, domain_name, country_id, fallback=True):
+ """Get the current website id.
+
+ First find all the websites for which the configured `domain` (after
+ ignoring a potential scheme) is equal to the given
+ `domain_name`. If there is only one result, return it immediately.
+
+ If there are no website found for the given `domain_name`, either
+ fallback to the first found website (no matter its `domain`) or return
+ False depending on the `fallback` parameter.
+
+ If there are multiple websites for the same `domain_name`, we need to
+ filter them out by country. We return the first found website matching
+ the given `country_id`. If no found website matching `domain_name`
+ corresponds to the given `country_id`, the first found website for
+ `domain_name` will be returned (no matter its country).
+
+ :param domain_name: the domain for which we want the website.
+ In regard to the `url_parse` method, only the `netloc` part should
+ be given here, no `scheme`.
+ :type domain_name: string
+
+ :param country_id: id of the country for which we want the website
+ :type country_id: int
+
+ :param fallback: if True and no website is found for the specificed
+ `domain_name`, return the first website (without filtering them)
+ :type fallback: bool
+
+ :return: id of the found website, or False if no website is found and
+ `fallback` is False
+ :rtype: int or False
+
+ :raises: if `fallback` is True but no website at all is found
+ """
+ def _remove_port(domain_name):
+ return (domain_name or '').split(':')[0]
+
+ def _filter_domain(website, domain_name, ignore_port=False):
+ """Ignore `scheme` from the `domain`, just match the `netloc` which
+ is host:port in the version of `url_parse` we use."""
+ # Here we add http:// to the domain if it's not set because
+ # `url_parse` expects it to be set to correctly return the `netloc`.
+ website_domain = urls.url_parse(website._get_http_domain()).netloc
+ if ignore_port:
+ website_domain = _remove_port(website_domain)
+ domain_name = _remove_port(domain_name)
+ return website_domain.lower() == (domain_name or '').lower()
+
+ # Sort on country_group_ids so that we fall back on a generic website:
+ # websites with empty country_group_ids will be first.
+ found_websites = self.search([('domain', 'ilike', _remove_port(domain_name))]).sorted('country_group_ids')
+ # Filter for the exact domain (to filter out potential subdomains) due
+ # to the use of ilike.
+ websites = found_websites.filtered(lambda w: _filter_domain(w, domain_name))
+ # If there is no domain matching for the given port, ignore the port.
+ websites = websites or found_websites.filtered(lambda w: _filter_domain(w, domain_name, ignore_port=True))
+
+ if not websites:
+ if not fallback:
+ return False
+ return self.search([], limit=1).id
+ elif len(websites) == 1:
+ return websites.id
+ else: # > 1 website with the same domain
+ country_specific_websites = websites.filtered(lambda website: country_id in website.country_group_ids.mapped('country_ids').ids)
+ return country_specific_websites[0].id if country_specific_websites else websites[0].id
+
+ def _force(self):
+ self._force_website(self.id)
+
+ def _force_website(self, website_id):
+ if request:
+ request.session['force_website_id'] = website_id and str(website_id).isdigit() and int(website_id)
+
+ @api.model
+ def is_publisher(self):
+ return self.env['ir.model.access'].check('ir.ui.view', 'write', False)
+
+ @api.model
+ def is_user(self):
+ return self.env['ir.model.access'].check('ir.ui.menu', 'read', False)
+
+ @api.model
+ def is_public_user(self):
+ return request.env.user.id == request.website._get_cached('user_id')
+
+ @api.model
+ def viewref(self, view_id, raise_if_not_found=True):
+ ''' Given an xml_id or a view_id, return the corresponding view record.
+ In case of website context, return the most specific one.
+
+ If no website_id is in the context, it will return the generic view,
+ instead of a random one like `get_view_id`.
+
+ Look also for archived views, no matter the context.
+
+ :param view_id: either a string xml_id or an integer view_id
+ :param raise_if_not_found: should the method raise an error if no view found
+ :return: The view record or empty recordset
+ '''
+ View = self.env['ir.ui.view'].sudo()
+ view = View
+ if isinstance(view_id, str):
+ if 'website_id' in self._context:
+ domain = [('key', '=', view_id)] + self.env['website'].website_domain(self._context.get('website_id'))
+ order = 'website_id'
+ else:
+ domain = [('key', '=', view_id)]
+ order = View._order
+ views = View.with_context(active_test=False).search(domain, order=order)
+ if views:
+ view = views.filter_duplicate()
+ else:
+ # we handle the raise below
+ view = self.env.ref(view_id, raise_if_not_found=False)
+ # self.env.ref might return something else than an ir.ui.view (eg: a theme.ir.ui.view)
+ if not view or view._name != 'ir.ui.view':
+ # make sure we always return a recordset
+ view = View
+ elif isinstance(view_id, int):
+ view = View.browse(view_id)
+ else:
+ raise ValueError('Expecting a string or an integer, not a %s.' % (type(view_id)))
+
+ if not view and raise_if_not_found:
+ raise ValueError('No record found for unique ID %s. It may have been deleted.' % (view_id))
+ return view
+
+ @tools.ormcache_context(keys=('website_id',))
+ def _cache_customize_show_views(self):
+ views = self.env['ir.ui.view'].with_context(active_test=False).sudo().search([('customize_show', '=', True)])
+ views = views.filter_duplicate()
+ return {v.key: v.active for v in views}
+
+ @tools.ormcache_context('key', keys=('website_id',))
+ def is_view_active(self, key, raise_if_not_found=False):
+ """
+ Return True if active, False if not active, None if not found or not a customize_show view
+ """
+ views = self._cache_customize_show_views()
+ view = key in views and views[key]
+ if view is None and raise_if_not_found:
+ raise ValueError('No view of type customize_show found for key %s' % key)
+ return view
+
+ @api.model
+ def get_template(self, template):
+ View = self.env['ir.ui.view']
+ if isinstance(template, int):
+ view_id = template
+ else:
+ if '.' not in template:
+ template = 'website.%s' % template
+ view_id = View.get_view_id(template)
+ if not view_id:
+ raise NotFound
+ return View.sudo().browse(view_id)
+
+ @api.model
+ def pager(self, url, total, page=1, step=30, scope=5, url_args=None):
+ return pager(url, total, page=page, step=step, scope=scope, url_args=url_args)
+
+ def rule_is_enumerable(self, rule):
+ """ Checks that it is possible to generate sensible GET queries for
+ a given rule (if the endpoint matches its own requirements)
+ :type rule: werkzeug.routing.Rule
+ :rtype: bool
+ """
+ endpoint = rule.endpoint
+ methods = endpoint.routing.get('methods') or ['GET']
+
+ converters = list(rule._converters.values())
+ if not ('GET' in methods and
+ endpoint.routing['type'] == 'http' and
+ endpoint.routing['auth'] in ('none', 'public') and
+ endpoint.routing.get('website', False) and
+ all(hasattr(converter, 'generate') for converter in converters)):
+ return False
+
+ # dont't list routes without argument having no default value or converter
+ sign = inspect.signature(endpoint.method.original_func)
+ params = list(sign.parameters.values())[1:] # skip self
+ supported_kinds = (inspect.Parameter.POSITIONAL_ONLY,
+ inspect.Parameter.POSITIONAL_OR_KEYWORD)
+ has_no_default = lambda p: p.default is inspect.Parameter.empty
+
+ # check that all args have a converter
+ return all(p.name in rule._converters for p in params
+ if p.kind in supported_kinds and has_no_default(p))
+
+ def _enumerate_pages(self, query_string=None, force=False):
+ """ Available pages in the website/CMS. This is mostly used for links
+ generation and can be overridden by modules setting up new HTML
+ controllers for dynamic pages (e.g. blog).
+ By default, returns template views marked as pages.
+ :param str query_string: a (user-provided) string, fetches pages
+ matching the string
+ :returns: a list of mappings with two keys: ``name`` is the displayable
+ name of the resource (page), ``url`` is the absolute URL
+ of the same.
+ :rtype: list({name: str, url: str})
+ """
+
+ router = request.httprequest.app.get_db_router(request.db)
+ # Force enumeration to be performed as public user
+ url_set = set()
+
+ sitemap_endpoint_done = set()
+
+ for rule in router.iter_rules():
+ if 'sitemap' in rule.endpoint.routing and rule.endpoint.routing['sitemap'] is not True:
+ if rule.endpoint in sitemap_endpoint_done:
+ continue
+ sitemap_endpoint_done.add(rule.endpoint)
+
+ func = rule.endpoint.routing['sitemap']
+ if func is False:
+ continue
+ for loc in func(self.env, rule, query_string):
+ yield loc
+ continue
+
+ if not self.rule_is_enumerable(rule):
+ continue
+
+ if 'sitemap' not in rule.endpoint.routing:
+ logger.warning('No Sitemap value provided for controller %s (%s)' %
+ (rule.endpoint.method, ','.join(rule.endpoint.routing['routes'])))
+
+ converters = rule._converters or {}
+ if query_string and not converters and (query_string not in rule.build({}, append_unknown=False)[1]):
+ continue
+
+ values = [{}]
+ # converters with a domain are processed after the other ones
+ convitems = sorted(
+ converters.items(),
+ key=lambda x: (hasattr(x[1], 'domain') and (x[1].domain != '[]'), rule._trace.index((True, x[0]))))
+
+ for (i, (name, converter)) in enumerate(convitems):
+ if 'website_id' in self.env[converter.model]._fields and (not converter.domain or converter.domain == '[]'):
+ converter.domain = "[('website_id', 'in', (False, current_website_id))]"
+
+ newval = []
+ for val in values:
+ query = i == len(convitems) - 1 and query_string
+ if query:
+ r = "".join([x[1] for x in rule._trace[1:] if not x[0]]) # remove model converter from route
+ query = sitemap_qs2dom(query, r, self.env[converter.model]._rec_name)
+ if query == FALSE_DOMAIN:
+ continue
+
+ for rec in converter.generate(uid=self.env.uid, dom=query, args=val):
+ newval.append(val.copy())
+ newval[-1].update({name: rec})
+ values = newval
+
+ for value in values:
+ domain_part, url = rule.build(value, append_unknown=False)
+ if not query_string or query_string.lower() in url.lower():
+ page = {'loc': url}
+ if url in url_set:
+ continue
+ url_set.add(url)
+
+ yield page
+
+ # '/' already has a http.route & is in the routing_map so it will already have an entry in the xml
+ domain = [('url', '!=', '/')]
+ if not force:
+ domain += [('website_indexed', '=', True), ('visibility', '=', False)]
+ # is_visible
+ domain += [
+ ('website_published', '=', True), ('visibility', '=', False),
+ '|', ('date_publish', '=', False), ('date_publish', '<=', fields.Datetime.now())
+ ]
+
+ if query_string:
+ domain += [('url', 'like', query_string)]
+
+ pages = self._get_website_pages(domain)
+
+ 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)
+ if page['write_date']:
+ record['lastmod'] = page['write_date'].date()
+ yield record
+
+ def _get_website_pages(self, domain=None, order='name', limit=None):
+ if domain is None:
+ domain = []
+ domain += self.get_current_website().website_domain()
+ pages = self.env['website.page'].sudo().search(domain, order=order, limit=limit)
+ return pages
+
+ def search_pages(self, needle=None, limit=None):
+ name = slugify(needle, max_length=50, path=True)
+ res = []
+ for page in self._enumerate_pages(query_string=name, force=True):
+ res.append(page)
+ if len(res) == limit:
+ break
+ return res
+
+ def get_suggested_controllers(self):
+ """
+ Returns a tuple (name, url, icon).
+ Where icon can be a module name, or a path
+ """
+ suggested_controllers = [
+ (_('Homepage'), url_for('/'), 'website'),
+ (_('Contact Us'), url_for('/contactus'), 'website_crm'),
+ ]
+ return suggested_controllers
+
+ @api.model
+ def image_url(self, record, field, size=None):
+ """ Returns a local url that points to the image field of a given browse record. """
+ sudo_record = record.sudo()
+ sha = hashlib.sha512(str(getattr(sudo_record, '__last_update')).encode('utf-8')).hexdigest()[:7]
+ size = '' if size is None else '/%s' % size
+ return '/web/image/%s/%s/%s%s?unique=%s' % (record._name, record.id, field, size, sha)
+
+ def get_cdn_url(self, uri):
+ self.ensure_one()
+ if not uri:
+ return ''
+ cdn_url = self.cdn_url
+ cdn_filters = (self.cdn_filters or '').splitlines()
+ for flt in cdn_filters:
+ if flt and re.match(flt, uri):
+ return urls.url_join(cdn_url, uri)
+ return uri
+
+ @api.model
+ def action_dashboard_redirect(self):
+ if self.env.user.has_group('base.group_system') or self.env.user.has_group('website.group_website_designer'):
+ return self.env["ir.actions.actions"]._for_xml_id("website.backend_dashboard")
+ return self.env["ir.actions.actions"]._for_xml_id("website.action_website")
+
+ def button_go_website(self, path='/', mode_edit=False):
+ self._force()
+ if mode_edit:
+ path += '?enable_editor=1'
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': path,
+ 'target': 'self',
+ }
+
+ def _get_http_domain(self):
+ """Get the domain of the current website, prefixed by http if no
+ scheme is specified and withtout trailing /.
+
+ Empty string if no domain is specified on the website.
+ """
+ self.ensure_one()
+ if not self.domain:
+ return ''
+
+ domain = self.domain
+ if not self.domain.startswith('http'):
+ domain = 'http://%s' % domain
+
+ return domain.rstrip('/')
+
+ def get_base_url(self):
+ 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.code:
+ arguments[key] = val.with_context(lang=lang.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
+
+ @tools.ormcache('self.id')
+ def _get_cached_values(self):
+ self.ensure_one()
+ return {
+ 'user_id': self.user_id.id,
+ 'company_id': self.company_id.id,
+ 'default_lang_id': self.default_lang_id.id,
+ }
+
+ def _get_cached(self, field):
+ return self._get_cached_values()[field]
+
+ def _get_relative_url(self, url):
+ return urls.url_parse(url).replace(scheme='', netloc='').to_url()
+
+
+class BaseModel(models.AbstractModel):
+ _inherit = 'base'
+
+ def get_base_url(self):
+ """
+ Returns baseurl about one given record.
+ If a website_id field exists in the current record we use the url
+ from this website as base url.
+
+ :return: the base url for this record
+ :rtype: string
+
+ """
+ self.ensure_one()
+ if 'website_id' in self and self.website_id.domain:
+ return self.website_id._get_http_domain()
+ else:
+ return super(BaseModel, self).get_base_url()
+
+ def get_website_meta(self):
+ # dummy version of 'get_website_meta' above; this is a graceful fallback
+ # for models that don't inherit from 'website.seo.metadata'
+ return {}