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 @@ -
+
+
+ +
+
+
+
+
@@ -184,7 +195,7 @@
-
+
@@ -296,31 +307,6 @@
-
-
- -
-
-
-
- -

Multi-Website

-
-
-
- -
-
-
-
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 @@
-
- +
+ +
- + +
+
@@ -107,4 +110,34 @@ action = {
+ + + + website.form + website + + + +
+ + + + +
+
+
+
+ + + + 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: