# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging import threading from odoo import api, fields, models from odoo.tools.translate import xml_translate from odoo.modules.module import get_resource_from_path from odoo.addons.base.models.ir_asset import AFTER_DIRECTIVE, APPEND_DIRECTIVE, BEFORE_DIRECTIVE, DEFAULT_SEQUENCE, INCLUDE_DIRECTIVE, PREPEND_DIRECTIVE, REMOVE_DIRECTIVE, REPLACE_DIRECTIVE _logger = logging.getLogger(__name__) class ThemeAsset(models.Model): _name = 'theme.ir.asset' _description = 'Theme Asset' key = fields.Char() name = fields.Char(required=True) bundle = fields.Char(required=True) directive = fields.Selection(selection=[ (APPEND_DIRECTIVE, 'Append'), (PREPEND_DIRECTIVE, 'Prepend'), (AFTER_DIRECTIVE, 'After'), (BEFORE_DIRECTIVE, 'Before'), (REMOVE_DIRECTIVE, 'Remove'), (REPLACE_DIRECTIVE, 'Replace'), (INCLUDE_DIRECTIVE, 'Include')], default=APPEND_DIRECTIVE) path = fields.Char(required=True) target = fields.Char() active = fields.Boolean(default=True) sequence = fields.Integer(default=DEFAULT_SEQUENCE, required=True) copy_ids = fields.One2many('ir.asset', 'theme_template_id', 'Assets using a copy of me', copy=False, readonly=True) def _convert_to_base_model(self, website, **kwargs): self.ensure_one() new_asset = { 'name': self.name, 'key': self.key, 'bundle': self.bundle, 'directive': self.directive, 'path': self.path, 'target': self.target, 'active': self.active, 'sequence': self.sequence, 'website_id': website.id, 'theme_template_id': self.id, } return new_asset class ThemeView(models.Model): _name = 'theme.ir.ui.view' _description = 'Theme UI View' def compute_arch_fs(self): if 'install_filename' not in self._context: return '' 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=DEFAULT_SEQUENCE, required=True) mode = fields.Selection([('primary', "Base view"), ('extension', "Extension View")]) active = fields.Boolean(default=True) arch = fields.Text(translate=xml_translate) 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) customize_show = fields.Boolean() def _convert_to_base_model(self, website, **kwargs): self.ensure_one() inherit = self.inherit_id if self.inherit_id and self.inherit_id._name == 'theme.ir.ui.view': inherit = self.inherit_id.with_context(active_test=False).copy_ids.filtered(lambda x: x.website_id == website) if not inherit: # inherit_id not yet created, add to the queue return False if inherit and inherit.website_id != website: website_specific_inherit = self.env['ir.ui.view'].with_context(active_test=False).search([ ('key', '=', inherit.key), ('website_id', '=', website.id) ], limit=1) if website_specific_inherit: inherit = website_specific_inherit new_view = { 'type': self.type or 'qweb', 'name': self.name, 'arch': self.arch, 'key': self.key, 'inherit_id': inherit and inherit.id, 'arch_fs': self.arch_fs, 'priority': self.priority, 'active': self.active, 'theme_template_id': self.id, 'website_id': website.id, 'customize_show': self.customize_show, } if self.mode: # if not provided, it will be computed automatically (if inherit_id or not) new_view['mode'] = self.mode return new_view class ThemeAttachment(models.Model): _name = 'theme.ir.attachment' _description = 'Theme Attachments' 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) def _convert_to_base_model(self, website, **kwargs): self.ensure_one() new_attach = { 'key': self.key, 'public': True, 'res_model': 'ir.ui.view', 'type': 'url', 'name': self.name, 'url': self.url, 'website_id': website.id, 'theme_template_id': self.id, } return new_attach class ThemeMenu(models.Model): _name = 'theme.website.menu' _description = 'Website Theme Menu' name = fields.Char(required=True, translate=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") mega_menu_content = fields.Html() mega_menu_classes = fields.Char() use_main_menu_as_parent = fields.Boolean(default=True) copy_ids = fields.One2many('website.menu', 'theme_template_id', 'Menu using a copy of me', copy=False, readonly=True) def _convert_to_base_model(self, website, **kwargs): self.ensure_one() page_id = self.page_id.copy_ids.filtered(lambda x: x.website_id == website) parent_id = False if self.parent_id: parent_id = self.parent_id.copy_ids.filtered(lambda x: x.website_id == website) elif self.use_main_menu_as_parent: parent_id = website.menu_id new_menu = { 'name': self.name, 'url': self.url, 'page_id': page_id and page_id.id or False, 'new_window': self.new_window, 'sequence': self.sequence, 'parent_id': parent_id and parent_id.id or False, 'website_id': website.id, 'mega_menu_content': self.mega_menu_content, 'mega_menu_classes': self.mega_menu_classes, 'theme_template_id': self.id, } return new_menu class ThemePage(models.Model): _name = 'theme.website.page' _description = 'Website Theme Page' url = fields.Char() view_id = fields.Many2one('theme.ir.ui.view', required=True, ondelete="cascade") website_indexed = fields.Boolean('Page Indexed', default=True) is_published = fields.Boolean() # Page options header_overlay = fields.Boolean() header_color = fields.Char() header_visible = fields.Boolean(default=True) footer_visible = fields.Boolean(default=True) copy_ids = fields.One2many('website.page', 'theme_template_id', 'Page using a copy of me', copy=False, readonly=True) def _convert_to_base_model(self, website, **kwargs): self.ensure_one() view_id = self.view_id.copy_ids.filtered(lambda x: x.website_id == website) if not view_id: # inherit_id not yet created, add to the queue return False new_page = { 'url': self.url, 'view_id': view_id.id, 'website_indexed': self.website_indexed, 'is_published': self.is_published, 'header_overlay': self.header_overlay, 'header_color': self.header_color, 'header_visible': self.header_visible, 'footer_visible': self.footer_visible, 'theme_template_id': self.id, } return new_page class Theme(models.AbstractModel): _name = 'theme.utils' _description = 'Theme Utils' _auto = False _header_templates = [ 'website.template_header_hamburger', 'website.template_header_vertical', 'website.template_header_sidebar', 'website.template_header_boxed', 'website.template_header_stretch', 'website.template_header_search', 'website.template_header_sales_one', 'website.template_header_sales_two', 'website.template_header_sales_three', 'website.template_header_sales_four', # Default one, keep it last 'website.template_header_default', ] _footer_templates = [ 'website.template_footer_descriptive', 'website.template_footer_centered', 'website.template_footer_links', 'website.template_footer_minimalist', 'website.template_footer_contact', 'website.template_footer_call_to_action', 'website.template_footer_headline', # Default one, keep it last 'website.footer_custom', ] def _post_copy(self, mod): # Call specific theme post copy 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(mod) return False @api.model def _reset_default_config(self): # Reinitialize some css customizations self.env['web_editor.assets'].make_scss_customization( '/website/static/src/scss/options/user_values.scss', { 'font': 'null', 'headings-font': 'null', 'navbar-font': 'null', 'buttons-font': 'null', 'color-palettes-number': 'null', 'color-palettes-name': 'null', 'btn-ripple': 'null', 'header-template': 'null', 'footer-template': 'null', 'footer-scrolltop': 'null', } ) # Reinitialize effets self.disable_asset("website.ripple_effect_scss") self.disable_asset("website.ripple_effect_js") # Reinitialize header templates for view in self._header_templates[:-1]: self.disable_view(view) self.enable_view(self._header_templates[-1]) # Reinitialize footer templates for view in self._footer_templates[:-1]: self.disable_view(view) self.enable_view(self._footer_templates[-1]) # Reinitialize footer scrolltop template self.disable_view('website.option_footer_scrolltop') @api.model def _toggle_asset(self, key, active): ThemeAsset = self.env['theme.ir.asset'].sudo().with_context(active_test=False) obj = ThemeAsset.search([('key', '=', key)]) website = self.env['website'].get_current_website() if obj: obj = obj.copy_ids.filtered(lambda x: x.website_id == website) else: Asset = self.env['ir.asset'].sudo().with_context(active_test=False) obj = Asset.search([('key', '=', key)], limit=1) has_specific = obj.key and Asset.search_count([ ('key', '=', obj.key), ('website_id', '=', website.id) ]) >= 1 if not has_specific and active == obj.active: return obj.write({'active': active}) @api.model def _toggle_view(self, xml_id, active): obj = self.env.ref(xml_id) website = self.env['website'].get_current_website() if obj._name == 'theme.ir.ui.view': obj = obj.with_context(active_test=False) obj = obj.copy_ids.filtered(lambda x: x.website_id == website) else: # If a theme post copy wants to enable/disable a view, this is to # enable/disable a given functionality which is disabled/enabled # by default. So if a post copy asks to enable/disable a view which # is already enabled/disabled, we would not consider it otherwise it # would COW the view for nothing. View = self.env['ir.ui.view'].with_context(active_test=False) has_specific = obj.key and View.search_count([ ('key', '=', obj.key), ('website_id', '=', website.id) ]) >= 1 if not has_specific and active == obj.active: return obj.write({'active': active}) @api.model def enable_asset(self, name): self._toggle_asset(name, True) @api.model def disable_asset(self, name): self._toggle_asset(name, False) @api.model def enable_view(self, xml_id): if xml_id in self._header_templates: for view in self._header_templates: self.disable_view(view) elif xml_id in self._footer_templates: for view in self._footer_templates: self.disable_view(view) self._toggle_view(xml_id, True) @api.model def disable_view(self, xml_id): self._toggle_view(xml_id, False) class IrUiView(models.Model): _inherit = 'ir.ui.view' theme_template_id = fields.Many2one('theme.ir.ui.view', copy=False) def write(self, vals): # During a theme module update, theme views' copies receiving an arch # update should not be considered as `arch_updated`, as this is not a # user made change. test_mode = getattr(threading.current_thread(), 'testing', False) if not (test_mode or self.pool._init): return super().write(vals) no_arch_updated_views = other_views = self.env['ir.ui.view'] for record in self: # Do not mark the view as user updated if original view arch is similar arch = vals.get('arch', vals.get('arch_base')) if record.theme_template_id and record.theme_template_id.arch == arch: no_arch_updated_views += record else: other_views += record res = super(IrUiView, other_views).write(vals) if no_arch_updated_views: vals['arch_updated'] = False res &= super(IrUiView, no_arch_updated_views).write(vals) return res class IrAsset(models.Model): _inherit = 'ir.asset' theme_template_id = fields.Many2one('theme.ir.asset', copy=False) class IrAttachment(models.Model): _inherit = 'ir.attachment' key = fields.Char(copy=False) theme_template_id = fields.Many2one('theme.ir.attachment', copy=False) class WebsiteMenu(models.Model): _inherit = 'website.menu' theme_template_id = fields.Many2one('theme.website.menu', copy=False) class WebsitePage(models.Model): _inherit = 'website.page' theme_template_id = fields.Many2one('theme.website.page', copy=False)