diff options
| author | Jeremy Kersten <jke@odoo.com> | 2018-08-12 23:17:24 +0200 |
|---|---|---|
| committer | Jeremy Kersten <jke@odoo.com> | 2018-08-13 20:16:34 +0200 |
| commit | c0968e68eda55df624fb94016d036a2af598b10c (patch) | |
| tree | 2bc27d7931036dc85bcbdd693db61a5803b44670 | |
| parent | 066cfc9598a307a48b86104f4bf861a58de09b41 (diff) | |
[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>
22 files changed, 430 insertions, 345 deletions
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 @@ <?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> 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 @@ </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> 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 @@ <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', '=', [])]}" /> 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}) - - 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}) + 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)) + + 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): + def _unload_one_theme_module(self, website): + _logger.info('Unload theme %s for website %s from template.' % (self.mapped('name'), website.id)) + for module in self: - menus = module._get_module_data('website.menu') - menus.filtered(lambda menu: menu.website_id == website).unlink() + 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() - pages = module._get_module_data('website.page') - pages.filtered(lambda page: page.website_id == website).unlink() + # 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_button_area { - opacity: 1; - visibility: visible; - transition: opacity 0.2s ease 0.1s; - } + .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; } } } 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 @@ <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> 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 @@ <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> 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: |
