[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.
This commit is contained in:
committed by
Jeremy Kersten
parent
5ff87e8039
commit
4f6ec1cd2a
@@ -95,7 +95,7 @@ class AuthSignupHome(Home):
|
||||
|
||||
get_param = request.env['ir.config_parameter'].sudo().get_param
|
||||
return {
|
||||
'signup_enabled': get_param('auth_signup.invitation_scope', 'b2b') == 'b2c',
|
||||
'signup_enabled': request.env['res.users']._get_signup_invitation_scope() == 'b2c',
|
||||
'reset_password_enabled': get_param('auth_signup.reset_password') == 'True',
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ class ResPartner(models.Model):
|
||||
"""
|
||||
res = defaultdict(dict)
|
||||
|
||||
allow_signup = self.env['ir.config_parameter'].sudo().get_param('auth_signup.invitation_scope', 'b2b') == 'b2c'
|
||||
allow_signup = self.env['res.users']._get_signup_invitation_scope() == 'b2c'
|
||||
for partner in self:
|
||||
if allow_signup and not partner.user_ids:
|
||||
partner = partner.sudo()
|
||||
|
||||
@@ -103,13 +103,17 @@ class ResUsers(models.Model):
|
||||
|
||||
return (self.env.cr.dbname, values.get('login'), values.get('password'))
|
||||
|
||||
@api.model
|
||||
def _get_signup_invitation_scope(self):
|
||||
return self.env['ir.config_parameter'].sudo().get_param('auth_signup.invitation_scope', 'b2b')
|
||||
|
||||
@api.model
|
||||
def _signup_create_user(self, values):
|
||||
""" signup a new user using the template user """
|
||||
|
||||
# check that uninvited users may sign up
|
||||
if 'partner_id' not in values:
|
||||
if self.env['ir.config_parameter'].sudo().get_param('auth_signup.invitation_scope', 'b2b') != 'b2c':
|
||||
if self._get_signup_invitation_scope() != 'b2c':
|
||||
raise SignupError(_('Signup is not allowed for uninvited users'))
|
||||
return self._create_user_from_template(values)
|
||||
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
'views/website_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/ir_actions_views.xml',
|
||||
'views/ir_attachment_views.xml',
|
||||
'views/res_users_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'wizard/base_language_install_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
|
||||
@@ -8,7 +8,7 @@ from odoo.http import request
|
||||
class WebsiteBackend(http.Controller):
|
||||
|
||||
@http.route('/website/fetch_dashboard_data', type="json", auth='user')
|
||||
def fetch_dashboard_data(self, date_from, date_to):
|
||||
def fetch_dashboard_data(self, website_id, date_from, date_to):
|
||||
has_group_system = request.env.user.has_group('base.group_system')
|
||||
has_group_designer = request.env.user.has_group('website.group_website_designer')
|
||||
dashboard_data = {
|
||||
@@ -21,6 +21,13 @@ class WebsiteBackend(http.Controller):
|
||||
'visits': {},
|
||||
}
|
||||
}
|
||||
|
||||
current_website = request.env['website'].browse(website_id) if website_id else request.env['website'].get_current_website()
|
||||
dashboard_data['websites'] = request.env['website'].search_read([], ['id', 'name'])
|
||||
for website in dashboard_data['websites']:
|
||||
if website['id'] == current_website.id:
|
||||
website['selected'] = True
|
||||
|
||||
if has_group_designer:
|
||||
config = request.env['res.config.settings'].sudo().create({})
|
||||
if config.has_google_analytics_dashboard:
|
||||
|
||||
@@ -81,6 +81,11 @@ class Website(Home):
|
||||
|
||||
raise request.not_found()
|
||||
|
||||
@http.route('/website/set_session_website', type='json', auth="user")
|
||||
def set_session_website(self, website_id):
|
||||
request.env['website'].browse(int(website_id) if website_id else False)._fix_to_session()
|
||||
return True
|
||||
|
||||
#------------------------------------------------------
|
||||
# Login - overwrite of the web login so that regular users are redirected to the backend
|
||||
# while portal users are redirected to the frontend by default
|
||||
@@ -221,7 +226,7 @@ class Website(Home):
|
||||
# default sortby order
|
||||
sort_order = searchbar_sortings.get(sortby, 'name')['order']
|
||||
|
||||
domain = ['|', ('website_ids', 'in', request.website.id), ('website_ids', '=', False)]
|
||||
domain = ['|', ('website_id', '=', False), ('website_id', '=', request.website.id)]
|
||||
if search:
|
||||
domain += ['|', ('name', 'ilike', search), ('url', 'ilike', search)]
|
||||
|
||||
@@ -236,6 +241,9 @@ class Website(Home):
|
||||
)
|
||||
pages = Page.search(domain, order=sort_order, limit=50, offset=pager['offset'])
|
||||
|
||||
# only include pages if they are the most specific one available
|
||||
pages = pages.filtered(pages._is_most_specific_page)
|
||||
|
||||
values = {
|
||||
'pager': pager,
|
||||
'pages': pages,
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
<record id="main_menu" model="website.menu">
|
||||
<field name="name">Top Menu</field>
|
||||
<field name="website_id" ref="default_website"/>
|
||||
</record>
|
||||
|
||||
<record id="menu_homepage" model="website.menu">
|
||||
@@ -19,7 +18,6 @@
|
||||
<field name="url">/</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">10</field>
|
||||
<field name="website_id" ref="default_website"/>
|
||||
</record>
|
||||
|
||||
<record id="menu_contactus" model="website.menu">
|
||||
@@ -27,7 +25,6 @@
|
||||
<field name="url">/contactus</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">60</field>
|
||||
<field name="website_id" ref="default_website"/>
|
||||
</record>
|
||||
|
||||
<!-- split between ir_ui_view & website_page record to keep external_id on ir_ui_view -->
|
||||
@@ -579,4 +576,9 @@
|
||||
<field name="url">/website/static/src/img/snippets_demo/s_team_member_4.png</field>
|
||||
</record>
|
||||
</data>
|
||||
<data>
|
||||
<record id="group_multi_website" model="res.groups">
|
||||
<field name="name">Multi-website</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -131,53 +131,6 @@ response = request.render("website.template_partner_comment", {
|
||||
<field name="domain">0.0.0.0</field>
|
||||
</record>
|
||||
|
||||
<record id="website2_homepage" model="ir.ui.view">
|
||||
<field name="name">Home</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="key">website2.homepage</field>
|
||||
<field name="arch" type="xml">
|
||||
<t name="Home" priority="29" t-name="website2.homepage">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="oe_structure oe_empty">
|
||||
<div class="carousel slide mb32" id="myCarousel0" style="height: 320px;">
|
||||
<ol class="carousel-indicators d-none">
|
||||
<li class="active" data-slide-to="0" data-target="#myCarousel0"/>
|
||||
</ol>
|
||||
<div class="carousel-inner">
|
||||
<div class="carousel-item image_text oe_img_bg active" style="background-image: url(http://0.0.0.0:8069/web/image/website.s_background_image_11);">
|
||||
<div class="container">
|
||||
<div class="row content">
|
||||
<div class="carousel-content col-lg-6 col-md-12">
|
||||
<h2>Homepage 0.0.0.0</h2>
|
||||
<h3>Click to customize this text</h3>
|
||||
<p>
|
||||
<a role="button" class="btn btn-success btn-large" href="/contactus">Contact us</a>
|
||||
</p>
|
||||
</div>
|
||||
<span class="carousel-img col-lg-6 d-none d-lg-block"> </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel-control-prev d-none" data-slide="prev" data-target="#myCarousel0" href="#myCarousel0" style="width: 10%" role="img" aria-label="Previous" title="Previous">
|
||||
<i class="fa fa-chevron-left"/>
|
||||
</div>
|
||||
<div class="carousel-control-next d-none" data-slide="next" data-target="#myCarousel0" href="#myCarousel0" style="width: 10%" role="img" aria-label="Next" title="Next">
|
||||
<i class="fa fa-chevron-right"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<record id="website2_homepage_page" model="website.page">
|
||||
<field name="website_published">True</field>
|
||||
<field name="url">/</field>
|
||||
<field name="view_id" ref="website2_homepage" />
|
||||
<field name="website_ids" eval="[(4, ref('website2'))]" />
|
||||
</record>
|
||||
|
||||
<record id="website2_contactus" model="ir.ui.view">
|
||||
<field name="name">Contact Us</field>
|
||||
<field name="type">qweb</field>
|
||||
@@ -216,29 +169,16 @@ response = request.render("website.template_partner_comment", {
|
||||
<field name="website_published">True</field>
|
||||
<field name="url">/contactus</field>
|
||||
<field name="view_id" ref="website2_contactus" />
|
||||
<field name="website_ids" eval="[(4, ref('website2'))]" />
|
||||
<field name="website_id" eval="ref('website2')" />
|
||||
</record>
|
||||
|
||||
<!-- Menu & Homepage -->
|
||||
<record id="website2" model="website">
|
||||
<field name="homepage_id" ref="website2_homepage_page" />
|
||||
</record>
|
||||
<record id="website2_main_menu" model="website.menu">
|
||||
<field name="name">Top Menu</field>
|
||||
<field name="website_id" ref="website2"/>
|
||||
</record>
|
||||
<record id="website2_menu_homepage" model="website.menu">
|
||||
<field name="name">Home</field>
|
||||
<field name="url">/</field>
|
||||
<field name="parent_id" ref="website.website2_main_menu"/>
|
||||
<field name="sequence" type="int">10</field>
|
||||
<field name="website_id" ref="website2"/>
|
||||
<field name="page_id" ref="website2_homepage_page" />
|
||||
</record>
|
||||
<record id="website2_menu_contactus" model="website.menu">
|
||||
<field name="name">Contact us</field>
|
||||
<field name="url">/contactus</field>
|
||||
<field name="parent_id" ref="website.website2_main_menu"/>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">60</field>
|
||||
<field name="website_id" ref="website2"/>
|
||||
<field name="page_id" ref="website2_contactus_page" />
|
||||
|
||||
@@ -10,5 +10,6 @@ from . import website
|
||||
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
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class Attachment(models.Model):
|
||||
|
||||
@@ -14,3 +16,50 @@ class Attachment(models.Model):
|
||||
@api.model
|
||||
def get_serving_groups(self):
|
||||
return super(Attachment, self).get_serving_groups() + ['website.group_website_designer']
|
||||
|
||||
key = fields.Char(help='Technical field used to resolve multiple attachments in a multi-website environment.')
|
||||
theme_id = fields.Many2one('ir.module.module')
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
self |= self.search([('key', 'in', self.filtered('key').mapped('key'))])
|
||||
return super(Attachment, self).unlink()
|
||||
|
||||
@api.multi
|
||||
def _get_theme_specific_attachment(self, theme_name):
|
||||
self.ensure_one()
|
||||
attachment = self
|
||||
module_being_updated = self.env['ir.module.module'].search([('name', '=', theme_name)])
|
||||
xml_id = self.env['ir.model.data'].search([('model', '=', 'ir.attachment'), ('res_id', '=', attachment.id)])
|
||||
if xml_id and xml_id.module != theme_name:
|
||||
_logger.info('%s is updating attachment %s (ID: %s)', theme_name, xml_id.complete_name, attachment.id)
|
||||
|
||||
# check if a previously copied attachment for this theme already exists
|
||||
theme_specific_attachment = self.env['ir.attachment'].search([('key', '=', attachment.key), ('theme_id', '=', module_being_updated.id)])
|
||||
if theme_specific_attachment:
|
||||
attachment = theme_specific_attachment
|
||||
_logger.info('diverting write to %s (ID: %s)', attachment.name, attachment.id)
|
||||
else:
|
||||
attachment.with_context(no_cow=True).key = xml_id.complete_name
|
||||
attachment = attachment.copy({
|
||||
'theme_id': module_being_updated.id,
|
||||
'key': xml_id.complete_name,
|
||||
})
|
||||
_logger.info('created new theme-specific attachment %s (ID: %s)', attachment.name, attachment.id)
|
||||
|
||||
return attachment
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
if not self._context.get('no_cow'):
|
||||
for attachment in self:
|
||||
currently_updating = self._context.get('install_mode_data', {}).get('module', '')
|
||||
if currently_updating.startswith('theme_'):
|
||||
attachment = attachment._get_theme_specific_attachment(currently_updating)
|
||||
|
||||
super(Attachment, attachment).write(vals)
|
||||
else:
|
||||
super(Attachment, self).write(vals)
|
||||
|
||||
return True
|
||||
>>>>>>> [IMP] website,*: support multiple websites
|
||||
|
||||
@@ -117,9 +117,16 @@ class Http(models.AbstractModel):
|
||||
@classmethod
|
||||
def _serve_page(cls):
|
||||
req_page = request.httprequest.path
|
||||
page_domain = [('url', '=', req_page), '|', ('website_id', '=', False), ('website_id', '=', request.website.id)]
|
||||
|
||||
domain = [('url', '=', req_page), '|', ('website_ids', 'in', request.website.id), ('website_ids', '=', False)]
|
||||
pages = request.env['website.page'].search(domain)
|
||||
published_domain = page_domain + [('website_published', '=', True)]
|
||||
pages = request.env['website.page'].search(published_domain)
|
||||
|
||||
if not pages:
|
||||
# Since there are no published pages, try to find a page
|
||||
# that could potentially be published.
|
||||
unpublished_domain = page_domain + [('website_published', '=', False)]
|
||||
pages = request.env['website.page'].search(unpublished_domain)
|
||||
|
||||
if not request.website.is_publisher():
|
||||
pages = pages.filtered('is_visible')
|
||||
@@ -239,7 +246,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,6 +258,15 @@ 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.installed_theme_id:
|
||||
obj = env['ir.attachment'].search([('key', '=', xmlid), ('theme_id', '=', website_id.installed_theme_id.id)])
|
||||
if obj:
|
||||
return obj[0]
|
||||
|
||||
return super(Http, cls)._xmlid_to_obj(env, xmlid)
|
||||
|
||||
class ModelConverter(ModelConverter):
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# 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._context.get('website_id')
|
||||
return res
|
||||
@@ -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):
|
||||
@@ -33,12 +35,112 @@ class View(models.Model):
|
||||
[('view_id', '=', self.id)]
|
||||
)
|
||||
|
||||
@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
|
||||
@@ -79,16 +181,48 @@ class View(models.Model):
|
||||
# assume it's already a view object (WTF?)
|
||||
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,
|
||||
|
||||
@@ -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])],
|
||||
})
|
||||
|
||||
@@ -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(
|
||||
@@ -34,6 +37,7 @@ class ResConfigSettings(models.TransientModel):
|
||||
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")
|
||||
auth_signup_uninvited = fields.Selection(string="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
|
||||
@@ -42,10 +46,15 @@ class ResConfigSettings(models.TransientModel):
|
||||
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')
|
||||
|
||||
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 +92,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._fix_to_session()
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '/',
|
||||
'target': 'self',
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import werkzeug
|
||||
|
||||
from odoo import api, models
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
def urlplus(url, params):
|
||||
@@ -11,9 +11,10 @@ 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')
|
||||
|
||||
@@ -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,10 +15,46 @@ 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-Website',
|
||||
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):
|
||||
self.ensure_one()
|
||||
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),
|
||||
'|', ('website_id', '=', False),
|
||||
('website_id', '=', user.website_id.id)]):
|
||||
raise ValidationError(_('You can not have two users with the same login!'))
|
||||
|
||||
@api.model
|
||||
def _get_login_domain(self, login):
|
||||
current_website_id = self.env['website'].get_current_website().id
|
||||
multi_website_domain = ['|', ('website_id', '=', False), ('website_id', '=', current_website_id)]
|
||||
return super(ResUsers, self)._get_login_domain(login) + multi_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()
|
||||
|
||||
@@ -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 _
|
||||
|
||||
@@ -44,26 +45,46 @@ 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)
|
||||
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)
|
||||
user_id = fields.Many2one('res.users', string='Public User', required=True)
|
||||
cdn_activated = fields.Boolean('Activate CDN for assets')
|
||||
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")
|
||||
@@ -71,29 +92,73 @@ class Website(models.Model):
|
||||
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.")
|
||||
installed_theme_id = fields.Many2one('ir.module.module')
|
||||
theme_ids = fields.Many2many('ir.module.module', 'website_theme', 'website_id', 'ir_module_module_id')
|
||||
auth_signup_uninvited = fields.Selection([
|
||||
('b2b', 'On invitation (B2B)'),
|
||||
('b2c', 'Free sign up (B2C)'),
|
||||
], string='Customer Account', default='b2b')
|
||||
|
||||
@api.multi
|
||||
def _compute_menu(self):
|
||||
Menu = self.env['website.menu']
|
||||
for website in self:
|
||||
website.menu_id = Menu.search([('parent_id', '=', False), ('website_id', '=', website.id)], order='id', limit=1).id
|
||||
website.menu_id = Menu.search([('parent_id', '=', False), ('website_id', 'in', (website.id, False))], order='id', limit=1).id
|
||||
|
||||
# cf. Wizard hack in website_views.xml
|
||||
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)])
|
||||
|
||||
@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
|
||||
@@ -130,8 +195,8 @@ class Website(models.Model):
|
||||
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': self.get_current_website().id,
|
||||
'view_id': view.id,
|
||||
})
|
||||
result['view_id'] = view.id
|
||||
if add_menu:
|
||||
@@ -154,7 +219,7 @@ class Website(models.Model):
|
||||
"""
|
||||
website_id = self.get_current_website().id
|
||||
inc = 0
|
||||
domain_static = ['|', ('website_ids', '=', False), ('website_ids', 'in', website_id)]
|
||||
domain_static = ['|', ('website_id', '=', False), ('website_id', '=', website_id)]
|
||||
page_temp = page_url
|
||||
while self.env['website.page'].with_context(active_test=False).sudo().search([('url', '=', page_temp)] + domain_static):
|
||||
inc += 1
|
||||
@@ -177,7 +242,7 @@ class Website(models.Model):
|
||||
#Look for unique key
|
||||
key_copy = string
|
||||
inc = 0
|
||||
domain_static = ['|', ('website_ids', '=', False), ('website_ids', 'in', website_id)]
|
||||
domain_static = ['|', ('website_id', '=', False), ('website_id', '=', website_id)]
|
||||
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 "")
|
||||
@@ -208,7 +273,7 @@ class Website(models.Model):
|
||||
|
||||
# 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_id', '=', False), ('website_id', '=', website_id), ('view_id.arch_db', 'ilike', url)
|
||||
]
|
||||
pages = self.env['website.page'].search(website_page_search_dom)
|
||||
page_key = _('Page')
|
||||
@@ -276,7 +341,7 @@ class Website(models.Model):
|
||||
|
||||
# search for website_page with link
|
||||
website_page_search_dom = [
|
||||
'|', ('website_ids', 'in', website_id), ('website_ids', '=', False), ('view_id.arch_db', 'ilike', key),
|
||||
'|', ('website_id', '=', False), ('website_id', '=', website_id), ('view_id.arch_db', 'ilike', key),
|
||||
('id', '!=', page.id),
|
||||
]
|
||||
pages = self.env['website.page'].search(website_page_search_dom)
|
||||
@@ -376,19 +441,38 @@ class Website(models.Model):
|
||||
|
||||
@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 _fix_to_session(self):
|
||||
# call on empty recordset to clear
|
||||
if request:
|
||||
request.session['force_website_id'] = self.id
|
||||
|
||||
@api.model
|
||||
def is_publisher(self):
|
||||
@@ -545,7 +629,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 += ['|', ('website_id', '=', False), ('website_id', '=', self.get_current_website().id)]
|
||||
pages = request.env['website.page'].search(domain, order='name', limit=limit)
|
||||
return pages
|
||||
|
||||
@@ -599,9 +683,48 @@ 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',
|
||||
compute='_compute_website_published',
|
||||
inverse='_inverse_website_published',
|
||||
search='_search_website_published') # todo jov evaluate places where this is used, we can probably replace some with is_published
|
||||
is_published = fields.Boolean('Is published')
|
||||
website_id = fields.Many2one('website', string='Website', help='Restrict publishing to this website.')
|
||||
website_url = fields.Char('Website URL', compute='_compute_website_url', help='The full URL to access the document through the website.')
|
||||
|
||||
@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):
|
||||
current_website_id = self._context.get('website_id')
|
||||
for record in self:
|
||||
record.is_published = record.website_published
|
||||
if record.website_published and current_website_id:
|
||||
record.write({'website_id': current_website_id})
|
||||
|
||||
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')
|
||||
if current_website_id:
|
||||
is_published = [('is_published', '=', True)]
|
||||
on_current_website = expression.OR([[('website_id', '=', False)], [('website_id', '=', 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', '=', value)]
|
||||
|
||||
@api.multi
|
||||
def _compute_website_url(self):
|
||||
for record in self:
|
||||
@@ -627,9 +750,9 @@ class Page(models.Model):
|
||||
_inherits = {'ir.ui.view': 'view_id'}
|
||||
_inherit = 'website.published.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 +765,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,10 +786,20 @@ 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.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)
|
||||
domain = ['|', ('website_id', '=', False), ('website_id', '=', website_id), ('id', '=', id)]
|
||||
item = self.search_read(domain, fields=['id', 'name', 'url', 'website_published', 'website_indexed', 'date_publish', 'menu_ids', 'is_homepage', 'website_id'], limit=1)
|
||||
return item
|
||||
|
||||
@api.multi
|
||||
@@ -709,11 +845,15 @@ 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.
|
||||
page.with_context(no_cow=True).write({
|
||||
'key': page_key,
|
||||
'name': data['name'],
|
||||
'url': url,
|
||||
'website_published': data['website_published'],
|
||||
'is_published': data['website_published'],
|
||||
'website_id': website.id if data['restrict_to_current_website'] else False,
|
||||
'website_indexed': data['website_indexed'],
|
||||
'date_publish': data['date_publish'] or None,
|
||||
'is_homepage': data['is_homepage'],
|
||||
@@ -733,17 +873,18 @@ 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 and not default.get('view_id'):
|
||||
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,
|
||||
}
|
||||
return super(Page, self).copy(default=default)
|
||||
|
||||
@api.model
|
||||
@@ -758,47 +899,30 @@ class Page(models.Model):
|
||||
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)]
|
||||
)
|
||||
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 +939,47 @@ 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 unlink(self):
|
||||
'''This implements COU (copy-on-unlink). When deleting a generic menu
|
||||
item website-specific menu items will be created.'''
|
||||
current_website_id = self._context.get('website_id')
|
||||
|
||||
if current_website_id and not self._context.get('no_cow'):
|
||||
for menu in self.filtered(lambda menu: not menu.website_id):
|
||||
for website in self.env['website'].search([('id', '!=', current_website_id)]):
|
||||
# reuse the COW mechanism to create website-specific copies
|
||||
menu.with_context(website_id=website.id).write({})
|
||||
|
||||
return super(Menu, self).unlink()
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
'''This implements COW (copy-on-write). This way editing websites does
|
||||
not impact other websites and newly created websites will only
|
||||
contain the default menus.
|
||||
'''
|
||||
current_website_id = self._context.get('website_id')
|
||||
|
||||
if current_website_id and not self.website_id and not self._context.get('no_cow'):
|
||||
new_website_specific_menu = self.copy({'website_id': current_website_id})
|
||||
|
||||
for child in self.child_id:
|
||||
child.write({'parent_id': new_website_specific_menu.id})
|
||||
|
||||
return new_website_specific_menu.write(vals)
|
||||
|
||||
return super(Menu, self).write(vals)
|
||||
|
||||
@api.one
|
||||
def _compute_visible(self):
|
||||
visible = True
|
||||
@@ -846,6 +1002,21 @@ class Menu(models.Model):
|
||||
url = '/%s' % self.url
|
||||
return url
|
||||
|
||||
def get_children_for_current_website(self):
|
||||
most_specific_child_menus = self.env['website.menu']
|
||||
website_id = self._context.get('website_id')
|
||||
|
||||
if not website_id:
|
||||
return self.child_id
|
||||
|
||||
for child in self.child_id:
|
||||
if child.website_id and child.website_id.id == website_id:
|
||||
most_specific_child_menus |= child
|
||||
elif not child.website_id and not any(child.clean_url() == child2.clean_url() and child2.website_id.id == website_id for child2 in self.child_id):
|
||||
most_specific_child_menus |= child
|
||||
|
||||
return most_specific_child_menus
|
||||
|
||||
# would be better to take a menu_id as argument
|
||||
@api.model
|
||||
def get_tree(self, website_id, menu_id=None):
|
||||
@@ -862,7 +1033,7 @@ class Menu(models.Model):
|
||||
children=[],
|
||||
is_homepage=is_homepage,
|
||||
)
|
||||
for child in node.child_id:
|
||||
for child in node.get_children_for_current_website():
|
||||
menu_node['children'].append(make_tree(child))
|
||||
return menu_node
|
||||
if menu_id:
|
||||
|
||||
@@ -64,6 +64,7 @@ var Dashboard = AbstractAction.extend(ControlPanelMixin, {
|
||||
return this._rpc({
|
||||
route: '/website/fetch_dashboard_data',
|
||||
params: {
|
||||
website_id: this.website_id || false,
|
||||
date_from: this.date_from.year()+'-'+(this.date_from.month()+1)+'-'+this.date_from.date(),
|
||||
date_to: this.date_to.year()+'-'+(this.date_to.month()+1)+'-'+this.date_to.date(),
|
||||
},
|
||||
@@ -72,6 +73,7 @@ var Dashboard = AbstractAction.extend(ControlPanelMixin, {
|
||||
self.dashboards_data = result.dashboards;
|
||||
self.currency_id = result.currency_id;
|
||||
self.groups = result.groups;
|
||||
self.websites = result.websites;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -245,6 +247,16 @@ var Dashboard = AbstractAction.extend(ControlPanelMixin, {
|
||||
|
||||
},
|
||||
|
||||
on_website_button: function(website_id) {
|
||||
var self = this;
|
||||
this.website_id = website_id;
|
||||
$.when(this.fetch_data()).then(function() {
|
||||
self.$('.o_website_dashboard').empty();
|
||||
self.render_dashboards();
|
||||
self.render_graphs();
|
||||
});
|
||||
},
|
||||
|
||||
on_reverse_breadcrumb: function() {
|
||||
var self = this;
|
||||
web_client.do_push_state({});
|
||||
@@ -293,10 +305,15 @@ var Dashboard = AbstractAction.extend(ControlPanelMixin, {
|
||||
this.$searchview = $(QWeb.render("website.DateRangeButtons", {
|
||||
widget: this,
|
||||
}));
|
||||
this.$searchview.click('button.js_date_range', function(ev) {
|
||||
self.on_date_range_button($(ev.target).data('date'));
|
||||
$(this).find('button.js_date_range.active').removeClass('active');
|
||||
this.$searchview.find('button.js_date_range').click(function(ev) {
|
||||
self.$searchview.find('button.js_date_range.active').removeClass('active');
|
||||
$(ev.target).addClass('active');
|
||||
self.on_date_range_button($(ev.target).data('date'));
|
||||
});
|
||||
this.$searchview.find('button.js_website').click(function(ev) {
|
||||
self.$searchview.find('button.js_website.active').removeClass('active');
|
||||
$(ev.target).addClass('active');
|
||||
self.on_website_button($(ev.target).data('website-id'));
|
||||
});
|
||||
}
|
||||
this.update_control_panel({
|
||||
|
||||
@@ -40,6 +40,7 @@ var WebsiteRoot = BodyManager.extend({
|
||||
'click .js_publish_management .js_publish_btn': '_onPublishBtnClick',
|
||||
'submit .js_website_submit_form': '_onWebsiteFormSubmit',
|
||||
'click .js_disable_on_click': '_onDisableOnClick',
|
||||
'click .js_multi_website_switch': '_multiWebsiteSwitch',
|
||||
}),
|
||||
custom_events: _.extend({}, BodyManager.prototype.custom_events || {}, {
|
||||
animation_start_demand: '_onAnimationStartDemand',
|
||||
@@ -279,6 +280,23 @@ var WebsiteRoot = BodyManager.extend({
|
||||
_onDisableOnClick: function (ev) {
|
||||
$(ev.currentTarget).addClass('disabled');
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when clicking on the multi-website switcher.
|
||||
*
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_multiWebsiteSwitch: function (ev) {
|
||||
var website_id_to_switch_to = ev.target.getAttribute('website-id');
|
||||
this._rpc({
|
||||
route: '/website/set_session_website',
|
||||
params: {
|
||||
website_id: website_id_to_switch_to || false,
|
||||
},
|
||||
}).then(function () {
|
||||
window.location.reload(true);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -145,6 +145,16 @@ var PagePropertiesDialog = weWidgets.Dialog.extend({
|
||||
});
|
||||
}));
|
||||
|
||||
defs.push(this._rpc({model: 'res.users',
|
||||
method: 'has_group',
|
||||
args: ['website.group_multi_website'],
|
||||
context: context})
|
||||
.then(function (has_group) {
|
||||
if (!has_group) {
|
||||
self.$('#website_restriction').addClass('hidden');
|
||||
}
|
||||
}));
|
||||
|
||||
var datepickersOptions = {
|
||||
minDate: moment({y: 1900}),
|
||||
maxDate: moment().add(200, 'y'),
|
||||
@@ -210,6 +220,7 @@ var PagePropertiesDialog = weWidgets.Dialog.extend({
|
||||
is_menu: this.$('#is_menu').prop('checked'),
|
||||
is_homepage: this.$('#is_homepage').prop('checked'),
|
||||
website_published: this.$('#is_published').prop('checked'),
|
||||
restrict_to_current_website: this.$('#restrict_to_current_website').prop('checked'),
|
||||
create_redirect: this.$('#create_redirect').prop('checked'),
|
||||
redirect_type: this.$('#redirect_type').val(),
|
||||
website_indexed: this.$('#is_indexed').prop('checked'),
|
||||
@@ -954,7 +965,7 @@ function _deletePage(pageId, fromPageManagement) {
|
||||
// Delete the page if the user confirmed
|
||||
return self._rpc({
|
||||
model: 'website.page',
|
||||
method: 'delete_page',
|
||||
method: 'unlink',
|
||||
args: [pageId],
|
||||
context: context,
|
||||
});
|
||||
|
||||
@@ -70,6 +70,14 @@
|
||||
<button class="btn btn-secondary js_date_range" data-date="month">Last Month</button>
|
||||
<button class="btn btn-secondary js_date_range" data-date="year">Last Year</button>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm pull-right" style="margin-right: 20px;">
|
||||
<t t-foreach="widget.websites" t-as="website">
|
||||
<button t-attf-class="btn btn-sm btn-default js_website #{website.selected ? 'active' : ''}"
|
||||
t-att-data-website-id="website.id">
|
||||
<t t-esc="website.name"/>
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="website.SalesChartTooltip">
|
||||
|
||||
@@ -127,8 +127,23 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label col-md-3" for="date_publish">Publishing Date</label>
|
||||
<div class="form-group" id="website_restriction">
|
||||
<!-- TODO hide with multi-website group -->
|
||||
<label class="control-label col-md-3" for="restrict_to_current_website">
|
||||
Restrict to current website
|
||||
<i class="fa fa-question-circle-o" title="When enabled, this page is only accessible through the current website" role="img" aria-label="Info"></i>
|
||||
</label>
|
||||
<div class="col-md-2">
|
||||
<a>
|
||||
<label class="o_switch" for="restrict_to_current_website" >
|
||||
<input type="checkbox" t-att-checked="widget.page.website_id ? true : undefined" id="restrict_to_current_website"/>
|
||||
<span/>
|
||||
</label>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="date_publish">Publishing Date</label>
|
||||
<div class="col-md-9">
|
||||
<div class='input-group date' id='date_publish_container'>
|
||||
<input type='text' class="form-control" name="date_publish" id="date_publish" />
|
||||
|
||||
@@ -6,3 +6,5 @@ from . import test_qweb
|
||||
from . import test_crawl
|
||||
from . import test_ui
|
||||
from . import test_views
|
||||
from . import test_menu
|
||||
from . import test_page
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# coding: utf-8
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
class TestMenu(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestMenu, self).setUp()
|
||||
Menu = self.env['website.menu']
|
||||
|
||||
self.menu_root = Menu.create({
|
||||
'name': 'Root',
|
||||
})
|
||||
|
||||
self.menu_child = Menu.create({
|
||||
'name': 'Child',
|
||||
'parent_id': self.menu_root.id,
|
||||
})
|
||||
|
||||
self.menu_leaf1 = Menu.create({
|
||||
'name': 'Leaf 1',
|
||||
'parent_id': self.menu_child.id,
|
||||
})
|
||||
|
||||
self.menu_leaf2 = Menu.create({
|
||||
'name': 'Leaf 2',
|
||||
'parent_id': self.menu_root.id,
|
||||
})
|
||||
|
||||
# create an extra website so we have 3 in total
|
||||
self.website_test = self.env['website'].create({'domain': 'test'})
|
||||
|
||||
def test_cow_leaf(self):
|
||||
Menu = self.env['website.menu']
|
||||
|
||||
# backend write, no COW
|
||||
total_menu_items = Menu.search_count([])
|
||||
self.menu_leaf1.write({'name': 'Leaf 1 modified'})
|
||||
self.assertEqual(total_menu_items, Menu.search_count([]))
|
||||
|
||||
# write through website 1
|
||||
self.menu_leaf1.with_context(website_id=1).write({'name': 'Leaf 1 Website 1'})
|
||||
self.assertEqual(self.menu_leaf1.name, 'Leaf 1 modified')
|
||||
self.assertEqual(total_menu_items + 1, Menu.search_count([]))
|
||||
new_menu = Menu.search([('name', '=', 'Leaf 1 Website 1')])
|
||||
self.assertEqual(new_menu.website_id.id, 1)
|
||||
|
||||
def test_cow_root(self):
|
||||
Menu = self.env['website.menu']
|
||||
|
||||
# backend write, no COW
|
||||
total_menu_items = Menu.search_count([])
|
||||
self.menu_root.write({'name': 'Root 1 modified'})
|
||||
self.assertEqual(total_menu_items, Menu.search_count([]))
|
||||
|
||||
# write through website 1
|
||||
self.menu_root.with_context(website_id=1).write({'name': 'Root Website 1'})
|
||||
self.assertEqual(self.menu_root.name, 'Root 1 modified')
|
||||
self.assertEqual(total_menu_items + 4, Menu.search_count([]))
|
||||
|
||||
# verify parent/child structure
|
||||
website_specific_leaf2 = Menu.search([('name', '=', 'Leaf 2'), ('website_id', '=', 1)])
|
||||
website_specific_leaf1 = Menu.search([('name', '=', 'Leaf 1'), ('website_id', '=', 1)])
|
||||
website_specific_child = Menu.search([('name', '=', 'Child'), ('website_id', '=', 1)])
|
||||
website_specific_root = Menu.search([('name', '=', 'Root Website 1'), ('website_id', '=', 1)])
|
||||
self.assertEqual(website_specific_leaf2.parent_id.id, website_specific_root.id)
|
||||
self.assertEqual(website_specific_leaf1.parent_id.id, website_specific_child.id)
|
||||
self.assertEqual(website_specific_child.parent_id.id, website_specific_root.id)
|
||||
|
||||
def test_cou_leaf(self):
|
||||
Menu = self.env['website.menu']
|
||||
|
||||
# backend unlink, no COU
|
||||
self.menu_leaf1.unlink()
|
||||
self.assertEqual(len(Menu.search([('name', '=', 'Leaf 1')])), 0)
|
||||
|
||||
total_menu_items = Menu.search_count([])
|
||||
self.menu_leaf2.with_context(website_id=1).unlink()
|
||||
|
||||
website_specific_menus = Menu.search([('name', '=', 'Leaf 2')])
|
||||
self.assertEqual(len(website_specific_menus), 2)
|
||||
self.assertEqual(set(website_specific_menus.mapped('website_id').ids), set([2, self.website_test.id]))
|
||||
|
||||
# exactly 2 website-specific menu items were created and one was deleted
|
||||
self.assertEqual(total_menu_items + 1, Menu.search_count([]))
|
||||
|
||||
def test_cou_root_backend(self):
|
||||
Menu = self.env['website.menu']
|
||||
total_menu_items = Menu.search_count([])
|
||||
self.menu_root.unlink()
|
||||
|
||||
# should have deleted root and all children
|
||||
self.assertEqual(total_menu_items - 4, Menu.search_count([]))
|
||||
|
||||
def test_cou_root_frontend(self):
|
||||
Menu = self.env['website.menu']
|
||||
total_menu_items = Menu.search_count([])
|
||||
self.menu_root.with_context(website_id=1).unlink()
|
||||
|
||||
# should have removed 4 generic menus and created root and all children for 2 remaining websites
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Root'), ('website_id', '=', 2)]), 1)
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Child'), ('website_id', '=', 2)]), 1)
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Leaf 1'), ('website_id', '=', 2)]), 1)
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Leaf 2'), ('website_id', '=', 2)]), 1)
|
||||
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Root'), ('website_id', '=', self.website_test.id)]), 1)
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Child'), ('website_id', '=', self.website_test.id)]), 1)
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Leaf 1'), ('website_id', '=', self.website_test.id)]), 1)
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Leaf 2'), ('website_id', '=', self.website_test.id)]), 1)
|
||||
|
||||
self.assertEqual(total_menu_items + 4, Menu.search_count([]))
|
||||
@@ -0,0 +1,136 @@
|
||||
# coding: utf-8
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
class TestPage(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestPage, self).setUp()
|
||||
View = self.env['ir.ui.view']
|
||||
Page = self.env['website.page']
|
||||
Menu = self.env['website.menu']
|
||||
|
||||
self.base_view = View.create({
|
||||
'name': 'Base',
|
||||
'type': 'qweb',
|
||||
'arch': '<div>content</div>',
|
||||
'key': 'test.base_view',
|
||||
})
|
||||
|
||||
self.extension_view = View.create({
|
||||
'name': 'Extension',
|
||||
'mode': 'extension',
|
||||
'inherit_id': self.base_view.id,
|
||||
'arch': '<div position="inside">, extended content</div>',
|
||||
})
|
||||
|
||||
self.page_1 = Page.create({
|
||||
'view_id': self.base_view.id,
|
||||
'url': '/page_1',
|
||||
})
|
||||
|
||||
self.page_1_menu = Menu.create({
|
||||
'name': 'Page 1 menu',
|
||||
'page_id': self.page_1.id,
|
||||
})
|
||||
|
||||
def test_cow_page(self):
|
||||
Menu = self.env['website.menu']
|
||||
Page = self.env['website.page']
|
||||
View = self.env['ir.ui.view']
|
||||
|
||||
# backend write, no COW
|
||||
total_pages = Page.search_count([])
|
||||
total_menus = Menu.search_count([])
|
||||
total_views = View.search_count([])
|
||||
self.page_1.write({'arch': '<div>modified base content</div>'})
|
||||
self.assertEqual(total_pages, Page.search_count([]))
|
||||
self.assertEqual(total_menus, Menu.search_count([]))
|
||||
self.assertEqual(total_views, View.search_count([]))
|
||||
|
||||
# edit through frontend
|
||||
self.page_1.with_context(website_id=1).write({'arch': '<div>website 1 content</div>'})
|
||||
|
||||
# should have created website-specific copies for:
|
||||
# - page
|
||||
# - menu
|
||||
# - view x2 (base view + extension view)
|
||||
# and shouldn't have touched original records
|
||||
self.assertEqual(total_pages + 1, Page.search_count([]))
|
||||
self.assertEqual(total_menus + 1, Menu.search_count([]))
|
||||
self.assertEqual(total_views + 2, View.search_count([]))
|
||||
|
||||
self.assertEqual(self.page_1.arch, '<div>modified base content</div>')
|
||||
self.assertEqual(bool(self.page_1.website_id), False)
|
||||
self.assertEqual(bool(self.page_1_menu.website_id), False)
|
||||
|
||||
new_page = Page.search([('url', '=', '/page_1'), ('id', '!=', self.page_1.id)])
|
||||
self.assertEqual(new_page.website_id.id, 1)
|
||||
self.assertEqual(new_page.view_id.inherit_children_ids[0].website_id.id, 1)
|
||||
self.assertEqual(new_page.arch, '<div>website 1 content</div>')
|
||||
|
||||
new_menu = new_page.menu_ids[0]
|
||||
self.assertEqual(new_menu.website_id.id, 1)
|
||||
|
||||
def test_cow_extension_view(self):
|
||||
''' test cow on extension view itself (like web_editor would do in the frontend) '''
|
||||
Menu = self.env['website.menu']
|
||||
Page = self.env['website.page']
|
||||
View = self.env['ir.ui.view']
|
||||
|
||||
# nothing special should happen when editing through the backend
|
||||
total_pages = Page.search_count([])
|
||||
total_menus = Menu.search_count([])
|
||||
total_views = View.search_count([])
|
||||
self.extension_view.write({'arch': '<div>modified extension content</div>'})
|
||||
self.assertEqual(self.extension_view.arch, '<div>modified extension content</div>')
|
||||
self.assertEqual(total_pages, Page.search_count([]))
|
||||
self.assertEqual(total_menus, Menu.search_count([]))
|
||||
self.assertEqual(total_views, View.search_count([]))
|
||||
|
||||
# When editing through the frontend a website-specific copy
|
||||
# for the extension view should be created. When rendering the
|
||||
# original website.page on website 1 it will look differently
|
||||
# due to this new extension view.
|
||||
self.extension_view.with_context(website_id=1).write({'arch': '<div>website 1 content</div>'})
|
||||
self.assertEqual(total_pages, Page.search_count([]))
|
||||
self.assertEqual(total_menus, Menu.search_count([]))
|
||||
self.assertEqual(total_views + 1, View.search_count([]))
|
||||
|
||||
self.assertEqual(self.extension_view.arch, '<div>modified extension content</div>')
|
||||
self.assertEqual(bool(self.page_1.website_id), False)
|
||||
self.assertEqual(bool(self.page_1_menu.website_id), False)
|
||||
|
||||
new_view = View.search([('name', '=', 'Extension'), ('website_id', '=', 1)])
|
||||
self.assertEqual(new_view.arch, '<div>website 1 content</div>')
|
||||
self.assertEqual(new_view.website_id.id, 1)
|
||||
|
||||
def test_cou_page_backend(self):
|
||||
Menu = self.env['website.menu']
|
||||
Page = self.env['website.page']
|
||||
View = self.env['ir.ui.view']
|
||||
|
||||
# currently the view unlink of website.page can't handle views with inherited views
|
||||
self.extension_view.unlink()
|
||||
|
||||
self.page_1.unlink()
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Page 1 menu')]), 0)
|
||||
self.assertEqual(Page.search_count([('url', '=', '/page_1')]), 0)
|
||||
self.assertEqual(View.search_count([('name', 'in', ('Base', 'Extension'))]), 0)
|
||||
|
||||
def test_cou_page_frontend(self):
|
||||
Menu = self.env['website.menu']
|
||||
Page = self.env['website.page']
|
||||
View = self.env['ir.ui.view']
|
||||
|
||||
# currently the view unlink of website.page can't handle views with inherited views
|
||||
self.extension_view.unlink()
|
||||
|
||||
self.page_1.with_context(website_id=1).unlink()
|
||||
|
||||
self.assertEqual(bool(self.base_view.exists()), False)
|
||||
self.assertEqual(bool(self.page_1.exists()), False)
|
||||
self.assertEqual(bool(self.page_1_menu.exists()), False)
|
||||
|
||||
self.assertEqual(Menu.search([('name', '=', 'Page 1 menu')]).website_id.id, 2)
|
||||
self.assertEqual(Page.search([('url', '=', '/page_1')]).website_id.id, 2)
|
||||
self.assertEqual(View.search([('name', 'in', ('Base', 'Extension'))]).mapped('website_id').id, 2)
|
||||
@@ -258,3 +258,86 @@ class TestViewSaving(common.TransactionCase):
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
def test_cow_leaf(self):
|
||||
View = self.env['ir.ui.view']
|
||||
|
||||
base_view = View.create({
|
||||
'name': 'Base',
|
||||
'type': 'qweb',
|
||||
'arch': '<div>base content</div>',
|
||||
}).with_context(load_all_views=True)
|
||||
|
||||
inherit_view = View.create({
|
||||
'name': 'Extension',
|
||||
'mode': 'extension',
|
||||
'inherit_id': base_view.id,
|
||||
'arch': '<div position="replace"><div>extended content</div></div>',
|
||||
})
|
||||
|
||||
# edit on backend, regular write
|
||||
inherit_view.write({'arch': '<div position="replace"><div>modified content</div></div>'})
|
||||
self.assertEqual(View.search_count([('name', '=', 'Base')]), 1)
|
||||
self.assertEqual(View.search_count([('name', '=', 'Extension')]), 1)
|
||||
|
||||
arch = base_view.read_combined(['arch'])['arch']
|
||||
self.assertEqual(arch, '<div>modified content</div>')
|
||||
|
||||
# edit on frontend, copy just the leaf
|
||||
inherit_view.with_context(website_id=1).write({'arch': '<div position="replace"><div>website 1 content</div></div>'})
|
||||
inherit_views = View.search([('name', '=', 'Extension')])
|
||||
self.assertEqual(View.search_count([('name', '=', 'Base')]), 1)
|
||||
self.assertEqual(len(inherit_views), 2)
|
||||
self.assertEqual(len(inherit_views.filtered(lambda v: v.website_id.id == 1)), 1)
|
||||
|
||||
# read in backend should be unaffected
|
||||
arch = base_view.read_combined(['arch'])['arch']
|
||||
self.assertEqual(arch, '<div>modified content</div>')
|
||||
# read on website should reflect change
|
||||
arch = base_view.with_context(website_id=1).read_combined(['arch'])['arch']
|
||||
self.assertEqual(arch, '<div>website 1 content</div>')
|
||||
|
||||
# website-specific inactive view should take preference over active generic one when viewing the website
|
||||
# this is necessary to make customize_show=True templates work correctly
|
||||
inherit_views.filtered(lambda v: v.website_id.id == 1).write({'active': False})
|
||||
arch = base_view.with_context(website_id=1).read_combined(['arch'])['arch']
|
||||
self.assertEqual(arch, '<div>base content</div>')
|
||||
|
||||
def test_cow_root(self):
|
||||
View = self.env['ir.ui.view']
|
||||
|
||||
base_view = View.create({
|
||||
'name': 'Base',
|
||||
'type': 'qweb',
|
||||
'arch': '<div>content</div>',
|
||||
})
|
||||
|
||||
View.create({
|
||||
'name': 'Extension',
|
||||
'mode': 'extension',
|
||||
'inherit_id': base_view.id,
|
||||
'arch': '<div position="inside">, extended content</div>',
|
||||
})
|
||||
|
||||
# edit on backend, regular write
|
||||
base_view.write({'arch': '<div>modified base content</div>'})
|
||||
self.assertEqual(View.search_count([('name', '=', 'Base')]), 1)
|
||||
self.assertEqual(View.search_count([('name', '=', 'Extension')]), 1)
|
||||
|
||||
# edit on frontend, copy the entire tree
|
||||
base_view.with_context(website_id=1).write({'arch': '<div>website 1 content</div>'})
|
||||
|
||||
generic_base_view = View.search([('name', '=', 'Base'), ('website_id', '=', False)])
|
||||
website_specific_base_view = View.search([('name', '=', 'Base'), ('website_id', '=', 1)])
|
||||
self.assertEqual(len(generic_base_view), 1)
|
||||
self.assertEqual(len(website_specific_base_view), 1)
|
||||
|
||||
inherit_views = View.search([('name', '=', 'Extension')])
|
||||
self.assertEqual(len(inherit_views), 2)
|
||||
self.assertEqual(len(inherit_views.filtered(lambda v: v.website_id.id == 1)), 1)
|
||||
|
||||
arch = generic_base_view.with_context(load_all_views=True).read_combined(['arch'])['arch']
|
||||
self.assertEqual(arch, '<div>modified base content, extended content</div>')
|
||||
|
||||
arch = website_specific_base_view.with_context(load_all_views=True, website_id=1).read_combined(['arch'])['arch']
|
||||
self.assertEqual(arch, '<div>website 1 content, extended content</div>')
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_attachment_form_inherit_website" model="ir.ui.view">
|
||||
<field name="name">ir.attachment.form.inherit.website</field>
|
||||
<field name="model">ir.attachment</field>
|
||||
<field name="inherit_id" ref="base.view_attachment_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="mimetype" position="after">
|
||||
<field name="key" groups="base.group_no_one"/>
|
||||
<field name="theme_id" groups="base.group_no_one"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -10,12 +10,27 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[hasclass('settings')]" position="inside">
|
||||
<div class="app_settings_block" data-string="Website" string="Website" data-key="website" groups="website.group_website_designer">
|
||||
<field name="website_id" invisible="1"/>
|
||||
<h2 groups="website.group_multi_website">Select the Website to Configure</h2>
|
||||
<div class="row mt16 o_settings_container" id="website_selection_settings" groups="website.group_multi_website">
|
||||
<div class="col-xs-12 col-md-6 o_setting_box" id="website">
|
||||
<div class="o_setting_right_pane">
|
||||
<label string="Website" for="website_id"/>
|
||||
<div class="text-muted">
|
||||
Settings on this page will apply to this website
|
||||
</div>
|
||||
<field name="website_id" options="{'no_open': True, 'no_create_edit': True}"/>
|
||||
<div>
|
||||
<button name="website_go_to" type="object" string="Go to website"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Website</h2>
|
||||
<div class="row mt16 o_settings_container" id="webmaster_settings">
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="domain_setting">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Website Title</span>
|
||||
<label for="website_name" string="Website Title"/>
|
||||
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
|
||||
<div class="text-muted">
|
||||
Name and favicon of your website
|
||||
</div>
|
||||
@@ -31,9 +46,40 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="company_settings" groups="base.group_multi_company">
|
||||
<div class="o_setting_right_pane">
|
||||
<label string="Company" for="website_company_id"/>
|
||||
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
|
||||
<div class="text-muted">
|
||||
The company this website belongs to
|
||||
</div>
|
||||
<field name="website_company_id"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="domain_settings" groups="website.group_multi_website">
|
||||
<div class="o_setting_right_pane">
|
||||
<label string="Domain" for="website_domain"/>
|
||||
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
|
||||
<div class="text-muted">
|
||||
Display this website when users visit this domain
|
||||
</div>
|
||||
<field name="website_domain"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="website_geoip_settings" groups="website.group_multi_website">
|
||||
<div class="o_setting_right_pane">
|
||||
<label string="Country Groups" for="website_country_group_ids"/>
|
||||
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
|
||||
<div class="text-muted">
|
||||
Display this website for users from these country groups
|
||||
</div>
|
||||
<field name="website_country_group_ids" widget="many2many_tags"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="languages_setting">
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="language_ids"/>
|
||||
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
|
||||
<div class="text-muted">
|
||||
Languages available on your website
|
||||
</div>
|
||||
@@ -58,6 +104,7 @@
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="has_google_analytics"/>
|
||||
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
|
||||
<div class="text-muted">
|
||||
Track visits in Google Analytics
|
||||
</div>
|
||||
@@ -142,6 +189,7 @@
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="auth_signup_uninvited"/>
|
||||
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
|
||||
<div class="text-muted">
|
||||
Let your customers log in to see their documents
|
||||
</div>
|
||||
@@ -155,12 +203,49 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label string="Social Media" for="social_twitter"/>
|
||||
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
|
||||
<div class="text-muted">
|
||||
Add links to social media on your website
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<div class="row">
|
||||
<label for="social_twitter" string="Twitter" class="col-md-3 o_light_label"/>
|
||||
<field name="social_twitter"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="social_facebook" string="Facebook" class="col-md-3 o_light_label"/>
|
||||
<field name="social_facebook"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="social_github" string="GitHub" class="col-md-3 o_light_label"/>
|
||||
<field name="social_github"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="social_linkedin" string="LinkedIn" class="col-md-3 o_light_label"/>
|
||||
<field name="social_linkedin"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="social_youtube" string="YouTube" class="col-md-3 o_light_label"/>
|
||||
<field name="social_youtube"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="social_googleplus" string="Google Plus" class="col-md-3 o_light_label"/>
|
||||
<field name="social_googleplus"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="cdn_setting" title="A CDN helps you serve your website’s content with high availability and high performance to any visitor wherever they are located." groups="base.group_no_one">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="cdn_activated"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="cdn_activated" string="Content Delivery Network (CDN)"/>
|
||||
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
|
||||
<div class="text-muted">
|
||||
Use a CDN to optimize the availability of your website's content
|
||||
</div>
|
||||
@@ -192,6 +277,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Multi-Website</h2>
|
||||
<div class="row mt16 o_settings_container" id="multi_website" name="multi_website">
|
||||
<div class="col-xs-12 col-md-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="group_multi_website"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label string="Multi-Website" for="group_multi_website"/>
|
||||
<div class="text-muted">
|
||||
Manage multiple websites
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
@@ -227,6 +326,14 @@
|
||||
<menuitem id="menu_website_add_features" parent="website.menu_website_global_configuration"
|
||||
sequence="20" groups="base.group_system" action="action_website_add_features"/>
|
||||
|
||||
<menuitem name="Websites"
|
||||
id="menu_website_websites_list"
|
||||
action="action_website_list"
|
||||
parent="menu_website_global_configuration"
|
||||
groups="website.group_multi_website"
|
||||
sequence="10"
|
||||
/>
|
||||
|
||||
<menuitem name="Pages"
|
||||
id="menu_website_pages_list"
|
||||
action="action_website_pages_list"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<record id="view_partner_form_inherit_website" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.website.inherit</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="company_id" position="after" options="{'no_create': True}">
|
||||
<field name="website_id" groups="website.group_multi_website"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -77,6 +77,20 @@
|
||||
<li class="o_mobile_preview" id="mobile-menu">
|
||||
<a data-action="show-mobile-preview" href="#"><span title="Mobile preview" role="img" aria-label="Mobile preview" class="fa fa-mobile"/></a>
|
||||
</li>
|
||||
<li class="dropdown" groups="website.group_multi_website">
|
||||
<a class="dropdown-toggle waves" data-toggle="dropdown" href="#"><t t-esc="multi_website_selected_website['name']"/> <span class="caret"/></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li t-as="multi_website_website" t-foreach="multi_website_websites">
|
||||
<a href="#">
|
||||
<t t-if="multi_website_selected_website['website_id'] != multi_website_website['website_id']">
|
||||
<span class="oe_menu_text js_multi_website_switch" t-att-website-id="str(multi_website_website['website_id'])">
|
||||
<t t-esc="multi_website_website['name']"/>
|
||||
</span>
|
||||
</t>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<!-- Page Edition -->
|
||||
<li class="o_new_content_menu" id="new-content-menu">
|
||||
<a href="#"><span class="fa fa-plus"/>New</a>
|
||||
|
||||
@@ -210,7 +210,7 @@
|
||||
<attribute name="t-if">website.user_id != user_id</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//header//ul[@id='top_menu']/li[hasclass('divider')]" position="before">
|
||||
<t t-foreach="website.menu_id.child_id" t-as="submenu">
|
||||
<t t-foreach="website.menu_id.get_children_for_current_website()" t-as="submenu">
|
||||
<t t-call="website.submenu">
|
||||
<t t-set="submenu_type" t-value="'nav'"/>
|
||||
</t>
|
||||
@@ -1126,4 +1126,22 @@ Sitemap: <t t-esc="url_root"/>sitemap.xml
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="default_homepage">
|
||||
<div id="wrap" class="oe_structure oe_empty">
|
||||
<div class="item image_text oe_img_bg active">
|
||||
<div class="container">
|
||||
<div class="row content">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<h2>Homepage of <t t-esc="website.name"/></h2>
|
||||
<h3>Click to customize this text</h3>
|
||||
<p>
|
||||
<a role="button" class="btn btn-success btn-large" href="/contactus">Contact us</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -14,29 +14,15 @@
|
||||
<field name="model">website</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Website Settings">
|
||||
<header>
|
||||
<button string="Apply" type="object" name="noop" class="oe_highlight"/>
|
||||
<button string="Cancel" type="object" name="noop" class="oe_link"/>
|
||||
</header>
|
||||
<div name="domain">
|
||||
<separator name="domain" string="Domain"/>
|
||||
<group name="domain">
|
||||
<field name="name"/>
|
||||
<field name="domain"/>
|
||||
<field name="country_group_ids" widget="many2many_tags"/>
|
||||
<field name="google_analytics_key" placeholder="UA-XXXXXXXX-Y"/>
|
||||
</group>
|
||||
</div>
|
||||
<div name="social_media">
|
||||
<separator string="Social Media"/>
|
||||
<group name="social_media">
|
||||
<field name="social_twitter" placeholder="https://twitter.com/Odoo"/>
|
||||
<field name="social_facebook" placeholder="https://www.facebook.com/odoo"/>
|
||||
<field name="social_googleplus" placeholder="https://plus.google.com/+Odooapps"/>
|
||||
<field name="social_linkedin" placeholder="https://www.linkedin.com/company/odoo"/>
|
||||
<field name="social_youtube" placeholder="https://www.youtube.com/user/OpenERPonline"/>
|
||||
<field name="social_github" placeholder="https://youraccount.github.io"/>
|
||||
</group>
|
||||
</div>
|
||||
<div name="other">
|
||||
<separator string="Other Info"/>
|
||||
<group name="other">
|
||||
@@ -55,8 +41,12 @@
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Websites">
|
||||
<field name="name"/>
|
||||
<field name="domain"/>
|
||||
<field name="country_group_ids" widget="many2many_tags"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="default_lang_id"/>
|
||||
<field name="installed_theme_id" groups="base.group_no_one"/>
|
||||
<field name="theme_ids" groups="base.group_no_one" widget="many2many_tags"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -77,6 +67,15 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_website_list" model="ir.actions.act_window">
|
||||
<field name="name">Websites</field>
|
||||
<field name="res_model">website</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="view_website_tree"/>
|
||||
<field name="target">current</field>
|
||||
</record>
|
||||
|
||||
<!-- website.redirect views -->
|
||||
<record id="website_redirect_form_view" model="ir.ui.view">
|
||||
<field name="name">website.redirect.form</field>
|
||||
@@ -91,7 +90,7 @@
|
||||
<field name="url_to"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="website_id"/>
|
||||
<field name="website_id" groups="website.group_multi_website"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
</group>
|
||||
@@ -108,7 +107,7 @@
|
||||
<field name="type"/>
|
||||
<field name="url_from"/>
|
||||
<field name="url_to"/>
|
||||
<field name="website_id"/>
|
||||
<field name="website_id" groups="website.group_multi_website"/>
|
||||
<field name="active"/>
|
||||
<field name="sequence" widget="handle" />
|
||||
</tree>
|
||||
@@ -137,6 +136,8 @@
|
||||
<field name="name"/>
|
||||
<field name="url"/>
|
||||
<field name="view_id"/>
|
||||
<field groups="base.group_no_one" name="is_published"/>
|
||||
<field groups="website.group_multi_website" name="website_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="website_indexed"/>
|
||||
@@ -144,7 +145,6 @@
|
||||
<field name="date_publish"/>
|
||||
</group>
|
||||
</group>
|
||||
<field groups="base.group_no_one" name="website_ids"/>
|
||||
<label for="menu_ids" string="Related Menu Items"/>
|
||||
<field name="menu_ids"/>
|
||||
</sheet>
|
||||
@@ -193,7 +193,7 @@
|
||||
<group>
|
||||
<field name="new_window"/>
|
||||
<field name="sequence"/>
|
||||
<field attrs="{'invisible': [('parent_id', '!=', False)]}" name="website_id"/>
|
||||
<field name="website_id" groups="website.group_multi_website"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="parent_id"/>
|
||||
@@ -219,7 +219,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Website menu" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="website_id" options="{'no_create': True}"/>
|
||||
<field name="website_id" options="{'no_create': True}" groups="website.group_multi_website"/>
|
||||
<field name="name"/>
|
||||
<field name="url"/>
|
||||
<field name="new_window"/>
|
||||
@@ -263,7 +263,8 @@
|
||||
<field name="inherit_id" ref="base.view_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="after">
|
||||
<field name="website_id" readonly="1"/>
|
||||
<field name="website_id" groups="website.group_multi_website"/>
|
||||
<field name="theme_id" groups="base.group_no_one"/>
|
||||
<field name="key" readonly="1"/>
|
||||
<field name="page_ids" invisible="1" />
|
||||
<field name="first_page_id" attrs="{'invisible': [('page_ids', '=', [])]}" />
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<field name="inherit_id" ref="base.view_base_language_install"/>
|
||||
<field name="arch" type="xml">
|
||||
<group states="init" position="inside">
|
||||
<field name="website_ids" widget="many2many_checkboxes"/>
|
||||
<field name="website_ids" widget="many2many_checkboxes" groups="website.group_multi_website"/>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="website.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<div id="webmaster_settings" position="after">
|
||||
<div id="multi_website" position="after">
|
||||
<h2>Communication</h2>
|
||||
<div class="row mt16 o_settings_container" id="communication_settings">
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="contact_form_install_setting" title="New messages are managed as leads or opportunities in your CRM app.">
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="live_chat_install_setting">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Live Chat</span>
|
||||
<span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
|
||||
<div class="text-muted">
|
||||
Live chat channel of your website
|
||||
</div>
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import wizard
|
||||
from . import report
|
||||
|
||||
@@ -12,8 +12,9 @@ from odoo.http import request
|
||||
class WebsiteSaleBackend(WebsiteBackend):
|
||||
|
||||
@http.route()
|
||||
def fetch_dashboard_data(self, date_from, date_to):
|
||||
results = super(WebsiteSaleBackend, self).fetch_dashboard_data(date_from, date_to)
|
||||
def fetch_dashboard_data(self, website_id, date_from, date_to):
|
||||
results = super(WebsiteSaleBackend, self).fetch_dashboard_data(website_id, date_from, date_to)
|
||||
current_website = request.env['website'].browse(website_id) if website_id else request.env['website'].get_current_website()
|
||||
|
||||
sales_values = dict(
|
||||
graph=[],
|
||||
@@ -40,6 +41,7 @@ class WebsiteSaleBackend(WebsiteBackend):
|
||||
# Product-based computation
|
||||
report_product_lines = request.env['sale.report'].read_group(
|
||||
domain=[
|
||||
('website_id', '=', current_website.id),
|
||||
('team_id.team_type', '=', 'website'),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('confirmation_date', '>=', date_from),
|
||||
@@ -57,6 +59,7 @@ class WebsiteSaleBackend(WebsiteBackend):
|
||||
|
||||
# Sale-based results computation
|
||||
sale_order_domain = [
|
||||
('website_id', '=', current_website.id),
|
||||
('team_id', 'in', request.env['crm.team'].search([('team_type', '=', 'website')]).ids),
|
||||
('date_order', '>=', fields.Datetime.to_string(datetime_from)),
|
||||
('date_order', '<=', fields.Datetime.to_string(datetime_to))]
|
||||
@@ -70,6 +73,7 @@ class WebsiteSaleBackend(WebsiteBackend):
|
||||
|
||||
report_price_lines = request.env['sale.report'].read_group(
|
||||
domain=[
|
||||
('website_id', '=', current_website.id),
|
||||
('team_id.team_type', '=', 'website'),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('date', '>=', date_from),
|
||||
@@ -110,6 +114,7 @@ class WebsiteSaleBackend(WebsiteBackend):
|
||||
previous_sale_label = _('Previous Year')
|
||||
|
||||
sales_domain = [
|
||||
('website_id', '=', current_website.id),
|
||||
('team_id.team_type', '=', 'website'),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('confirmation_date', '>=', date_from),
|
||||
|
||||
@@ -245,7 +245,7 @@ class WebsiteSale(http.Controller):
|
||||
if attrib_list:
|
||||
post['attrib'] = attrib_list
|
||||
|
||||
categs = request.env['product.public.category'].search([('parent_id', '=', False)])
|
||||
categs = request.env['product.public.category'].search([('parent_id', '=', False), ('website_published', '=', True)])
|
||||
Product = request.env['product.template']
|
||||
|
||||
parent_category_ids = []
|
||||
@@ -578,6 +578,7 @@ class WebsiteSale(http.Controller):
|
||||
|
||||
new_values['customer'] = True
|
||||
new_values['team_id'] = request.website.salesteam_id and request.website.salesteam_id.id
|
||||
new_values['website_id'] = request.website.id
|
||||
|
||||
lang = request.lang if request.lang in request.website.mapped('language_ids.code') else None
|
||||
if lang:
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record model="website" id="website.website2">
|
||||
<field name="salesteam_id" ref="sales_team.salesteam_website_sales"/>
|
||||
</record>
|
||||
|
||||
<record id="product.product_attribute_2" model="product.attribute">
|
||||
<field name="type">color</field>
|
||||
|
||||
@@ -53,7 +53,7 @@ class ProductPricelist(models.Model):
|
||||
|
||||
class ProductPublicCategory(models.Model):
|
||||
_name = "product.public.category"
|
||||
_inherit = ["website.seo.metadata"]
|
||||
_inherit = ["website.seo.metadata", "website.published.mixin"]
|
||||
_description = "Website Product Category"
|
||||
_order = "sequence, name"
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ class SaleOrder(models.Model):
|
||||
only_services = fields.Boolean(compute='_compute_cart_info', string='Only Services')
|
||||
is_abandoned_cart = fields.Boolean('Abandoned Cart', compute='_compute_abandoned_cart', search='_search_abandoned_cart')
|
||||
cart_recovery_email_sent = fields.Boolean('Cart recovery email already sent')
|
||||
website_id = fields.Many2one('website', related='partner_id.website_id', string='Website',
|
||||
help='Website through which this order was placed.',
|
||||
store=True, readonly=True)
|
||||
|
||||
@api.multi
|
||||
@api.depends('website_order_line.product_uom_qty', 'website_order_line.product_id')
|
||||
|
||||
@@ -172,6 +172,7 @@ class Website(models.Model):
|
||||
'partner_invoice_id': partner.id,
|
||||
'partner_shipping_id': addr['delivery'],
|
||||
'user_id': salesperson_id or self.salesperson_id.id or default_user_id,
|
||||
'website_id': self._context.get('website_id'),
|
||||
}
|
||||
company = self.company_id or pricelist.company_id
|
||||
if company:
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# coding: utf-8
|
||||
from . import sale_report
|
||||
@@ -0,0 +1,21 @@
|
||||
# coding: utf-8
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class SaleReport(models.Model):
|
||||
_inherit = 'sale.report'
|
||||
|
||||
website_id = fields.Many2one('website', readonly=True)
|
||||
|
||||
def _select(self):
|
||||
select = super(SaleReport, self)._select()
|
||||
select += ', s.website_id as website_id'
|
||||
|
||||
return select
|
||||
|
||||
def _group_by(self):
|
||||
group_by = super(SaleReport, self)._group_by()
|
||||
group_by += ', s.website_id'
|
||||
|
||||
return group_by
|
||||
@@ -172,6 +172,11 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Website Public Categories">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button class="oe_stat_button" name="website_publish_button" type="object" icon="fa-globe">
|
||||
<field name="website_published" widget="website_button"/>
|
||||
</button>
|
||||
</div>
|
||||
<field name="image_medium" widget='image' class="oe_avatar"/>
|
||||
<div class="oe_left">
|
||||
<group>
|
||||
@@ -219,7 +224,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<field name='currency_id' position='after'>
|
||||
<field name="selectable"/>
|
||||
<field name="website_id" string='Allow to use on ' placeholder="None website" />
|
||||
<field name="website_id" string="Allow to use on " placeholder="None website" groups="website.group_multi_website"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -228,4 +228,14 @@
|
||||
action="product.variants_action"
|
||||
parent="menu_product_settings" groups="product.group_product_variant" sequence="3"/>
|
||||
|
||||
<record id="sale_order_view_form" model="ir.ui.view">
|
||||
<field name="name">sale.order.form</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="origin" position="after">
|
||||
<field name="website_id" groups="website.group_multi_website"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -9,8 +9,8 @@ from odoo.http import request
|
||||
class WebsiteSaleLinkTrackerBackend(WebsiteSaleBackend):
|
||||
|
||||
@http.route()
|
||||
def fetch_dashboard_data(self, date_from, date_to):
|
||||
results = super(WebsiteSaleLinkTrackerBackend, self).fetch_dashboard_data(date_from, date_to)
|
||||
def fetch_dashboard_data(self, website_id, date_from, date_to):
|
||||
results = super(WebsiteSaleLinkTrackerBackend, self).fetch_dashboard_data(website_id, date_from, date_to)
|
||||
results['dashboards']['sales']['utm_graph'] = self.fetch_utm_data(date_from, date_to)
|
||||
return results
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
'data': [
|
||||
'views/product_template_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/website_sale_stock_templates.xml'
|
||||
'views/website_sale_stock_templates.xml',
|
||||
'views/stock_picking_views.xml'
|
||||
],
|
||||
'demo': [
|
||||
'data/website_sale_stock_demo.xml',
|
||||
|
||||
@@ -6,3 +6,4 @@ from . import product_product
|
||||
from . import product_template
|
||||
from . import res_config_settings
|
||||
from . import sale_order
|
||||
from . import stock_picking
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, api, fields
|
||||
from odoo.tools.translate import _
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit = 'stock.picking'
|
||||
|
||||
website_id = fields.Many2one('website', related='partner_id.website_id', string='Website',
|
||||
help='Website this picking belongs to.',
|
||||
store=True, readonly=True)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_picking_form_inherit_website_sale_stock" model="ir.ui.view">
|
||||
<field name="name">stock.picking.form.inherit.website.sale.stock</field>
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="company_id" position="before">
|
||||
<field name="website_id" groups="website.group_multi_website"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -7,6 +7,7 @@
|
||||
'data': [
|
||||
'views/assets.xml',
|
||||
'views/views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
],
|
||||
'depends': ['website'],
|
||||
'auto_install': True,
|
||||
|
||||
@@ -2,3 +2,5 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import ir_module_module
|
||||
from . import ir_ui_view
|
||||
from . import res_config_settings
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IrModuleModule(models.Model):
|
||||
_name = "ir.module.module"
|
||||
@@ -13,6 +16,71 @@ class IrModuleModule(models.Model):
|
||||
image_ids = fields.One2many('ir.attachment', 'res_id',
|
||||
domain=[('res_model', '=', _name), ('mimetype', '=like', 'image/%')],
|
||||
string='Screenshots', readonly=True)
|
||||
is_installed_on_current_website = fields.Boolean(compute='_compute_is_installed_on_current_website')
|
||||
|
||||
@api.multi
|
||||
def _compute_is_installed_on_current_website(self):
|
||||
for module in self:
|
||||
module.is_installed_on_current_website = module == self.env['website'].get_current_website().installed_theme_id
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
if vals.get('state') == 'installed':
|
||||
self._handle_auto_installs()
|
||||
|
||||
return super(IrModuleModule, self).write(vals)
|
||||
|
||||
@api.multi
|
||||
def _assign_views(self):
|
||||
IrModelData = self.env['ir.model.data']
|
||||
for module in self:
|
||||
views = self.env['ir.ui.view'].browse(IrModelData.search([('module', '=', module.name),
|
||||
('model', '=', 'ir.ui.view')]).mapped('res_id'))
|
||||
views.write({
|
||||
'theme_id': module.id
|
||||
})
|
||||
|
||||
@api.model
|
||||
def _handle_auto_installs(self):
|
||||
for module in self.search([('auto_install', '=', True), ('name', '=like', 'theme%'), ('state', 'in', ('installed', 'to install'))]):
|
||||
theme_deps = module.get_upstream_theme_dependencies()
|
||||
websites = self.env['website'].search([]).filtered(lambda w: len(theme_deps - w.theme_ids) == 0)
|
||||
if websites:
|
||||
_logger.info('auto-loading %s on %s', module.name, websites.mapped('name'))
|
||||
websites.write({'theme_ids': [(4, module.id, 0)]})
|
||||
module._assign_views()
|
||||
|
||||
# put demo date on first website
|
||||
module._make_demo_data_website_specific(websites[0])
|
||||
|
||||
@api.multi
|
||||
def _get_module_data(self, model_name):
|
||||
IrModelData = self.env['ir.model.data']
|
||||
records = self.env[model_name]
|
||||
|
||||
for module in self:
|
||||
records |= self.env[model_name].browse(IrModelData.search([('module', '=', module.name), ('model', '=', model_name)]).mapped('res_id'))
|
||||
|
||||
return records
|
||||
|
||||
@api.multi
|
||||
def _make_demo_data_website_specific(self, website):
|
||||
for module in self:
|
||||
menus = module._get_module_data('website.menu')
|
||||
menus.with_context(no_cow=True).write({'website_id': website.id})
|
||||
|
||||
pages = module._get_module_data('website.page')
|
||||
pages.with_context(no_cow=True).write({'website_id': website.id})
|
||||
pages.with_context(no_cow=True).mapped('view_id').write({'website_id': website.id})
|
||||
|
||||
@api.multi
|
||||
def _remove_website_specific_demo_data(self, website):
|
||||
for module in self:
|
||||
menus = module._get_module_data('website.menu')
|
||||
menus.filtered(lambda menu: menu.website_id == website).unlink()
|
||||
|
||||
pages = module._get_module_data('website.page')
|
||||
pages.filtered(lambda page: page.website_id == website).unlink()
|
||||
|
||||
@api.model
|
||||
def update_list(self):
|
||||
@@ -40,6 +108,11 @@ class IrModuleModule(models.Model):
|
||||
|
||||
return res
|
||||
|
||||
def get_upstream_theme_dependencies(self):
|
||||
upstream_deps = self.upstream_dependencies(exclude_states=('uninstallable', 'to remove'))
|
||||
upstream_theme_deps = upstream_deps.filtered(lambda mod: any(mod.name.startswith(keyword) for keyword in ('theme_', 'snippet_', 'website_animate')))
|
||||
return upstream_theme_deps
|
||||
|
||||
@api.multi
|
||||
def button_choose_theme(self):
|
||||
theme_category = self.env.ref('base.module_category_theme', False)
|
||||
@@ -47,15 +120,71 @@ class IrModuleModule(models.Model):
|
||||
|
||||
theme_category_id = theme_category.id if theme_category else 0
|
||||
hidden_categories_ids = [theme_hidden_category.id if theme_hidden_category else 0]
|
||||
current_website = self.env['website'].get_current_website()
|
||||
|
||||
self.search([ # Uninstall the theme(s) which is (are) installed
|
||||
themes_to_uninstall = self.search([ # Uninstall the theme(s) which is (are) installed
|
||||
('state', '=', 'installed'),
|
||||
('category_id', 'not in', hidden_categories_ids),
|
||||
('id', 'in', current_website.theme_ids.ids),
|
||||
'|', ('category_id', '=', theme_category_id), ('category_id.parent_id', '=', theme_category_id)
|
||||
]).button_immediate_uninstall()
|
||||
])
|
||||
|
||||
if themes_to_uninstall:
|
||||
themes_to_uninstall.button_immediate_uninstall()
|
||||
|
||||
modules_belonging_to_theme = self + self.get_upstream_theme_dependencies()
|
||||
current_website.installed_theme_id = self
|
||||
current_website.theme_ids |= modules_belonging_to_theme
|
||||
|
||||
next_action = self.button_immediate_install() # Then install the new chosen one
|
||||
modules_belonging_to_theme._assign_views()
|
||||
modules_belonging_to_theme._make_demo_data_website_specific(current_website)
|
||||
self._handle_auto_installs()
|
||||
_logger.info('installed themes %s on %s', modules_belonging_to_theme.mapped('name'), current_website.name)
|
||||
|
||||
if next_action.get('tag') == 'reload' and not next_action.get('params', {}).get('menu_id'):
|
||||
next_action = self.env.ref('website.action_website').read()[0]
|
||||
|
||||
return next_action
|
||||
|
||||
@api.multi
|
||||
def button_uninstall(self):
|
||||
if not any(mod.name.startswith('theme_') for mod in self):
|
||||
return super(IrModuleModule, self).button_uninstall()
|
||||
|
||||
# only one theme can be uninstalled
|
||||
self.ensure_one()
|
||||
|
||||
Website = self.env['website']
|
||||
current_website = Website.get_current_website()
|
||||
if Website.search_count([('theme_ids', 'in', self.id)]) > 1:
|
||||
modules_belonging_to_theme = self + self.get_upstream_theme_dependencies() + self.downstream_dependencies()
|
||||
current_website.installed_theme_id -= modules_belonging_to_theme
|
||||
current_website.theme_ids -= modules_belonging_to_theme
|
||||
modules_belonging_to_theme._remove_website_specific_demo_data(current_website)
|
||||
_logger.info('removed themes %s from %s', modules_belonging_to_theme.mapped('name'), current_website.name)
|
||||
return True
|
||||
else:
|
||||
uninstalled_modules = self + self.downstream_dependencies()
|
||||
res = super(IrModuleModule, self).button_uninstall()
|
||||
|
||||
for website in Website.search([]):
|
||||
website.installed_theme_id -= uninstalled_modules
|
||||
website.theme_ids -= uninstalled_modules
|
||||
_logger.info('removed themes %s from %s', uninstalled_modules.mapped('name'), website.name)
|
||||
|
||||
# Attempt to uninstall upstream dependencies. This is
|
||||
# necessary for uninstalling e.g. theme_zap, when doing so
|
||||
# theme_treehouse should also be removed from the current
|
||||
# website. This way the user can install a completely new
|
||||
# theme.
|
||||
#
|
||||
# Make sure to only uninstall every module once with
|
||||
# context. Otherwise it might get removed from the current
|
||||
# website the first time and the second time it might end
|
||||
# up being wrongly uninstalled.
|
||||
if self._context.get('uninstall_upstream') is not False:
|
||||
for upstream_dependency in self.get_upstream_theme_dependencies().filtered(lambda mod: mod.name.startswith('theme_')):
|
||||
upstream_dependency.with_context(uninstall_upstream=False).button_uninstall()
|
||||
|
||||
return res
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# coding: utf-8
|
||||
from odoo import api, fields, models
|
||||
|
||||
class View(models.Model):
|
||||
_inherit = 'ir.ui.view'
|
||||
|
||||
@api.model
|
||||
def _get_inheriting_views_arch_website(self, view_id):
|
||||
res = self.env['website'].get_current_website()
|
||||
|
||||
# In certain cases view validation as done by
|
||||
# _validate_module_views will fail. When attempting to load
|
||||
# inherited view CHILD:
|
||||
#
|
||||
# PARENT (theme_common)
|
||||
# ^
|
||||
# |
|
||||
# CHILD (other_theme)
|
||||
#
|
||||
# During the view validation no website_id will be in context
|
||||
# so the view will fail to apply, since PARENT won't be
|
||||
# selected by the inheriting view arch domain.
|
||||
#
|
||||
# This is not a problem however, CHILD will never be loaded on
|
||||
# a website where CHILD is not installed. To simulate this,
|
||||
# return a website which has the theme containing CHILD
|
||||
# installed.
|
||||
if res:
|
||||
return res
|
||||
else:
|
||||
view_theme = self.browse(view_id).theme_id
|
||||
if view_theme:
|
||||
return self.env['website'].search([('theme_ids', 'in', view_theme.id)], limit=1)
|
||||
else:
|
||||
return self.env['website']
|
||||
@@ -0,0 +1,14 @@
|
||||
# coding: utf-8
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
def website_install_theme(self):
|
||||
self.website_id._fix_to_session()
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '/web#action=website_theme_install.theme_install_kanban_action',
|
||||
'target': 'self',
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.website_theme_install</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="website.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<button name="website_go_to" position="after">
|
||||
<button name="website_install_theme" type="object" string="Change theme"/>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -13,17 +13,21 @@
|
||||
<field name="image_ids"/>
|
||||
<field name="category_id"/>
|
||||
<field name="display_name"/>
|
||||
<field name="is_installed_on_current_website"/>
|
||||
<templates>
|
||||
<div t-name="kanban-box" t-attf-class="o_theme_preview mb16 mt16 #{record.state.raw_value == 'installed'? 'o_theme_installed' : ''}">
|
||||
<div t-name="kanban-box" t-attf-class="o_theme_preview mb16 mt16 #{record.is_installed_on_current_website.raw_value? 'o_theme_installed' : ''}">
|
||||
<t t-set="has_image" t-value="record.image_ids.raw_value.length > 0"/>
|
||||
<t t-set="has_screenshot" t-value="record.image_ids.raw_value.length > 1"/>
|
||||
<t t-set="image_url" t-value="has_image ? 'web/image/' + record.image_ids.raw_value[0] : record.icon.value"/>
|
||||
|
||||
<div class="o_theme_preview_top bg-white mb4">
|
||||
<div class="o_browser_bar"/>
|
||||
<i states="installed" class="text-center bg-white text-success rounded-circle fa fa-check-circle" role="img" aria-label="Installed" title="Installed"/>
|
||||
<i states="installed" t-if="record.is_installed_on_current_website.raw_value" class="text-center bg-white text-success img-circle fa fa-check-circle" role="img" aria-label="Installed" title="Installed"/>
|
||||
<div t-attf-class="bg-gray-lighter #{has_screenshot? 'o_theme_screenshot' : (has_image ? 'o_theme_cover' : 'o_theme_logo')}" t-attf-style="background-image: url(#{image_url});"/>
|
||||
<div states="uninstalled" class="o_button_area">
|
||||
<div t-if="record.is_installed_on_current_website.raw_value" class="o_button_area">
|
||||
<button type="object" name="button_immediate_uninstall" class="btn btn-primary">Uninstall theme</button>
|
||||
</div>
|
||||
<div t-else="" class="o_button_area">
|
||||
<button type="object" name="button_choose_theme" class="btn btn-primary">Use this theme</button>
|
||||
<hr t-if="record.url.value"/>
|
||||
<a role="button" t-if="record.url.value" class="btn btn-secondary" t-att-href="record.url.value" target="_blank">Live Preview</a>
|
||||
@@ -59,9 +63,14 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- themes should be installed through website_theme_install -->
|
||||
<record id="base.open_module_tree" model="ir.actions.act_window">
|
||||
<field name="domain">['!', ('name', '=like', 'theme_%')]</field>
|
||||
</record>
|
||||
|
||||
<!-- Actions to list themes with custom kanban (launched on module installation) -->
|
||||
<record id="theme_install_kanban_action" model="ir.actions.act_window">
|
||||
<field name="name">Choose Your Theme</field>
|
||||
<field name="name">Choose a theme for this website</field>
|
||||
<field name="res_model">ir.module.module</field>
|
||||
<field name="view_mode">kanban</field>
|
||||
<field name="view_id" ref="theme_view_kanban"/>
|
||||
@@ -71,6 +80,7 @@
|
||||
'|', ('category_id', '=', ref('base.module_category_theme', False)), ('category_id.parent_id', '=', ref('base.module_category_theme', False))
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record id="theme_install_todo_action" model="ir.actions.server">
|
||||
<field name="name">Config: Choose Your Theme</field>
|
||||
<field name="model_id" ref="model_ir_module_module"/>
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
|
||||
<record model="ir.module.category" id="module_category_theme">
|
||||
<field name="name">Theme</field>
|
||||
<field name="exclusive" eval="1"/>
|
||||
<field name="exclusive" eval="0"/>
|
||||
<field name="sequence">50</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -214,7 +214,8 @@ class AssetsBundle(object):
|
||||
"""
|
||||
ira = self.env['ir.attachment']
|
||||
domain = [
|
||||
('url', '=like', '/web/content/%-%/{0}{1}%.{2}'.format( # The wilcards are id, version and pagination number (if any)
|
||||
('url', '=like', '/web/content/%-%-{}/{}{}%.{}'.format( # The wilcards are id, version and pagination number (if any)
|
||||
self.env.context.get('website_id'),
|
||||
('rtl/' if type == 'css' and self.user_direction == 'rtl' else ''),
|
||||
self.name,
|
||||
type)),
|
||||
@@ -235,8 +236,9 @@ class AssetsBundle(object):
|
||||
by file name and only return the one with the max id for each group.
|
||||
"""
|
||||
version = "%" if ignore_version else self.version
|
||||
url_pattern = '/web/content/%-{0}/{1}{2}{3}.{4}'.format(
|
||||
url_pattern = '/web/content/%-{}-{}/{}{}{}.{}'.format(
|
||||
version,
|
||||
self.env.context.get('website_id'),
|
||||
('rtl/' if type == 'css' and self.user_direction == 'rtl' else ''),
|
||||
self.name,
|
||||
'.%' if type == 'css' else '',
|
||||
@@ -270,7 +272,7 @@ class AssetsBundle(object):
|
||||
values = {
|
||||
'name': "/web/content/%s" % type,
|
||||
'datas_fname': fname,
|
||||
'mimetype' : mimetype,
|
||||
'mimetype': mimetype,
|
||||
'res_model': 'ir.ui.view',
|
||||
'res_id': False,
|
||||
'type': 'binary',
|
||||
@@ -279,7 +281,13 @@ class AssetsBundle(object):
|
||||
}
|
||||
attachment = ira.sudo().create(values)
|
||||
|
||||
url = '/web/content/%s-%s%s/%s' % (attachment.id, self.version, ('/rtl' if type == 'css' and self.user_direction == 'rtl' else ''), fname)
|
||||
url = '/web/content/%s-%s-%s%s/%s' % (
|
||||
attachment.id,
|
||||
self.version,
|
||||
self.env.context.get('website_id'),
|
||||
('/rtl' if type == 'css' and self.user_direction == 'rtl' else ''),
|
||||
fname
|
||||
)
|
||||
values = {
|
||||
'name': url,
|
||||
'url': url,
|
||||
|
||||
@@ -243,6 +243,10 @@ class IrHttp(models.AbstractModel):
|
||||
def content_disposition(cls, filename):
|
||||
return content_disposition(filename)
|
||||
|
||||
@classmethod
|
||||
def _xmlid_to_obj(cls, env, xmlid):
|
||||
return env.ref(xmlid, False)
|
||||
|
||||
@classmethod
|
||||
def binary_content(cls, xmlid=None, model='ir.attachment', id=None, field='datas',
|
||||
unique=False, filename=None, filename_field='datas_fname', download=False,
|
||||
@@ -277,7 +281,7 @@ class IrHttp(models.AbstractModel):
|
||||
# get object and content
|
||||
obj = None
|
||||
if xmlid:
|
||||
obj = env.ref(xmlid, False)
|
||||
obj = cls._xmlid_to_obj(env, xmlid)
|
||||
elif id and model == 'ir.attachment' and access_token:
|
||||
obj = env[model].sudo().browse(int(id))
|
||||
if not consteq(obj.access_token, access_token):
|
||||
|
||||
@@ -545,6 +545,10 @@ class Users(models.Model):
|
||||
# extra records will be deleted by the periodical garbage collection
|
||||
self.env['res.users.log'].create({}) # populated by defaults
|
||||
|
||||
@api.model
|
||||
def _get_login_domain(self, login):
|
||||
return [('login', '=', login)]
|
||||
|
||||
@classmethod
|
||||
def _login(cls, db, login, password):
|
||||
if not password:
|
||||
@@ -554,13 +558,11 @@ class Users(models.Model):
|
||||
with cls.pool.cursor() as cr:
|
||||
self = api.Environment(cr, SUPERUSER_ID, {})[cls._name]
|
||||
with self._assert_can_auth():
|
||||
user = self.search([('login', '=', login)])
|
||||
if not user:
|
||||
raise AccessDenied()
|
||||
|
||||
user = user.sudo(user.id)
|
||||
user._check_credentials(password)
|
||||
user._update_last_login()
|
||||
user = self.search(self._get_login_domain(login))
|
||||
if user:
|
||||
user = user.sudo(user.id)
|
||||
user._check_credentials(password)
|
||||
user._update_last_login()
|
||||
except AccessDenied:
|
||||
_logger.info("Login failed for db:%s login:%s from %s", db, login, ip)
|
||||
raise
|
||||
|
||||
Reference in New Issue
Block a user