diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py
index 7dbc3415f21..9509a541c88 100644
--- a/addons/web_editor/controllers/main.py
+++ b/addons/web_editor/controllers/main.py
@@ -146,6 +146,9 @@ class Web_Editor(http.Controller):
# therefore we have to recover the files from the request object
Attachments = request.env['ir.attachment'] # registry for the attachment table
+ website = request.env['website'].get_current_website() # # TO MOVE
+
+
res_model = kwargs.get('res_model', 'ir.ui.view')
if res_model != 'ir.ui.view' and kwargs.get('res_id'):
res_id = int(kwargs['res_id'])
@@ -167,6 +170,8 @@ class Web_Editor(http.Controller):
'public': res_model == 'ir.ui.view',
'res_id': res_id,
'res_model': res_model,
+ 'website_id': website.id,
+
})
attachment.generate_access_token()
uploads += attachment.read(['name', 'mimetype', 'checksum', 'url', 'res_id', 'res_model', 'access_token'])
@@ -198,6 +203,8 @@ class Web_Editor(http.Controller):
'public': res_model == 'ir.ui.view',
'res_id': res_id,
'res_model': res_model,
+ 'website_id': website.id, # TO MOVE IN WEBSITEID
+
})
attachment.generate_access_token()
attachments += attachment
diff --git a/addons/website/models/ir_attachment.py b/addons/website/models/ir_attachment.py
index 3225fa0918f..15fe50f5471 100644
--- a/addons/website/models/ir_attachment.py
+++ b/addons/website/models/ir_attachment.py
@@ -2,16 +2,18 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
-from odoo import api, fields, models
-
+from odoo import fields, models, api
_logger = logging.getLogger(__name__)
+
class Attachment(models.Model):
_inherit = "ir.attachment"
# related for backward compatibility with saas-6
website_url = fields.Char(string="Website URL", related='local_url', deprecated=True)
+ key = fields.Char(help='Technical field used to resolve multiple attachments in a multi-website environment.')
+ website_id = fields.Many2one('website')
@api.model
def create(self, vals):
@@ -23,50 +25,3 @@ 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
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index 21c37801b96..e3eb27a9d77 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -255,8 +255,8 @@ class Http(models.AbstractModel):
@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 website_id and website_id.theme_id:
+ obj = env['ir.attachment'].search([('key', '=', xmlid), ('website_id', '=', website_id.id)])
if obj:
return obj[0]
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index c7a87da022e..00354a43613 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -23,11 +23,12 @@ class View(models.Model):
website_id = fields.Many2one('website', ondelete='cascade', string="Website")
page_ids = fields.One2many('website.page', 'view_id')
first_page_id = fields.Many2one('website.page', string='Website Page', help='First page linked to this view', compute='_compute_first_page_id')
- theme_id = fields.Many2one('ir.module.module')
+ # theme_id = fields.Many2one('ir.module.module')
- @api.one
+ @api.multi
def _compute_first_page_id(self):
- self.first_page_id = self.env['website.page'].search([('view_id', '=', self.id)], limit=1)
+ for view in self:
+ view.first_page_id = self.env['website.page'].search([('view_id', '=', view.id)], limit=1)
@api.multi
def write(self, vals):
@@ -38,11 +39,6 @@ class View(models.Model):
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})
@@ -81,30 +77,10 @@ class View(models.Model):
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({
+ page.copy({
'view_id': new_view.id,
})
@@ -184,9 +160,6 @@ class View(models.Model):
# 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])
@@ -207,10 +180,7 @@ class View(models.Model):
def get_view_id(self, xml_id):
if 'website_id' in self._context and not isinstance(xml_id, pycompat.integer_types):
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)] + current_website.website_domain()
- domain = expression.AND([expression.OR([theme_views_domain, website_views_domain]), key_domain])
+ domain = ['&', ('key', '=', xml_id)] + current_website.website_domain()
view = self.search(domain, order='website_id', limit=1)
if not view:
diff --git a/addons/website/models/res_users.py b/addons/website/models/res_users.py
index 5071ad6ca08..6af02a52ce3 100644
--- a/addons/website/models/res_users.py
+++ b/addons/website/models/res_users.py
@@ -16,7 +16,7 @@ class ResUsers(models.Model):
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',
+ 'Multi-Websites',
compute='_compute_groups_id', inverse='_inverse_groups_id',
group_xml_id='website.group_multi_website')
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index 1fd2ea4b5e5..913334fc086 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -101,8 +101,8 @@ 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')
+ theme_id = fields.Many2one('ir.module.module', help='Installed theme')
+
auth_signup_uninvited = fields.Selection([
('b2b', 'On invitation'),
('b2c', 'Free sign up'),
@@ -168,13 +168,14 @@ class Website(models.Model):
self.homepage_id = self.env['website.page'].search([('website_id', '=', self.id),
('key', '=', standard_homepage.key)])
top_menu = self.env['website.menu'].create({
- 'name': 'Top Menu for website ' + str(self.id),
+ 'name': _('Top Menu for website %s') % self.id,
'website_id': self.id,
'sequence': 0
})
self.menu_id = top_menu.id
self.env['website.menu'].create({
- 'name': 'Home', 'url': '/',
+ 'name': _('Home'),
+ 'url': '/',
'website_id': self.id,
'parent_id': top_menu.id,
'sequence': 10
@@ -391,15 +392,16 @@ class Website(models.Model):
return dependencies
- @api.model
- def page_exists(self, name, module='website'):
- try:
- name = (name or "").replace("/website.", "").replace("/", "")
- if not name:
- return False
- return self.env.ref('%s.%s' % module, name)
- except Exception:
- return False
+ # removed by 3c9e6c89e7207636c1bdab4ed8118c0c1089d43e
+ # @api.model
+ # def page_exists(self, name, module='website'):
+ # try:
+ # name = (name or "").replace("/website.", "").replace("/", "")
+ # if not name:
+ # return False
+ # return self.env.ref('%s.%s' % module, name)
+ # except Exception:
+ # return False
# ----------------------------------------------------------
# Languages
@@ -532,8 +534,7 @@ class Website(models.Model):
endpoint.routing['type'] == 'http' and
endpoint.routing['auth'] in ('none', 'public') and
endpoint.routing.get('website', False) and
- all(hasattr(converter, 'generate') for converter in converters) and
- endpoint.routing.get('website')):
+ all(hasattr(converter, 'generate') for converter in converters)):
return False
# dont't list routes without argument having no default value or converter
@@ -915,21 +916,14 @@ class Page(models.Model):
@api.multi
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
- 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)
- website_id = default.get('website_id')
- # website_id should be set during copy to avoid copying a generic page to another generic copy and then write on it
- # which would result in the generic page copied to a generic one and a specific one
- new_view = view.copy({'key': view.key + '.copy', 'name': '%s %s' % (view.name, _('(copy)')), 'website_id': website_id.id})
- default = {
- 'name': '%s %s' % (self.name, _('(copy)')),
- 'url': self.env['website'].get_unique_path(self.url),
- 'view_id': new_view.id,
- }
+ if default:
+ if not default.get('view_id'):
+ view = self.env['ir.ui.view'].browse(self.view_id.id)
+ new_view = view.copy({'website_id': default.get('website_id')})
+ default['view_id'] = new_view.id
+
+ default['name'] = default.get('name', '%s %s' % (self.name, _('(copy)')))
+ default['url'] = default.get('url', self.env['website'].get_unique_path(self.url))
return super(Page, self).copy(default=default)
@api.model
@@ -938,7 +932,7 @@ class Page(models.Model):
:param page_id : website.page identifier
"""
page = self.browse(int(page_id))
- new_page = page.copy(dict(website_id=self.env['website'].get_current_website()))
+ new_page = page.copy(dict(website_id=self.env['website'].get_current_website().id))
# Should not clone menu if the page was cloned from one website to another
# Eg: Cloning a generic page (no website) will create a page with a website, we can't clone menu (not same container)
if clone_menu and new_page.website_id == page.website_id:
@@ -1013,7 +1007,7 @@ class Menu(models.Model):
if vals.get('website_id'):
return super(Menu, self).create(vals)
elif self._context.get('website_id'):
- vals.update({'website_id': self._context.get('website_id')})
+ vals['website_id'] = self._context.get('website_id')
return super(Menu, self).create(vals)
else:
# create for every site
diff --git a/addons/website/views/ir_attachment_views.xml b/addons/website/views/ir_attachment_views.xml
index c553c280d2f..7be322eaa65 100644
--- a/addons/website/views/ir_attachment_views.xml
+++ b/addons/website/views/ir_attachment_views.xml
@@ -1,13 +1,12 @@
-
+
ir.attachment.form.inherit.website
ir.attachment
-
-
+
diff --git a/addons/website/views/res_config_settings_views.xml b/addons/website/views/res_config_settings_views.xml
index eac57f25ec5..d1e56621e1e 100644
--- a/addons/website/views/res_config_settings_views.xml
+++ b/addons/website/views/res_config_settings_views.xml
@@ -104,7 +104,18 @@
-
+
+
+
+
+
+
+
+ Manage multiple websites
+
+
+
+
-
-
-
-
-
-
-
-
- Track clicks on UTM links
-
-
-
-
- Multi-Website
-
-
-
-
-
-
-
-
- Manage multiple websites
-
-
-
diff --git a/addons/website/views/website_views.xml b/addons/website/views/website_views.xml
index 2c5d5dc4daa..0153704be49 100644
--- a/addons/website/views/website_views.xml
+++ b/addons/website/views/website_views.xml
@@ -45,8 +45,7 @@
-
-
+
@@ -265,7 +264,6 @@
-
diff --git a/addons/website_theme_install/__manifest__.py b/addons/website_theme_install/__manifest__.py
index 854673b0b29..26266cb8ff3 100644
--- a/addons/website_theme_install/__manifest__.py
+++ b/addons/website_theme_install/__manifest__.py
@@ -8,6 +8,7 @@
'views/assets.xml',
'views/views.xml',
'views/res_config_settings_views.xml',
+ 'security/ir.model.access.csv',
],
'depends': ['website'],
'auto_install': True,
diff --git a/addons/website_theme_install/models/__init__.py b/addons/website_theme_install/models/__init__.py
index 786d2febfb6..0e35edfc5eb 100644
--- a/addons/website_theme_install/models/__init__.py
+++ b/addons/website_theme_install/models/__init__.py
@@ -2,5 +2,7 @@
# 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_company
from . import res_config_settings
+from . import theme_models
+from . import website
diff --git a/addons/website_theme_install/models/ir_module_module.py b/addons/website_theme_install/models/ir_module_module.py
index 560bd37a932..7207a613ade 100644
--- a/addons/website_theme_install/models/ir_module_module.py
+++ b/addons/website_theme_install/models/ir_module_module.py
@@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import os
import logging
+import os
from odoo import api, fields, models
-
_logger = logging.getLogger(__name__)
@@ -16,71 +15,174 @@ class IrModuleModule(models.Model):
image_ids = fields.One2many('ir.attachment', 'res_id',
domain=[('res_model', '=', _name), ('mimetype', '=like', 'image/%')],
string='Screenshots', readonly=True)
+ # for kanban view
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
+ module.is_installed_on_current_website = module == self.env['website'].get_current_website().theme_id
@api.multi
def write(self, vals):
- if vals.get('state') == 'installed':
- self._handle_auto_installs()
-
+ if vals.get('state') == 'installed' and self.name.startswith('theme_'):
+ _logger.info('Module %s has been loaded as theme template' % self.name)
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'))
-
+ imd_ids = IrModelData.search([('module', '=', module.name), ('model', '=', model_name)]).mapped('res_id')
+ records |= self.env[model_name].browse(imd_ids)
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})
+ def _load_one_theme_module(self, website, loaded):
+ # loaded is a dict updated in place !
+ # keep previous record created to update overrided record
+ _logger.info('Load theme %s for website %s from template.' % (self.mapped('name'), 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})
+ for attach in self._get_module_data('theme.ir.attachment'):
+ already_create = loaded['attachments'].filtered(lambda x: x.key == attach.key)
+ new_attach = {
+ 'public': True,
+ 'res_model': 'ir.ui.view',
+ 'type': 'url',
+ 'name': attach.name,
+ 'url': attach.url,
+ 'website_id': website.id,
+ 'theme_template_id': attach.id,
+ }
+ if already_create:
+ already_create.update(new_attach)
+ else: # if module B override attachment from dependence A
+ loaded['attachments'] += self.env['ir.attachment'].create(new_attach)
+
+ for view in self._get_module_data('theme.ir.ui.view'):
+ if view.inherit_id and view.inherit_id._name == 'theme.ir.ui.view':
+ view.inherit_id = loaded['views'].filtered(lambda x: x.theme_template_id == view.inherit_id)
+
+ new_view = {
+ 'type': view.type or 'qweb',
+ 'name': view.name,
+ 'arch': view.arch,
+ 'key': view.key,
+ 'arch': view.arch,
+ 'arch_fs': view.arch_fs,
+ 'priority': view.priority,
+ 'active': view.active,
+ 'inherit_id': view.inherit_id and view.inherit_id.id,
+ 'theme_template_id': view.id,
+ 'website_id': website.id,
+ }
+ loaded['views'] += self.env['ir.ui.view'].create(new_view)
+
+ for page in self._get_module_data('theme.website.page'):
+ new_page = {
+ 'url': page.url,
+ 'view_id': loaded['views'].filtered(lambda x: x.theme_template_id == page.view_id).id,
+ 'website_indexed': page.website_indexed,
+ 'theme_template_id': page.id,
+ }
+ loaded['pages'] += self.env['website.page'].create(new_page)
+
+ for menu in self._get_module_data('theme.website.menu'):
+ new_menu = {
+ 'name': menu.name,
+ 'url': menu.url,
+ 'page_id': loaded['pages'].filtered(lambda x: x.theme_template_id == menu.page_id).id,
+ 'new_window': menu.new_window,
+ 'sequence': menu.sequence,
+ 'parent_id': loaded['menus'].filtered(lambda x: x.theme_template_id == menu.parent_id).id,
+ 'theme_template_id': menu.id,
+ }
+ loaded['menus'] += self.env['website.menu'].create(new_menu)
+
+ return True
@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()
+ def _unload_one_theme_module(self, website):
+ _logger.info('Unload theme %s for website %s from template.' % (self.mapped('name'), website.id))
- pages = module._get_module_data('website.page')
- pages.filtered(lambda page: page.website_id == website).unlink()
+ for module in self:
+ menus = module._get_module_data('theme.website.menu')
+ menu_todel = menus.mapped('copy_ids').filtered(lambda m: m.website_id == website)
+ menu_todel.unlink()
+
+ pages = module._get_module_data('theme.website.page')
+ pages_todel = pages.mapped('copy_ids').filtered(lambda p: p.website_id == website)
+ pages_todel.unlink()
+
+ attachs = module._get_module_data('theme.ir.attachment')
+ atachs_todel = attachs.mapped('copy_ids').filtered(lambda a: a.website_id == website)
+ atachs_todel.unlink()
+
+ views = module._get_module_data('theme.ir.ui.view')
+ views_todel = views.mapped('copy_ids').filtered(lambda v: v.website_id == website)
+ views_todel.unlink()
+
+ def _remove_theme_on_website(self, website):
+ installed_deps = website.theme_id.upstream_dependencies(exclude_states=('',)).filtered(lambda x: x.name.startswith('theme_'))
+ for mod in installed_deps:
+ mod._unload_one_theme_module(website)
+
+ def _copy_theme_on_website(self, website):
+ loaded = {
+ 'views': self.env['ir.ui.view'],
+ 'attachments': self.env['ir.attachment'],
+ 'pages': self.env['website.page'],
+ 'menus': self.env['website.menu'],
+ }
+ mods_to_load = reversed(self + self.upstream_dependencies(exclude_states=('',)).filtered(lambda x: x.name.startswith('theme_')))
+ for mod in mods_to_load:
+ mod._load_one_theme_module(website, loaded)
+ self.env['theme.utils']._post_copy(mod)
+
+ @api.multi
+ def button_choose_theme(self):
+ website = self.env['website'].get_current_website()
+
+ # Unload previous theme
+ if website.theme_id:
+ website.theme_id._remove_theme_on_website(website)
+
+ website.theme_id = self
+
+ # create template data of missing theme
+ next_action = False
+ if self.state != 'installed':
+ next_action = self.button_immediate_install()
+
+ # Copy new theme from template table to real table
+ self._copy_theme_on_website(website)
+
+ # Alter next action for redirect
+ if not next_action:
+ next_action = website.button_go_website()
+ 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
+
+ def button_remove_theme(self):
+ website = self.env['website'].get_current_website()
+ self._remove_theme_on_website(website)
+ website.theme_id = False
+
+ def button_refresh_theme(self):
+ website = self.env['website'].get_current_website()
+ # load data from xml to template table
+ self.button_immediate_upgrade()
+
+ # delete current theme
+ # todo: quid of no_update record?
+ self.button_remove_theme()
+
+ # copy data from template table to real table
+ self._copy_theme_on_website(website)
@api.model
def update_list(self):
@@ -105,86 +207,4 @@ class IrModuleModule(models.Model):
'res_model': self._name,
'res_id': app.id,
})
-
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)
- theme_hidden_category = self.env.ref('base.module_category_theme_hidden', False)
-
- 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()
-
- 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)
- ])
-
- 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
diff --git a/addons/website_theme_install/models/ir_ui_view.py b/addons/website_theme_install/models/ir_ui_view.py
deleted file mode 100644
index a222fbd884f..00000000000
--- a/addons/website_theme_install/models/ir_ui_view.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# 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']
diff --git a/addons/website_theme_install/models/res_company.py b/addons/website_theme_install/models/res_company.py
new file mode 100644
index 00000000000..f125a87dca3
--- /dev/null
+++ b/addons/website_theme_install/models/res_company.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+from ast import literal_eval
+
+
+class ResCompany(models.Model):
+ _inherit = 'res.company'
+
+ def _compute_website_theme_onboarding_done(self):
+ """ The step is marked as done if one theme is installed. """
+ # we need the same domain as the existing action
+ action = self.env.ref('website_theme_install.theme_install_kanban_action').read()[0]
+ domain = literal_eval(action['domain'])
+ domain.append(('state', '=', 'installed'))
+ installed_themes_count = self.env['ir.module.module'].sudo().search_count(domain)
+ for record in self:
+ record.website_theme_onboarding_done = (installed_themes_count > 0)
+
+ website_theme_onboarding_done = fields.Boolean("Onboarding website theme step done",
+ compute='_compute_website_theme_onboarding_done')
+
+ @api.model
+ def action_open_website_theme_selector(self):
+ action = self.env.ref('website_theme_install.theme_install_kanban_action').read()[0]
+ action['target'] = 'new'
+ return action
diff --git a/addons/website_theme_install/models/res_config_settings.py b/addons/website_theme_install/models/res_config_settings.py
index 271114a430f..f080cea9575 100644
--- a/addons/website_theme_install/models/res_config_settings.py
+++ b/addons/website_theme_install/models/res_config_settings.py
@@ -1,11 +1,11 @@
# coding: utf-8
-from odoo import api, fields, models
+from odoo import models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
- def website_install_theme(self):
+ def install_theme_on_current_website(self):
self.website_id._force()
return {
'type': 'ir.actions.act_url',
diff --git a/addons/website_theme_install/models/theme_models.py b/addons/website_theme_install/models/theme_models.py
new file mode 100644
index 00000000000..428fa55a559
--- /dev/null
+++ b/addons/website_theme_install/models/theme_models.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+from odoo import fields, models
+from odoo.modules.module import get_resource_from_path
+
+_logger = logging.getLogger(__name__)
+
+
+class ThemeView(models.Model):
+ _name = 'theme.ir.ui.view'
+
+ def compute_arch_fs(self):
+ path_info = get_resource_from_path(self._context['install_filename'])
+ if path_info:
+ return '/'.join(path_info[0:2])
+
+ name = fields.Char(required=True)
+ key = fields.Char()
+ type = fields.Char()
+ priority = fields.Integer(default=16, required=True)
+ active = fields.Boolean(default=True)
+ arch = fields.Text()
+ arch_fs = fields.Char(default=compute_arch_fs)
+ inherit_id = fields.Reference(selection=[('ir.ui.view', 'ir.ui.view'), ('theme.ir.ui.view', 'theme.ir.ui.view')])
+ copy_ids = fields.One2many('ir.ui.view', 'theme_template_id', 'Views using a copy of me', copy=False, readonly=True)
+
+
+class ThemeAttachment(models.Model):
+ _name = 'theme.ir.attachment'
+
+ name = fields.Char(required=True)
+ key = fields.Char(required=True)
+ url = fields.Char()
+ copy_ids = fields.One2many('ir.attachment', 'theme_template_id', 'Attachment using a copy of me', copy=False, readonly=True)
+
+
+class ThemeMenu(models.Model):
+ _name = 'theme.website.menu'
+
+ name = fields.Char(required=True)
+ url = fields.Char(default='')
+ page_id = fields.Many2one('theme.website.page', ondelete='cascade')
+ new_window = fields.Boolean('New Window')
+ sequence = fields.Integer()
+ parent_id = fields.Many2one('theme.website.menu', index=True, ondelete="cascade")
+ copy_ids = fields.One2many('website.menu', 'theme_template_id', 'Menu using a copy of me', copy=False, readonly=True)
+
+
+class ThemePage(models.Model):
+ _name = 'theme.website.page'
+
+ url = fields.Char()
+ view_id = fields.Many2one('theme.ir.ui.view', required=True, ondelete="cascade")
+ website_indexed = fields.Boolean('Page Indexed', default=True)
+ copy_ids = fields.One2many('website.page', 'theme_template_id', 'Theme using a copy of me', copy=False, readonly=True)
+
+
+class Theme(models.AbstractModel):
+ _name = 'theme.utils'
+ _auto = False
+
+ def _post_copy(self, mod):
+ theme_post_copy = '_%s_post_copy' % mod.name
+ if hasattr(self, theme_post_copy):
+ _logger.info('Executing method %s' % theme_post_copy)
+ method = getattr(self, theme_post_copy)
+ return method()
+
+
+class IrUiView(models.Model):
+ _inherit = 'ir.ui.view'
+
+ theme_template_id = fields.Many2one('theme.ir.ui.view')
+
+
+class IrAttachment(models.Model):
+ _inherit = 'ir.attachment'
+
+ key = fields.Char()
+ theme_template_id = fields.Many2one('theme.ir.attachment')
+
+
+class WebiteMenu(models.Model):
+ _inherit = 'website.menu'
+
+ theme_template_id = fields.Many2one('theme.website.menu')
+
+
+class WebsitePage(models.Model):
+ _inherit = 'website.page'
+
+ theme_template_id = fields.Many2one('theme.website.page')
diff --git a/addons/website_theme_install/models/website.py b/addons/website_theme_install/models/website.py
new file mode 100644
index 00000000000..46ee9df91e2
--- /dev/null
+++ b/addons/website_theme_install/models/website.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models
+
+
+class Website(models.Model):
+ _name = "website"
+ _inherit = _name
+
+ def button_view_theme(self):
+ self._force()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': '/web#action=website_theme_install.theme_install_kanban_action',
+ 'target': 'self',
+ }
+
+ def button_go_website(self):
+ self._force()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': '/',
+ 'target': 'self',
+ }
diff --git a/addons/website_theme_install/security/ir.model.access.csv b/addons/website_theme_install/security/ir.model.access.csv
new file mode 100644
index 00000000000..fa2d6a8b262
--- /dev/null
+++ b/addons/website_theme_install/security/ir.model.access.csv
@@ -0,0 +1,6 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_theme_ir_ui_view,access_theme_ir_ui_view,model_theme_ir_ui_view,base.group_system,1,1,1,1
+access_theme_ir_attachment,access_theme_ir_attachment,model_theme_ir_attachment,base.group_system,1,1,1,1
+access_theme_website_menu,access_theme_website_menu,model_theme_website_menu,base.group_system,1,1,1,1
+access_theme_website_page,access_theme_website_page,model_theme_website_page,base.group_system,1,1,1,1
+
diff --git a/addons/website_theme_install/static/src/scss/website_theme_install.scss b/addons/website_theme_install/static/src/scss/website_theme_install.scss
index cd83f76a4e7..ac4cf8ba575 100644
--- a/addons/website_theme_install/static/src/scss/website_theme_install.scss
+++ b/addons/website_theme_install/static/src/scss/website_theme_install.scss
@@ -25,9 +25,9 @@
background: none;
border: none;
- .o_browser_bar:before {
+ .o_browser_bar:after {
content: "•••";
- display: block;
+ display: inline-block;
padding-left: 10px;
color: darken($o-theme-kanban-gray, 10%);
font: 12px/20px Courier, monospace;
@@ -35,13 +35,10 @@
}
&.o_theme_installed .o_theme_preview_top {
- border-color: $o-brand-primary;
+ border: 3px solid $o-brand-primary;
- > .fa-check-circle {
- @include o-position-absolute(10px, 10px);
- width: 20px;
- height: 20px;
- font-size: 20px;
+ .fa {
+ color: $o-brand-primary;
}
}
@@ -95,18 +92,16 @@
}
}
- &:not(.o_theme_installed) {
- .o_theme_preview_top:hover {
- transition: all .3s ease 0s;
- transform: translateY(-10px);
- border-color: darken($o-theme-kanban-gray, 26%);
- box-shadow: 0 15px 12px -8px rgba(0, 0, 0, .4);
+ .o_theme_preview_top:hover {
+ transition: all .3s ease 0s;
+ transform: translateY(-10px);
+ border-color: darken($o-theme-kanban-gray, 26%);
+ box-shadow: 0 15px 12px -8px rgba(0, 0, 0, .4);
- .o_button_area {
- opacity: 1;
- visibility: visible;
- transition: opacity 0.2s ease 0.1s;
- }
+ .o_button_area {
+ opacity: 1;
+ visibility: visible;
+ transition: opacity 0.2s ease 0.1s;
}
}
}
diff --git a/addons/website_theme_install/views/res_config_settings_views.xml b/addons/website_theme_install/views/res_config_settings_views.xml
index 635b202b187..b7c3598569c 100644
--- a/addons/website_theme_install/views/res_config_settings_views.xml
+++ b/addons/website_theme_install/views/res_config_settings_views.xml
@@ -5,9 +5,11 @@
res.config.settings
-
+
+
+
+
+
diff --git a/addons/website_theme_install/views/views.xml b/addons/website_theme_install/views/views.xml
index a915dc47f18..7ad568c4def 100644
--- a/addons/website_theme_install/views/views.xml
+++ b/addons/website_theme_install/views/views.xml
@@ -21,11 +21,14 @@
-
-
+
+
+
- Uninstall theme
+ Update theme
+
+ Remove theme
Use this theme
@@ -107,4 +110,34 @@ action = {
+
+
+
+ website.form
+ website
+
+
+
+
+
+
+ View themes
+
+
+
+
+
+
+
+
+
+ Website Theme
+ Select a theme for your website.
+ Let's start!
+
+
+
+
+
+
diff --git a/odoo/tools/convert.py b/odoo/tools/convert.py
index 6e276a65cf6..9104cf6d205 100644
--- a/odoo/tools/convert.py
+++ b/odoo/tools/convert.py
@@ -573,7 +573,7 @@ form: module.record_id""" % (xml_id,)
def _tag_record(self, rec, data_node=None, mode=None):
rec_model = rec.get("model")
model = self.env[rec_model]
- rec_id = rec.get("id",'')
+ rec_id = rec.get("id", '')
rec_context = rec.get("context", {})
if rec_context:
rec_context = safe_eval(rec_context)
@@ -673,9 +673,14 @@ form: module.record_id""" % (xml_id,)
el.tag = 'data'
el.attrib.pop('id', None)
+ if self.module.startswith('theme_'):
+ model = 'theme.ir.ui.view'
+ else:
+ model = 'ir.ui.view'
+
record_attrs = {
'id': tpl_id,
- 'model': 'ir.ui.view',
+ 'model': model,
}
for att in ['forcecreate', 'context']:
if att in el.attrib: