[ADD] website_theme_install: try to make theme multi website compliant

From now, when you install a theme, it load the data from xml to template
table theme [ir.ui.view|ir.attachment|website.page|website.menu].

Data are only copied from this template table into the real table when you
choose a theme on a website; Making them website_specific and with a link
to the original to allow futur update.

A special case is done to create theme.ir.ui.view when you are installing
a theme, even if you continue to use template tag to create quick view.

Co-authored-by: Derie Romain <rde@odoo.com>
Co-authored-by: Kersten Jérémy <jke@odoo.com>
This commit is contained in:
Jeremy Kersten
2018-08-13 20:16:34 +02:00
co-authored by Derie Romain
parent 066cfc9598
commit c0968e68ed
22 changed files with 429 additions and 344 deletions
+7
View File
@@ -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
+4 -49
View File
@@ -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
+2 -2
View File
@@ -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]
+6 -36
View File
@@ -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:
+1 -1
View File
@@ -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')
+26 -32
View File
@@ -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
+2 -3
View File
@@ -1,13 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_attachment_form_inherit_website" model="ir.ui.view">
<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 name="website_id" groups="base.group_multi_website"/>
</field>
</field>
</record>
@@ -104,7 +104,18 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="google_analytics_setting">
<div id="multi_website" 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-Websites" for="group_multi_website"/>
<div class="text-muted">
Manage multiple websites
</div>
</div>
</div>
<div class="col-12 col-lg-offset-6 col-lg-6 o_setting_box" id="google_analytics_setting">
<div class="o_setting_left_pane">
<field name="has_google_analytics"/>
</div>
@@ -184,7 +195,7 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box">
<div class="col-12 col-lg-6 o_setting_box" id="ab_testing_setting">
<div class="o_setting_left_pane">
<field name="module_website_version" widget="upgrade_boolean"/>
</div>
@@ -296,31 +307,6 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="utm_tracker_settings" title="Analyze the efficiency of your marketing campaigns by using trackable UTM trackers (campaigns, medium, sources). Create trackers and follow clicks from the Promote menu of your website. Those trackers can be used in Google Analytics or in Odoo reports where you can see the opportunities and sales revenue generated thanks to your links.">
<div class="o_setting_left_pane">
<field name="module_website_links"/>
</div>
<div class="o_setting_right_pane">
<label for="module_website_links"/>
<div class="text-muted">
Track clicks on UTM links
</div>
</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>
+1 -3
View File
@@ -45,8 +45,7 @@
<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"/>
<field name="theme_id" groups="base.group_no_one"/>
</tree>
</field>
</record>
@@ -265,7 +264,6 @@
<field name="arch" type="xml">
<field name="name" position="after">
<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', '=', [])]}" />
@@ -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,
@@ -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
@@ -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
@@ -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']
@@ -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
@@ -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',
@@ -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')
@@ -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',
}
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_theme_ir_ui_view access_theme_ir_ui_view model_theme_ir_ui_view base.group_system 1 1 1 1
3 access_theme_ir_attachment access_theme_ir_attachment model_theme_ir_attachment base.group_system 1 1 1 1
4 access_theme_website_menu access_theme_website_menu model_theme_website_menu base.group_system 1 1 1 1
5 access_theme_website_page access_theme_website_page model_theme_website_page base.group_system 1 1 1 1
@@ -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;
}
}
}
@@ -5,9 +5,11 @@
<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>
<xpath expr="//div[@id='domain_setting']/div[hasclass('o_setting_right_pane')]" position="inside">
<div>
<button name="install_theme_on_current_website" type="object" string="Change theme" class="btn-link" icon="fa-arrow-right"/>
</div>
</xpath>
</field>
</record>
</odoo>
+36 -3
View File
@@ -21,11 +21,14 @@
<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" 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 class="o_browser_bar">
<i states="installed" t-if="record.is_installed_on_current_website.raw_value" class="text-center ml4 img-circle fa fa-check-circle" role="img" aria-label="Installed" title="Installed"/>
</div>
<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 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>
<button type="object" name="button_refresh_theme" class="btn btn-primary">Update theme</button>
<hr />
<button type="object" name="button_remove_theme" class="btn btn-secondary">Remove 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>
@@ -107,4 +110,34 @@ action = {
</div>
</xpath>
</template>
<!-- Add link to theme install in default customize modal -->
<record id="website_settings_payment" model="ir.ui.view">
<field name="name">website.form</field>
<field name="model">website</field>
<field name="inherit_id" ref="website.view_website_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='other']" position='before'>
<div name="theme">
<separator string="Theme"/>
<group name="Theme">
<button type="object" name="button_view_theme" class="btn btn-primary">View themes</button>
</group>
</div>
</xpath>
</field>
</record>
<!-- ONBOARDING -->
<template id="onboarding_website_theme_step">
<t t-call="base.onboarding_step">
<t t-set="title">Website Theme</t>
<t t-set="description">Select a theme for your website.</t>
<t t-set="btn_text">Let's start!</t>
<t t-set="method" t-value="'action_open_website_theme_selector'" />
<t t-set="model" t-value="'res.company'" />
<t t-set="done" t-value="company.website_theme_onboarding_done" />
</t>
</template>
</odoo>
+7 -2
View File
@@ -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: