summaryrefslogtreecommitdiff
path: root/addons/website/models/ir_ui_view.py
diff options
context:
space:
mode:
authorJoren Van Onder <jov@odoo.com>2018-08-12 22:40:50 +0200
committerJeremy Kersten <jke@odoo.com>2018-08-13 19:51:10 +0200
commit4f6ec1cd2a9253bb0c6c17d304490c7809facf8a (patch)
tree3ef047b311595b8f89d74adf90b2f891e7bba2a3 /addons/website/models/ir_ui_view.py
parent5ff87e8039676db5a0c48422998d0cabd556a12d (diff)
[IMP] website,*: support multiple websites
This implements support to administer multiple websites. Although the core functionality already existed, managing multiple websites was fairly technical. In the interest of database updates and migration this attempts to keep duplicated data to a minimum. To do this the usual generic records are rendered unless some website-specific record exists that replaces it. Copy-on-write (COW) is used to create these website-specific records. Through this mechanism creating a website-specific record is delayed until necessary. A COW mechanism has been implemented on 4 models: ir.ui.view, website.page, website.menu and ir.attachment. These COW mechanisms are activated when editing data through the website (aka frontend). These frontend edits (e.g. with web_editor) will be website-specific, possibly creating a website-specific record when necessary. When editing data in the backend nothing special will happen, even when editing a generic record. Note that because of this mechanism also facilitates the ability to create new, uncustomized websites because the generic data is kept. Support is provided for a website to have any theme. Themes are fairly complex to handle. Standalone themes can depend on other standalone themes (e.g. theme_beauty depends on theme_loftspace) and themes usually modify some data of the themes they depend on. Because a theme can be installed on multiple websites, using website_id m2o fields does not work well. It would require duplicate data, making updates and migration harder. Because of this, data for themes (ir.ui.view and ir.attachment specifically) have a theme_id m2o. website has a theme_ids m2m that identifies all theme modules currently installed on it. Through these fields we figure out what to render. A theme is only fully uninstalled when it's no longer active on any website. The advantage of this approach is that upgrading or migrating theme data is no different from the single-website case. The website.published.mixin class was modified to handle multiple websites. A wizard was added in the backend to easily manage this for multiple website. Although not used anywhere in this commit, a 'website_id' variable has been added in the evaluation context of ir.rule. It allows to easily make any model multi-website aware, all that's needed is a custom website_id m2o field on a model and a custom record rule.
Diffstat (limited to 'addons/website/models/ir_ui_view.py')
-rw-r--r--addons/website/models/ir_ui_view.py152
1 files changed, 149 insertions, 3 deletions
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index 9a793b84fe6..8a8e5a3f8f3 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -4,9 +4,10 @@
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
@@ -22,6 +23,7 @@ class View(models.Model):
website_id = fields.Many2one('website', ondelete='cascade', string="Website")
page_ids = fields.One2many('website.page', compute='_compute_page_ids', store=False)
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
def _compute_first_page_id(self):
@@ -34,12 +36,112 @@ class View(models.Model):
)
@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:
+ currently_updating = self._context.get('install_mode_data', {}).get('module', '')
+ if 'theme_' in currently_updating:
+ current_website_id = False
+ view = view._get_theme_specific_view(currently_updating)
+
+ # 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))
+
+ # 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({'key': '%s [website %s]' % (view.key, website.id)})
+
+ self |= self.with_context(active_test=False).search([('key', 'in', self.filtered('key').mapped('key'))])
result = super(View, self).unlink()
self.clear_caches()
return result
@api.multi
+ def _get_theme_specific_view(self, theme_name):
+ self.ensure_one()
+ view = self
+ module_being_updated = self.env['ir.module.module'].search([('name', '=', theme_name)])
+ xml_id = self.env['ir.model.data'].search([('model', '=', 'ir.ui.view'), ('res_id', '=', view.id)])
+ if xml_id and xml_id.module != theme_name:
+ _logger.info('%s is updating view %s (ID: %s)', theme_name, xml_id.complete_name, view.id)
+
+ # check if a previously copied view for this theme already exists
+ theme_specific_view = self.env['ir.ui.view'].search([('key', '=', view.key), ('theme_id', '=', module_being_updated.id)])
+ if theme_specific_view:
+ view = theme_specific_view
+ _logger.info('diverting write to %s (ID: %s)', view.name, view.id)
+ else:
+ view = view.copy({'theme_id': module_being_updated.id})
+ _logger.info('created new theme-specific view %s (ID: %s)', view.name, view.id)
+
+ return view
+
+ def _create_website_specific_pages_for_view(self, new_view, website):
+ for page in self.page_ids:
+ # create new pages for this view
+ new_page = page.copy({
+ 'view_id': new_view.id,
+ })
+ for menu in page.menu_ids:
+ # trigger COW
+ menu.write({'page_id': new_page.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
Suitability of a view is defined as follow:
@@ -80,15 +182,47 @@ 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 = [('theme_id', '=', False), '|', ('website_id', '=', False), ('website_id', '=', current_website.id)]
+ # 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]
+ if current_website.theme_ids:
+ theme_views_domain = [('theme_id', 'in', current_website.theme_ids.ids)]
+ website_views_domain = expression.OR([website_views_domain, theme_views_domain])
+
+ 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'))
+ key_domain = [('key', '=', xml_id)]
+ theme_views_domain = [('theme_id', 'in', current_website.theme_ids.ids)]
+ website_views_domain = [('theme_id', '=', False), '|', ('website_id', '=', False), ('website_id', '=', current_website.id)]
+ domain = expression.AND([expression.OR([theme_views_domain, website_views_domain]), key_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 +259,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 +267,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,