summaryrefslogtreecommitdiff
path: root/addons/website/models
diff options
context:
space:
mode:
authorJeremy Kersten <jke@odoo.com>2018-08-13 20:36:28 +0200
committerJeremy Kersten <jke@odoo.com>2018-08-13 20:36:28 +0200
commitfbbf24f58acbcd9488cfa8393bd2d4e3a8e4c053 (patch)
tree2994a4151cbc70ac62d1741d84f7758c049557d4 /addons/website/models
parent15df14b9e1863ca09115d920595a6eeb9f6514f1 (diff)
parent853e13800fb030f7ae7180558cb6c3c74b843cd1 (diff)
[MERGE] website: add multi-websites support
Diffstat (limited to 'addons/website/models')
-rw-r--r--addons/website/models/__init__.py2
-rw-r--r--addons/website/models/ir_attachment.py13
-rw-r--r--addons/website/models/ir_http.py29
-rw-r--r--addons/website/models/ir_qweb.py14
-rw-r--r--addons/website/models/ir_rule.py16
-rw-r--r--addons/website/models/ir_ui_view.py138
-rw-r--r--addons/website/models/res_company.py18
-rw-r--r--addons/website/models/res_config_settings.py43
-rw-r--r--addons/website/models/res_partner.py24
-rw-r--r--addons/website/models/res_users.py39
-rw-r--r--addons/website/models/website.py459
11 files changed, 622 insertions, 173 deletions
diff --git a/addons/website/models/__init__.py b/addons/website/models/__init__.py
index 263c44b9e27..0ed659a8f3a 100644
--- a/addons/website/models/__init__.py
+++ b/addons/website/models/__init__.py
@@ -7,8 +7,10 @@ from . import ir_http
from . import ir_qweb
from . import ir_qweb_fields
from . import website
+from . import ir_rule
from . import ir_ui_view
from . import res_company
from . import res_partner
+from . import res_users
from . import res_config_settings
from . import res_users
diff --git a/addons/website/models/ir_attachment.py b/addons/website/models/ir_attachment.py
index a1f9df5dad1..15fe50f5471 100644
--- a/addons/website/models/ir_attachment.py
+++ b/addons/website/models/ir_attachment.py
@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import api, fields, models
+import logging
+from odoo import fields, models, api
+_logger = logging.getLogger(__name__)
class Attachment(models.Model):
@@ -10,6 +12,15 @@ class Attachment(models.Model):
# related for backward compatibility with saas-6
website_url = fields.Char(string="Website URL", related='local_url', deprecated=True)
+ key = fields.Char(help='Technical field used to resolve multiple attachments in a multi-website environment.')
+ website_id = fields.Many2one('website')
+
+ @api.model
+ def create(self, vals):
+ website = self.env['website'].get_current_website()
+ if website and 'website_id' not in vals and 'not_force_website_id' not in self.env.context:
+ vals['website_id'] = website.id
+ return super(Attachment, self).create(vals)
@api.model
def get_serving_groups(self):
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index 10250e2a756..e3eb27a9d77 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -117,9 +117,13 @@ class Http(models.AbstractModel):
@classmethod
def _serve_page(cls):
req_page = request.httprequest.path
+ page_domain = [('url', '=', req_page)] + request.website.website_domain()
- domain = [('url', '=', req_page), '|', ('website_ids', 'in', request.website.id), ('website_ids', '=', False)]
- pages = request.env['website.page'].search(domain)
+ published_domain = page_domain
+ # need to bypass website_published, to apply is_most_specific
+ # filter later if not publisher
+ pages = request.env['website.page'].sudo().search(published_domain, order='website_id')
+ pages = pages.filtered(pages._is_most_specific_page)
if not request.website.is_publisher():
pages = pages.filtered('is_visible')
@@ -137,10 +141,7 @@ class Http(models.AbstractModel):
@classmethod
def _serve_redirect(cls):
req_page = request.httprequest.path
- domain = [
- '|', ('website_id', '=', request.website.id), ('website_id', '=', False),
- ('url_from', '=', req_page)
- ]
+ domain = [('url_from', '=', req_page)] + request.website.website_domain()
return request.env['website.redirect'].search(domain, limit=1)
@classmethod
@@ -178,7 +179,7 @@ class Http(models.AbstractModel):
return response
except Exception as e:
if 'werkzeug' in config['dev_mode'] and (not isinstance(exception, QWebException) or not exception.qweb.get('cause')):
- raise
+ raise e
exception = e
values = dict(
@@ -239,7 +240,7 @@ class Http(models.AbstractModel):
env = env or request.env
obj = None
if xmlid:
- obj = env.ref(xmlid, False)
+ obj = cls._xmlid_to_obj(env, xmlid)
elif id and model in env:
obj = env[model].browse(int(id))
if obj and 'website_published' in obj._fields:
@@ -251,11 +252,23 @@ class Http(models.AbstractModel):
default_mimetype=default_mimetype, access_token=access_token, share_id=share_id, share_token=share_token,
force_ext=force_ext, env=env)
+ @classmethod
+ def _xmlid_to_obj(cls, env, xmlid):
+ website_id = env['website'].get_current_website()
+ if website_id and website_id.theme_id:
+ obj = env['ir.attachment'].search([('key', '=', xmlid), ('website_id', '=', website_id.id)])
+ if obj:
+ return obj[0]
+
+ return super(Http, cls)._xmlid_to_obj(env, xmlid)
+
class ModelConverter(ModelConverter):
def generate(self, uid, dom=None, args=None):
Model = request.env[self.model].sudo(uid)
+ # Allow to current_website_id directly in route domain
+ args.update(current_website_id=request.env['website'].get_current_website().id)
domain = safe_eval(self.domain, (args or {}).copy())
if dom:
domain += dom
diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py
index 0f73bc469f5..090502aa56f 100644
--- a/addons/website/models/ir_qweb.py
+++ b/addons/website/models/ir_qweb.py
@@ -5,11 +5,22 @@ from collections import OrderedDict
from odoo import models
from odoo.http import request
+from odoo.addons.base.models.assetsbundle import AssetsBundle
+
re_background_image = re.compile(r"(background-image\s*:\s*url\(\s*['\"]?\s*)([^)'\"]+)")
+class AssetsBundleMultiWebsite(AssetsBundle):
+ def _get_asset_url_values(self, id, unique, extra, name, page, type):
+ website_id = self.env.context.get('website_id')
+ website_id_path = website_id and ('%s/' % website_id) or ''
+ extra = website_id_path + extra
+ res = super(AssetsBundleMultiWebsite, self)._get_asset_url_values(id, unique, extra, name, page, type)
+ return res
+
+
class QWeb(models.AbstractModel):
""" QWeb object for rendering stuff in the website context """
@@ -23,6 +34,9 @@ class QWeb(models.AbstractModel):
'img': 'src',
}
+ def get_asset_bundle(self, xmlid, files, remains=None, env=None):
+ return AssetsBundleMultiWebsite(xmlid, files, remains=remains, env=env)
+
def _post_processing_att(self, tagName, atts, options):
if atts.get('data-no-post-process'):
return atts
diff --git a/addons/website/models/ir_rule.py b/addons/website/models/ir_rule.py
new file mode 100644
index 00000000000..ba5e07128ee
--- /dev/null
+++ b/addons/website/models/ir_rule.py
@@ -0,0 +1,16 @@
+# coding: utf-8
+from odoo import api, models
+
+
+class IrRule(models.Model):
+ _inherit = 'ir.rule'
+
+ @api.model
+ def _eval_context(self):
+ res = super(IrRule, self)._eval_context()
+ res['website_id'] = self.env['website'].get_current_website().id
+ return res
+
+ def _compute_domain_keys(self):
+ """ Return the list of context keys to use for caching ``_compute_domain``. """
+ return super(IrRule, self)._compute_domain_keys() + ['website_id']
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index b69a315f6d7..00354a43613 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -1,12 +1,13 @@
-# -*- coding: ascii -*-
+# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from itertools import groupby
-from odoo import api, fields, models
+from odoo import api, fields, models, _
from odoo import tools
from odoo.addons.http_routing.models.ir_http import url_for
+from odoo.osv import expression
from odoo.http import request
from odoo.tools import pycompat
@@ -20,25 +21,92 @@ class View(models.Model):
customize_show = fields.Boolean("Show As Optional Inherit", default=False)
website_id = fields.Many2one('website', ondelete='cascade', string="Website")
- page_ids = fields.One2many('website.page', compute='_compute_page_ids', store=False)
+ page_ids = fields.One2many('website.page', 'view_id')
first_page_id = fields.Many2one('website.page', string='Website Page', help='First page linked to this view', compute='_compute_first_page_id')
+ # theme_id = fields.Many2one('ir.module.module')
- @api.one
+ @api.multi
def _compute_first_page_id(self):
- self.first_page_id = self.env['website.page'].search([('view_id', '=', self.id)], limit=1)
+ for view in self:
+ view.first_page_id = self.env['website.page'].search([('view_id', '=', view.id)], limit=1)
+
+ @api.multi
+ def write(self, vals):
+ '''COW for ir.ui.view. This way editing websites does not impact other
+ websites. Also this way newly created websites will only
+ contain the default views.
+ '''
+ if not self._context.get('no_cow'):
+ current_website_id = self._context.get('website_id')
+ for view in self:
+ # if generic view in multi-website context
+ if current_website_id and not view.website_id:
+ new_website_specific_view = view.copy({'website_id': current_website_id})
+ view._create_website_specific_pages_for_view(new_website_specific_view,
+ view.env['website'].browse(current_website_id))
- @api.one
- def _compute_page_ids(self):
- self.page_ids = self.env['website.page'].search(
- [('view_id', '=', self.id)]
- )
+ # trigger COW on inheriting views
+ for inherit_child in view.inherit_children_ids:
+ inherit_child.write({'inherit_id': new_website_specific_view.id})
+
+ new_website_specific_view.write(vals)
+ else:
+ super(View, view).write(vals)
+ else:
+ super(View, self).write(vals)
+
+ return True
@api.multi
def unlink(self):
+ '''This implements COU (copy-on-unlink). When deleting a generic page
+ website-specific pages will be created so only the current
+ website is affected.
+ '''
+ current_website_id = self._context.get('website_id')
+
+ if current_website_id and not self._context.get('no_cow'):
+ for view in self.filtered(lambda view: not view.website_id):
+ for website in self.env['website'].search([('id', '!=', current_website_id)]):
+ # reuse the COW mechanism to create
+ # website-specific copies, it will take
+ # care of creating pages and menus.
+ view.with_context(website_id=website.id).write({'name': view.name})
+
result = super(View, self).unlink()
self.clear_caches()
return result
+ def _create_website_specific_pages_for_view(self, new_view, website):
+ for page in self.page_ids:
+ # create new pages for this view
+ page.copy({
+ 'view_id': new_view.id,
+ })
+
+ @api.model
+ def get_related_views(self, key, bundles=False):
+ '''Make this only return most specific views for website.'''
+ # get_related_views can be called through website=False routes
+ # (e.g. /web_editor/get_assets_editor_resources), so website
+ # dispatch_parameters may not be added. Manually set
+ # website_id.
+ self = self.with_context(website_id=self.env['website'].get_current_website().id)
+ views = super(View, self).get_related_views(key, bundles=bundles)
+ current_website_id = self._context.get('website_id')
+ most_specific_views = self.env['ir.ui.view']
+
+ if not current_website_id:
+ return views
+
+ for view in views:
+ if view.website_id and view.website_id.id == current_website_id:
+ most_specific_views |= view
+ elif not view.website_id and not any(view.key == view2.key and view2.website_id and view2.website_id.id == current_website_id for view2 in views):
+ most_specific_views |= view
+
+ return most_specific_views
+
@api.multi
def _sort_suitability_key(self):
""" Key function to sort views by descending suitability
@@ -55,15 +123,15 @@ class View(models.Model):
def filter_duplicate(self):
""" Filter current recordset only keeping the most suitable view per distinct key """
filtered = self.env['ir.ui.view']
- for dummy, group in groupby(self, key=lambda record: record.key):
+ for dummy, group in groupby(self.sorted('key'), key=lambda record: record.key):
filtered += sorted(group, key=lambda record: record._sort_suitability_key())[0]
- return filtered
+ return filtered.sorted(key=lambda view: (view.priority, view.id))
@api.model
def _view_obj(self, view_id):
if isinstance(view_id, pycompat.string_types):
if 'website_id' in self._context:
- domain = [('key', '=', view_id), '|', ('website_id', '=', False), ('website_id', '=', self._context.get('website_id'))]
+ domain = [('key', '=', view_id)] + self.env['website'].website_domain(self._context.get('website_id'))
order = 'website_id'
else:
domain = [('key', '=', view_id)]
@@ -80,15 +148,40 @@ class View(models.Model):
return view_id
@api.model
+ def _get_inheriting_views_arch_website(self, view_id):
+ return self.env['website'].browse(self._context.get('website_id'))
+
+ @api.model
def _get_inheriting_views_arch_domain(self, view_id, model):
domain = super(View, self)._get_inheriting_views_arch_domain(view_id, model)
- return ['|', ('website_id', '=', False), ('website_id', '=', self.env.context.get('website_id'))] + domain
+ current_website = self._get_inheriting_views_arch_website(view_id)
+ website_views_domain = current_website.website_domain()
+ # when rendering for the website we have to include inactive views
+ # we will prefer inactive website-specific views over active generic ones
+ if current_website:
+ domain = [leaf for leaf in domain if 'active' not in leaf]
+
+ return expression.AND([website_views_domain, domain])
+
+ @api.model
+ def get_inheriting_views_arch(self, view_id, model):
+ if not self._context.get('website_id'):
+ return super(View, self).get_inheriting_views_arch(view_id, model)
+
+ inheriting_views = super(View, self.with_context(active_test=False)).get_inheriting_views_arch(view_id, model)
+
+ # prefer inactive website-specific views over active generic ones
+ inheriting_views = self.browse([view[1] for view in inheriting_views]).filter_duplicate().filtered('active')
+
+ return [(view.arch, view.id) for view in inheriting_views]
@api.model
@tools.ormcache_context('self._uid', 'xml_id', keys=('website_id',))
def get_view_id(self, xml_id):
if 'website_id' in self._context and not isinstance(xml_id, pycompat.integer_types):
- domain = [('key', '=', xml_id), '|', ('website_id', '=', self._context['website_id']), ('website_id', '=', False)]
+ current_website = self.env['website'].browse(self._context.get('website_id'))
+ domain = ['&', ('key', '=', xml_id)] + current_website.website_domain()
+
view = self.search(domain, order='website_id', limit=1)
if not view:
_logger.warning("Could not find view object with xml_id '%s'", xml_id)
@@ -125,6 +218,7 @@ class View(models.Model):
qcontext = super(View, self)._prepare_qcontext()
if request and getattr(request, 'is_frontend', False):
+ Website = self.env['website']
editable = request.website.is_publisher()
translatable = editable and self._context.get('lang') != request.env['ir.http']._get_default_lang().code
editable = not translatable and editable
@@ -132,6 +226,17 @@ class View(models.Model):
if 'main_object' not in qcontext:
qcontext['main_object'] = self
+ domain_based_info = {'website_id': '', 'name': _('Domain Based')}
+ force_website_id = request.session.get('force_website_id', False)
+ if force_website_id:
+ selected_website = Website.browse(force_website_id)
+ qcontext['multi_website_selected_website'] = {'website_id': selected_website.id, 'name': selected_website.name}
+ else:
+ qcontext['multi_website_selected_website'] = domain_based_info
+
+ qcontext['multi_website_websites'] = [{'website_id': website.id, 'name': website.name} for website in Website.search([])]
+ qcontext['multi_website_websites'] += [domain_based_info]
+
qcontext.update(dict(
self._context.copy(),
website=request.website,
@@ -162,3 +267,6 @@ class View(models.Model):
'url': '/website/pages',
'target': 'self',
}
+
+ def _read_template_keys(self):
+ return super(View, self)._read_template_keys() + ['website_id']
diff --git a/addons/website/models/res_company.py b/addons/website/models/res_company.py
index bc1e6fa5002..e9932e8a15e 100644
--- a/addons/website/models/res_company.py
+++ b/addons/website/models/res_company.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import api, fields, models
+from odoo import api, models
class Company(models.Model):
@@ -16,3 +16,19 @@ class Company(models.Model):
def google_map_link(self, zoom=8):
partner = self.sudo().partner_id
return partner and partner.google_map_link(zoom) or None
+
+ @api.multi
+ def _get_public_user(self):
+ self.ensure_one()
+ public_users = self.env.ref('base.group_public').with_context(active_test=False).users
+ public_users_for_website = public_users.filtered(lambda user: user.company_id == self)
+
+ if public_users_for_website:
+ return public_users_for_website[0]
+ else:
+ return self.env.ref('base.public_user').copy({
+ 'name': 'Public user for %s' % self.name,
+ 'login': 'public_company_%s' % self.id,
+ 'company_id': self.id,
+ 'company_ids': [(6, 0, [self.id])],
+ })
diff --git a/addons/website/models/res_config_settings.py b/addons/website/models/res_config_settings.py
index f2b79b88479..36ca15c2e58 100644
--- a/addons/website/models/res_config_settings.py
+++ b/addons/website/models/res_config_settings.py
@@ -16,6 +16,9 @@ class ResConfigSettings(models.TransientModel):
website_id = fields.Many2one('website', string="website",
default=_default_website, required=True, ondelete='cascade')
website_name = fields.Char('Website Name', related='website_id.name')
+ website_domain = fields.Char('Website Domain', related='website_id.domain')
+ website_country_group_ids = fields.Many2many(related='website_id.country_group_ids')
+ website_company_id = fields.Many2one(related='website_id.company_id', string='Website Company')
language_ids = fields.Many2many(related='website_id.language_ids', relation='res.lang')
language_count = fields.Integer(string='Number of languages', compute='_compute_language_count', readonly=True)
website_default_lang_id = fields.Many2one(
@@ -25,27 +28,35 @@ class ResConfigSettings(models.TransientModel):
website_default_lang_code = fields.Char(
'Default language code', related='website_id.default_lang_code',
oldname='default_lang_code')
+ specific_user_account = fields.Boolean('Specific User Account', config_parameter='website_id.specific_user_account',
+ help='Are newly created user accounts website specific')
+
google_analytics_key = fields.Char('Google Analytics Key', related='website_id.google_analytics_key')
google_management_client_id = fields.Char('Google Client ID', related='website_id.google_management_client_id')
google_management_client_secret = fields.Char('Google Client Secret', related='website_id.google_management_client_secret')
- cdn_activated = fields.Boolean('Use a Content Delivery Network (CDN)', related='website_id.cdn_activated')
+ cdn_activated = fields.Boolean(related='website_id.cdn_activated')
cdn_url = fields.Char(related='website_id.cdn_url')
cdn_filters = fields.Text(related='website_id.cdn_filters')
module_website_version = fields.Boolean("A/B Testing")
- module_website_links = fields.Boolean(string="Link Trackers")
+ module_website_links = fields.Boolean("Link Trackers")
+ auth_signup_uninvited = fields.Selection("Customer Account", related='website_id.auth_signup_uninvited')
favicon = fields.Binary('Favicon', related='website_id.favicon')
- # Set as global config parameter since methods using it are not website-aware. To be changed
- # when multi-website is implemented
- google_maps_api_key = fields.Char(string='Google Maps API Key', config_parameter='google_maps_api_key', default='')
- has_google_analytics = fields.Boolean("Google Analytics", config_parameter='website.has_google_analytics')
- has_google_analytics_dashboard = fields.Boolean("Embedded Google Analytics", config_parameter='website.has_google_analytics_dashboard')
- has_google_maps = fields.Boolean("Google Maps", config_parameter='website.has_google_maps')
- auth_signup_uninvited = fields.Selection([
- ('b2b', 'On invitation (B2B)'),
- ('b2c', 'Free sign up (B2C)'),
- ], string='Customer Account', default='b2b', config_parameter='auth_signup.invitation_scope')
+
+ google_maps_api_key = fields.Char(related='website_id.google_maps_api_key')
+ has_google_analytics = fields.Boolean(related='website_id.has_google_analytics')
+ has_google_analytics_dashboard = fields.Boolean(related='website_id.has_google_analytics_dashboard')
+ has_google_maps = fields.Boolean(related='website_id.has_google_maps')
+
+ social_twitter = fields.Char(related='website_id.social_twitter')
+ social_facebook = fields.Char(related='website_id.social_facebook')
+ social_github = fields.Char(related='website_id.social_github')
+ social_linkedin = fields.Char(related='website_id.social_linkedin')
+ social_youtube = fields.Char(related='website_id.social_youtube')
+ social_googleplus = fields.Char(related='website_id.social_googleplus')
+
+ group_multi_website = fields.Boolean("Multi-website", implied_group="website.group_multi_website")
@api.onchange('has_google_analytics')
def onchange_has_google_analytics(self):
@@ -83,3 +94,11 @@ class ResConfigSettings(models.TransientModel):
action['res_id'] = literal_eval(self.env['ir.config_parameter'].sudo().get_param('base.template_portal_user_id', 'False'))
action['views'] = [[self.env.ref('base.view_users_form').id, 'form']]
return action
+
+ def website_go_to(self):
+ self.website_id._force()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': '/',
+ 'target': 'self',
+ }
diff --git a/addons/website/models/res_partner.py b/addons/website/models/res_partner.py
index d3a3b1bc9f9..247804c387b 100644
--- a/addons/website/models/res_partner.py
+++ b/addons/website/models/res_partner.py
@@ -3,7 +3,8 @@
import werkzeug
-from odoo import api, models
+from odoo import api, fields, models
+from odoo.tools.pycompat import izip
def urlplus(url, params):
@@ -11,12 +12,13 @@ def urlplus(url, params):
class Partner(models.Model):
-
_inherit = "res.partner"
+ website_id = fields.Many2one('website', string='Registration Website')
+
@api.multi
def google_map_img(self, zoom=8, width=298, height=298):
- google_maps_api_key = self.env['ir.config_parameter'].sudo().get_param('google_maps_api_key')
+ google_maps_api_key = self.env['website'].get_current_website().google_maps_api_key
if not google_maps_api_key:
return False
params = {
@@ -35,3 +37,19 @@ class Partner(models.Model):
'z': zoom,
}
return urlplus('https://maps.google.com/maps', params)
+
+ @api.multi
+ def _get_name(self):
+ name = super(Partner, self)._get_name()
+ if self._context.get('display_website') and self.env.user.has_group('website.group_multi_website'):
+ if self.website_id:
+ name += ' [%s]' % self.website_id.name
+ return name
+
+ def _compute_display_name(self):
+ self2 = self.with_context(display_website=False)
+ super(Partner, self2)._compute_display_name()
+
+ # onchange uses the cache to retrieve value, we need to copy computed_value into the initial env
+ for record, record2 in izip(self, self2):
+ record.display_name = record2.display_name
diff --git a/addons/website/models/res_users.py b/addons/website/models/res_users.py
index d8eb61d9e7c..6af02a52ce3 100644
--- a/addons/website/models/res_users.py
+++ b/addons/website/models/res_users.py
@@ -1,7 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import logging
-from odoo import api, fields, models
+from odoo import api, fields, models, _
+from odoo.exceptions import ValidationError
+
+_logger = logging.getLogger(__name__)
class ResUsers(models.Model):
@@ -11,6 +15,17 @@ class ResUsers(models.Model):
selection=lambda self: self._get_group_selection('base.module_category_website'),
string='Website Editor', compute='_compute_groups_id', inverse='_inverse_groups_id',
category_xml_id='base.module_category_website')
+ has_group_multi_website = fields.Boolean(
+ 'Multi-Websites',
+ compute='_compute_groups_id', inverse='_inverse_groups_id',
+ group_xml_id='website.group_multi_website')
+
+ _sql_constraints = [
+ # this is done in Python because a SQL constraint like UNIQUE
+ # (login, website_id) allows ('abc', NULL) and
+ # ('abc', NULL) to coexist because of how SQL handles NULLs.
+ ('login_key', 'CHECK (1=1)', 'You can not have two users with the same login!')
+ ]
@api.multi
def _has_unsplash_key_rights(self):
@@ -18,3 +33,25 @@ class ResUsers(models.Model):
if self.has_group('website.group_website_designer'):
return True
return super(ResUsers, self)._has_unsplash_key_rights()
+
+ @api.constrains('login', 'website_id')
+ def _check_login(self):
+ for user in self:
+ if self.search([('id', '!=', user.id), ('login', '=', user.login)] + user.website_id.website_domain()):
+ raise ValidationError(_('You can not have two users with the same login!'))
+
+ @api.model
+ def _get_login_domain(self, login):
+ website = self.env['website'].get_current_website()
+ return super(ResUsers, self)._get_login_domain(login) + website.website_domain()
+
+ @api.model
+ def _signup_create_user(self, values):
+ new_user = super(ResUsers, self)._signup_create_user(values)
+ new_user.website_id = self.env['website'].get_current_website()
+ return new_user
+
+ @api.model
+ def _get_signup_invitation_scope(self):
+ current_website = self.env['website'].get_current_website()
+ return current_website.auth_signup_uninvited or super(ResUsers, self)._get_signup_invitation_scope()
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index 94d73420ccf..913334fc086 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -15,6 +15,7 @@ from odoo.addons.website.models.ir_http import sitemap_qs2dom
from odoo.addons.portal.controllers.portal import pager
from odoo.tools import pycompat
from odoo.http import request
+from odoo.osv import expression
from odoo.osv.expression import FALSE_DOMAIN
from odoo.tools.translate import _
@@ -33,9 +34,13 @@ DEFAULT_CDN_FILTERS = [
class Website(models.Model):
- _name = "website" # Avoid website.website convention for conciseness (for new api). Got a special authorization from xmo and rco
+ _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
@@ -44,33 +49,64 @@ class Website(models.Model):
def_lang = self.env['res.lang'].search([('code', '=', lang_code)], limit=1)
return def_lang.id if def_lang else self._active_languages()[0]
+ def _default_social_twitter(self):
+ return self.env.ref('base.main_company').social_twitter
+
+ 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_googleplus(self):
+ return self.env.ref('base.main_company').social_googleplus
+
name = fields.Char('Website Name')
domain = fields.Char('Website Domain')
+ 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.ref('base.main_company').id)
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(related='default_lang_id.code', string="Default language code", store=True)
+ default_lang_code = fields.Char("Default language code", related='default_lang_id.code', store=True)
auto_redirect_lang = fields.Boolean('Autoredirect Language', default=True, help="Should users be redirected to their browser's language")
- social_twitter = fields.Char(related="company_id.social_twitter")
- social_facebook = fields.Char(related="company_id.social_facebook")
- social_github = fields.Char(related="company_id.social_github")
- social_linkedin = fields.Char(related="company_id.social_linkedin")
- social_youtube = fields.Char(related="company_id.social_youtube")
- social_googleplus = fields.Char(related="company_id.social_googleplus")
+ 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_googleplus = fields.Char('Google+ Account', default=_default_social_googleplus)
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')
- user_id = fields.Many2one('res.users', string='Public User', required=True, default=lambda self: self.env.ref('base.public_user').id)
- cdn_activated = fields.Boolean('Activate CDN for assets')
+ google_maps_api_key = fields.Char('Google Maps API Key')
+ has_google_analytics = fields.Boolean("Google Analytics")
+ has_google_analytics_dashboard = fields.Boolean("Embedded Google Analytics")
+ has_google_maps = fields.Boolean("Google Maps")
+
+ 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', relation='res.partner', string='Public Partner')
menu_id = fields.Many2one('website.menu', compute='_compute_menu', string='Main Menu')
homepage_id = fields.Many2one('website.page', string='Homepage')
favicon = fields.Binary(string="Website Favicon", help="This field holds the image used to display a favicon on the website.")
+ theme_id = fields.Many2one('ir.module.module', help='Installed theme')
+
+ auth_signup_uninvited = fields.Selection([
+ ('b2b', 'On invitation'),
+ ('b2c', 'Free sign up'),
+ ], string='Customer Account', default='b2b')
@api.multi
def _compute_menu(self):
@@ -82,18 +118,69 @@ class Website(models.Model):
def noop(self, *args, **kwargs):
pass
+ @api.model
+ def create(self, 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()
+ return res
+
@api.multi
def write(self, values):
self._get_languages.clear_cache(self)
+ if 'company_id' in values and 'user_id' not in values:
+ company = self.env['res.company'].browse(values['company_id'])
+ values['user_id'] = company._get_public_user().id
+
result = super(Website, self).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()
return result
- #----------------------------------------------------------
+ @api.onchange('company_id')
+ def _onchange_company_id(self):
+ self.social_twitter = self.company_id.social_twitter
+ self.social_facebook = self.company_id.social_facebook
+ self.social_github = self.company_id.social_github
+ self.social_linkedin = self.company_id.social_linkedin
+ self.social_youtube = self.company_id.social_youtube
+ self.social_googleplus = self.company_id.social_googleplus
+
+ # ----------------------------------------------------------
# Page Management
- #----------------------------------------------------------
+ # ----------------------------------------------------------
+ def _bootstrap_homepage(self):
+ 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">
+%s
+ </t>
+</t>''' % (self.id, self.env['ir.ui.view'].render_template('website.default_homepage', values={'website': self}).decode())
+ standard_homepage.with_context(website_id=self.id).arch_db = new_homepage_view
+
+ self.homepage_id = self.env['website.page'].search([('website_id', '=', self.id),
+ ('key', '=', standard_homepage.key)])
+ top_menu = self.env['website.menu'].create({
+ 'name': _('Top Menu for website %s') % self.id,
+ 'website_id': self.id,
+ 'sequence': 0
+ })
+ self.menu_id = top_menu.id
+ self.env['website.menu'].create({
+ 'name': _('Home'),
+ 'url': '/',
+ 'website_id': self.id,
+ 'parent_id': top_menu.id,
+ 'sequence': 10
+ })
+
@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
@@ -127,20 +214,21 @@ class Website(models.Model):
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_ids': [(6, None, [self.get_current_website().id])],
- 'view_id': view.id
+ 'website_id': website.id, # remove it if only one webiste or not?
+ 'view_id': view.id,
})
result['view_id'] = view.id
if add_menu:
self.env['website.menu'].create({
'name': name,
'url': page_url,
- 'parent_id': self.get_current_website().menu_id.id,
+ 'parent_id': website.menu_id.id,
'page_id': page.id,
- 'website_id': self.get_current_website().id,
+ 'website_id': website.id,
})
return result
@@ -152,9 +240,11 @@ class Website(models.Model):
""" Given an url, return that url suffixed by counter if it already exists
:param page_url : the url to be checked for uniqueness
"""
- website_id = self.get_current_website().id
inc = 0
- domain_static = ['|', ('website_ids', '=', False), ('website_ids', 'in', website_id)]
+ # 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
@@ -167,17 +257,16 @@ class Website(models.Model):
: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
"""
- website_id = self.get_current_website().id
if template_module:
string = template_module + '.' + string
else:
if not string.startswith('website.'):
string = 'website.' + string
- #Look for unique key
+ # Look for unique key
key_copy = string
inc = 0
- domain_static = ['|', ('website_ids', '=', False), ('website_ids', 'in', website_id)]
+ 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 "")
@@ -185,10 +274,8 @@ class Website(models.Model):
def key_to_view_id(self, view_id):
return self.env['ir.ui.view'].search([
- ('id', '=', view_id),
- '|', ('website_id', '=', self._context.get('website_id')), ('website_id', '=', False),
- ('type', '=', 'qweb')
- ])
+ ('id', '=', view_id), ('type', '=', 'qweb')
+ ] + self.env['website'].website_domain(self._context.get('website_id')))
@api.model
def page_search_dependencies(self, page_id=False):
@@ -203,13 +290,11 @@ class Website(models.Model):
return dependencies
page = self.env['website.page'].browse(int(page_id))
- website_id = self._context.get('website_id')
+ website = self.env['website'].browse(self._context.get('website_id'))
url = page.url
# search for website_page with link
- website_page_search_dom = [
- '|', ('website_ids', 'in', website_id), ('website_ids', '=', False), ('view_id.arch_db', 'ilike', url)
- ]
+ 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:
@@ -225,10 +310,7 @@ class Website(models.Model):
page_view_ids.append(page.view_id.id)
# search for ir_ui_view (not from a website_page) with link
- page_search_dom = [
- '|', ('website_id', '=', website_id), ('website_id', '=', False),
- ('arch_db', 'ilike', url), ('id', 'not in', page_view_ids)
- ]
+ 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:
@@ -241,9 +323,7 @@ class Website(models.Model):
'item': _('%s (id:%s)') % (view.key or view.name, view.id),
})
# search for menu with link
- menu_search_dom = [
- '|', ('website_id', '=', website_id), ('website_id', '=', False), ('url', 'ilike', '%s' % url)
- ]
+ menu_search_dom = [('url', 'ilike', '%s' % url)] + website.website_domain()
menus = self.env['website.menu'].search(menu_search_dom)
menu_key = _('Menu')
@@ -271,14 +351,14 @@ class Website(models.Model):
return dependencies
page = self.env['website.page'].browse(int(page_id))
- website_id = self._context.get('website_id')
+ website = self.env['website'].browse(self._context.get('website_id'))
key = page.key
# search for website_page with link
website_page_search_dom = [
- '|', ('website_ids', 'in', website_id), ('website_ids', '=', False), ('view_id.arch_db', 'ilike', key),
- ('id', '!=', page.id),
- ]
+ ('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:
@@ -295,10 +375,9 @@ class Website(models.Model):
# search for ir_ui_view (not from a website_page) with link
page_search_dom = [
- '|', ('website_id', '=', website_id), ('website_id', '=', False),
('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:
@@ -306,26 +385,27 @@ class Website(models.Model):
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,
+ '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
- @api.model
- def page_exists(self, name, module='website'):
- try:
- name = (name or "").replace("/website.", "").replace("/", "")
- if not name:
- return False
- return self.env.ref('%s.%s' % module, name)
- except Exception:
- return False
-
- #----------------------------------------------------------
+ # removed by 3c9e6c89e7207636c1bdab4ed8118c0c1089d43e
+ # @api.model
+ # def page_exists(self, name, module='website'):
+ # try:
+ # name = (name or "").replace("/website.", "").replace("/", "")
+ # if not name:
+ # return False
+ # return self.env.ref('%s.%s' % module, name)
+ # except Exception:
+ # return False
+
+ # ----------------------------------------------------------
# Languages
- #----------------------------------------------------------
+ # ----------------------------------------------------------
@api.multi
def get_languages(self):
@@ -370,25 +450,46 @@ class Website(models.Model):
lang['hreflang'] = lang['short']
return langs
- #----------------------------------------------------------
+ # ----------------------------------------------------------
# Utilities
- #----------------------------------------------------------
+ # ----------------------------------------------------------
@api.model
def get_current_website(self):
+ if request and request.session.get('force_website_id'):
+ return self.browse(request.session['force_website_id'])
+
domain_name = request and request.httprequest.environ.get('HTTP_HOST', '').split(':')[0] or None
- website_id = self._get_current_website_id(domain_name)
+
+ country = request.session.geoip.get('country_code') if request and request.session.geoip else False
+ country_id = False
+ if country:
+ country_id = request.env['res.country'].search([('code', '=', country)], limit=1).id
+
+ website_id = self._get_current_website_id(domain_name, country_id)
if request:
request.context = dict(request.context, website_id=website_id)
return self.browse(website_id)
- @tools.cache('domain_name')
- def _get_current_website_id(self, domain_name):
- """ Reminder : cached method should be return record, since they will use a closed cursor. """
- website = self.search([('domain', '=', domain_name)], limit=1)
- if not website:
- website = self.search([], limit=1)
- return website.id
+ @tools.cache('domain_name', 'country_id')
+ def _get_current_website_id(self, domain_name, country_id):
+ # sort on country_group_ids so that we fall back on a generic website (empty country_group_ids)
+ websites = self.search([('domain', '=', domain_name)]).sorted('country_group_ids')
+
+ if not websites:
+ 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 int(website_id)
@api.model
def is_publisher(self):
@@ -429,13 +530,12 @@ class Website(models.Model):
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)
- and endpoint.routing.get('website')):
- return False
+ 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
spec = inspect.getargspec(endpoint.method.original_func)
@@ -495,7 +595,7 @@ class Website(models.Model):
for (i, (name, converter)) in enumerate(convitems):
newval = []
for val in values:
- query = i == len(convitems)-1 and query_string
+ 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)
@@ -527,7 +627,7 @@ class Website(models.Model):
domain = [('url', '!=', '/')]
if not force:
domain += [('website_indexed', '=', True)]
- #is_visible
+ # is_visible
domain += [('website_published', '=', True), '|', ('date_publish', '=', False), ('date_publish', '<=', fields.Datetime.now())]
if query_string:
@@ -545,7 +645,7 @@ class Website(models.Model):
@api.multi
def get_website_pages(self, domain=[], order='name', limit=None):
- domain += ['|', ('website_ids', 'in', self.get_current_website().id), ('website_ids', '=', False)]
+ domain += self.get_current_website().website_domain()
pages = request.env['website.page'].search(domain, order='name', limit=limit)
return pages
@@ -595,11 +695,28 @@ class SeoMetadata(models.AbstractModel):
website_meta_keywords = fields.Char("Website meta keywords", translate=True)
+class WebsiteMultiMixin(models.AbstractModel):
+
+ _name = 'website.multi.mixin'
+
+ website_id = fields.Many2one('website', string='Website', help='Restrict publishing to this website.')
+
+ @api.multi
+ def can_access_from_current_website(self, website_id=False):
+ can_access = True
+ for record in self:
+ if (website_id or record.website_id.id) not in (False, request.website.id):
+ can_access = False
+ continue
+ return can_access
+
+
class WebsitePublishedMixin(models.AbstractModel):
_name = "website.published.mixin"
- website_published = fields.Boolean('Visible in Website', copy=False)
+ website_published = fields.Boolean('Visible on current website', related='is_published')
+ is_published = fields.Boolean('Is published')
website_url = fields.Char('Website URL', compute='_compute_website_url', help='The full URL to access the document through the website.')
@api.multi
@@ -611,6 +728,9 @@ class WebsitePublishedMixin(models.AbstractModel):
def website_publish_button(self):
self.ensure_one()
if self.env.user.has_group('website.group_website_publisher') and self.website_url != '#':
+ # Force website to land on record's website to publish/unpublish it
+ if 'website_id' in self and self.env.user.has_group('website.group_multi_website'):
+ self.website_id._force()
return self.open_website_url()
return self.write({'website_published': not self.website_published})
@@ -622,14 +742,56 @@ class WebsitePublishedMixin(models.AbstractModel):
}
+class WebsitePublishedMultiMixin(WebsitePublishedMixin):
+
+ _name = 'website.published.multi.mixin'
+ _inherit = ['website.published.mixin', 'website.multi.mixin']
+
+ website_published = fields.Boolean(compute='_compute_website_published',
+ inverse='_inverse_website_published',
+ search='_search_website_published',
+ related=False)
+
+ @api.multi
+ @api.depends('is_published', 'website_id')
+ def _compute_website_published(self):
+ current_website_id = self._context.get('website_id')
+ for record in self:
+ if current_website_id:
+ record.website_published = record.is_published and (not record.website_id or record.website_id.id == current_website_id)
+ else:
+ record.website_published = record.is_published
+
+ @api.multi
+ def _inverse_website_published(self):
+ for record in self:
+ record.is_published = record.website_published
+
+ def _search_website_published(self, operator, value):
+ if not isinstance(value, bool) or operator not in ('=', '!='):
+ logger.warning('unsupported search on website_published: %s, %s', operator, value)
+ return [()]
+
+ if operator in expression.NEGATIVE_TERM_OPERATORS:
+ value = not value
+
+ current_website_id = self._context.get('website_id')
+ is_published = [('is_published', '=', value)]
+ if current_website_id:
+ on_current_website = self.env['website'].website_domain(current_website_id)
+ return (['!'] if value is False else []) + expression.AND([is_published, on_current_website])
+ else: # should be in the backend, return things that are published anywhere
+ return is_published
+
+
class Page(models.Model):
_name = 'website.page'
_inherits = {'ir.ui.view': 'view_id'}
- _inherit = 'website.published.mixin'
+ _inherit = 'website.published.multi.mixin'
_description = 'Page'
+ _order = 'website_id'
url = fields.Char('Page URL')
- website_ids = fields.Many2many('website', string='Websites')
view_id = fields.Many2one('ir.ui.view', string='View', required=True, ondelete="cascade")
website_indexed = fields.Boolean('Page Indexed', default=True)
date_publish = fields.Datetime('Publishing Date')
@@ -642,6 +804,9 @@ class Page(models.Model):
header_overlay = fields.Boolean()
header_color = fields.Char()
+ # don't use mixin website_id but use website_id on ir.ui.view instead
+ website_id = fields.Many2one(related='view_id.website_id', store=True)
+
@api.one
def _compute_homepage(self):
self.is_homepage = self == self.env['website'].get_current_website().homepage_id
@@ -660,11 +825,21 @@ class Page(models.Model):
def _compute_visible(self):
self.is_visible = self.website_published and (not self.date_publish or self.date_publish < fields.Datetime.now())
+ @api.multi
+ def _is_most_specific_page(self, page_to_test):
+ '''This will test if page_to_test is the most specific page in self.'''
+ pages_for_url = self.sorted(key=lambda p: not p.website_id).filtered(lambda page: page.url == page_to_test.url)
+
+ # this works because pages are _order'ed by website_id
+ most_specific_page = pages_for_url[0]
+
+ return most_specific_page == page_to_test
+
@api.model
- def get_page_info(self, id, website_id):
- domain = ['|', ('website_ids', 'in', website_id), ('website_ids', '=', False), ('id', '=', id)]
- item = self.search_read(domain, fields=['id', 'name', 'url', 'website_published', 'website_indexed', 'date_publish', 'menu_ids', 'is_homepage'], limit=1)
- return item
+ def get_page_info(self, id):
+ return self.browse(id).read(
+ ['id', 'name', 'url', 'website_published', 'website_indexed', 'date_publish', 'menu_ids', 'is_homepage', 'website_id'],
+ )
@api.multi
def get_view_identifier(self):
@@ -676,7 +851,7 @@ class Page(models.Model):
website = self.env['website'].browse(website_id)
page = self.browse(int(data['id']))
- #If URL has been edited, slug it
+ # If URL has been edited, slug it
original_url = page.url
url = data['url']
if not url.startswith('/'):
@@ -685,7 +860,7 @@ class Page(models.Model):
url = '/' + slugify(url, max_length=1024, path=True)
url = self.env['website'].get_unique_path(url)
- #If name has changed, check for key uniqueness
+ # If name has changed, check for key uniqueness
if page.name != data['name']:
page_key = self.env['website'].get_unique_key(slugify(data['name']))
else:
@@ -693,11 +868,11 @@ class Page(models.Model):
menu = self.env['website.menu'].search([('page_id', '=', int(data['id']))])
if not data['is_menu']:
- #If the page is no longer in menu, we should remove its website_menu
+ # If the page is no longer in menu, we should remove its website_menu
if menu:
menu.unlink()
else:
- #The page is now a menu, check if has already one
+ # The page is now a menu, check if has already one
if menu:
menu.write({'url': url})
else:
@@ -709,15 +884,23 @@ class Page(models.Model):
'website_id': website.id,
})
- page.write({
+ # Edits via the page manager shouldn't trigger the COW
+ # mechanism and generate new pages. The user manages page
+ # visibility manually with is_published here.
+ w_vals = {
'key': page_key,
'name': data['name'],
'url': url,
- 'website_published': data['website_published'],
+ 'is_published': data['website_published'],
+ 'website_id': False if data['share_page_info'] else website.id,
'website_indexed': data['website_indexed'],
'date_publish': data['date_publish'] or None,
'is_homepage': data['is_homepage'],
- })
+ }
+ # toggle is hidden to prevent user to unshare a page
+ if 'share_page_info' in data:
+ w_vals['website_id'] = False if data['share_page_info'] else website.id
+ page.with_context(no_cow=True).write(w_vals)
# Create redirect if needed
if data['create_redirect']:
@@ -733,17 +916,14 @@ class Page(models.Model):
@api.multi
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
- view = self.env['ir.ui.view'].browse(self.view_id.id)
- # website.page's ir.ui.view should have a different key than the one it
- # is copied from.
- # (eg: website_version: an ir.ui.view record with the same key is
- # expected to be the same ir.ui.view but from another version)
- new_view = view.copy({'key': view.key + '.copy', 'name': '%s %s' % (view.name, _('(copy)'))})
- default = {
- 'name': '%s %s' % (self.name, _('(copy)')),
- 'url': self.env['website'].get_unique_path(self.url),
- 'view_id': new_view.id,
- }
+ if default:
+ if not default.get('view_id'):
+ view = self.env['ir.ui.view'].browse(self.view_id.id)
+ new_view = view.copy({'website_id': default.get('website_id')})
+ default['view_id'] = new_view.id
+
+ default['name'] = default.get('name', '%s %s' % (self.name, _('(copy)')))
+ default['url'] = default.get('url', self.env['website'].get_unique_path(self.url))
return super(Page, self).copy(default=default)
@api.model
@@ -752,53 +932,38 @@ class Page(models.Model):
:param page_id : website.page identifier
"""
page = self.browse(int(page_id))
- new_page = page.copy()
- if clone_menu:
+ new_page = page.copy(dict(website_id=self.env['website'].get_current_website().id))
+ # Should not clone menu if the page was cloned from one website to another
+ # Eg: Cloning a generic page (no website) will create a page with a website, we can't clone menu (not same container)
+ if clone_menu and new_page.website_id == page.website_id:
menu = self.env['website.menu'].search([('page_id', '=', page_id)], limit=1)
if menu:
# If the page being cloned has a menu, clone it too
new_menu = menu.copy()
- new_menu.write({'url': new_page.url, 'name': '%s %s' % (menu.name, _('(copy)')), 'page_id': new_page.id})
+ new_menu.write({'url': new_page.url, 'name': '%s %s' % (menu.name, _('(copy)')), 'page_id': new_page.id})
return new_page.url + '?enable_editor=1'
@api.multi
def unlink(self):
- """ When a website_page is deleted, the ORM does not delete its ir_ui_view.
- So we got to delete it ourself, but only if the ir_ui_view is not used by another website_page.
- """
- # Handle it's ir_ui_view
+ # When a website_page is deleted, the ORM does not delete its
+ # ir_ui_view. So we got to delete it ourself, but only if the
+ # ir_ui_view is not used by another website_page.
for page in self:
# Other pages linked to the ir_ui_view of the page being deleted (will it even be possible?)
pages_linked_to_iruiview = self.search(
- [('view_id', '=', self.view_id.id), ('id', '!=', self.id)]
+ [('view_id', '=', page.view_id.id), ('id', '!=', page.id)]
)
- if len(pages_linked_to_iruiview) == 0:
+ if len(pages_linked_to_iruiview) == 0 and not page.view_id.inherit_children_ids:
# If there is no other pages linked to that ir_ui_view, we can delete the ir_ui_view
- self.env['ir.ui.view'].search([('id', '=', self.view_id.id)]).unlink()
- # And then delete the website_page itself
+ page.view_id.unlink()
return super(Page, self).unlink()
- @api.model
- def delete_page(self, page_id):
- """ Delete a page, given its identifier
- :param page_id : website.page identifier
- """
- # If we are deleting a page (that could possibly be a menu with a page)
- page = self.env['website.page'].browse(int(page_id))
- if page:
- # Check if it is a menu with a page and also delete menu if so
- menu = self.env['website.menu'].search([('page_id', '=', page.id)], limit=1)
- if menu:
- menu.unlink()
- page.unlink()
-
@api.multi
def write(self, vals):
if 'url' in vals and not vals['url'].startswith('/'):
vals['url'] = '/' + vals['url']
- result = super(Page, self).write(vals)
- return result
+ return super(Page, self).write(vals)
class Menu(models.Model):
@@ -815,15 +980,45 @@ class Menu(models.Model):
name = fields.Char('Menu', required=True, translate=True)
url = fields.Char('Url', default='')
- page_id = fields.Many2one('website.page', 'Related Page')
+ page_id = fields.Many2one('website.page', 'Related Page', ondelete='cascade')
new_window = fields.Boolean('New Window')
sequence = fields.Integer(default=_default_sequence)
- website_id = fields.Many2one('website', 'Website') # TODO: support multiwebsite once done for ir.ui.views
+ website_id = fields.Many2one('website', 'Website')
parent_id = fields.Many2one('website.menu', 'Parent Menu', index=True, ondelete="cascade")
child_id = fields.One2many('website.menu', 'parent_id', string='Child Menus')
parent_path = fields.Char(index=True)
is_visible = fields.Boolean(compute='_compute_visible', string='Is Visible')
+ @api.multi
+ def name_get(self):
+ res = []
+ for menu in self:
+ website_suffix = '%s - %s' % (menu.name, menu.website_id.name)
+ res.append((menu.id, website_suffix if menu.website_id and self.env.user.has_group('website.group_multi_website') else menu.name))
+ return res
+
+ @api.model
+ def create(self, vals):
+ ''' In case a menu without a website_id is trying to be created, we duplicate
+ it for every website.
+ Note: Particulary useful when installing a module that adds a menu like
+ /shop. So every website has the shop menu.
+ '''
+ if vals.get('website_id'):
+ return super(Menu, self).create(vals)
+ elif self._context.get('website_id'):
+ vals['website_id'] = self._context.get('website_id')
+ return super(Menu, self).create(vals)
+ else:
+ # create for every site
+ for website in self.env['website'].search([]):
+ vals.update({
+ 'website_id': website.id,
+ 'parent_id': website.menu_id.id,
+ })
+ res = super(Menu, self).create(vals)
+ return res # Only one record is returned but multiple could have been created
+
@api.one
def _compute_visible(self):
visible = True
@@ -886,7 +1081,7 @@ class Menu(models.Model):
mid = menu['id']
# new menu are prefixed by new-
if isinstance(mid, pycompat.string_types):
- new_menu = self.create({'name': menu['name']})
+ new_menu = self.create({'name': menu['name'], 'website_id': website_id})
replace_id(mid, new_menu.id)
for menu in data['data']:
menu_id = self.browse(menu['id'])