Before this commit, this flow was broken: - Install website - Create a new website of your own (not using the one created automatically from XML data) - Choose another color palette for that website - Uninstall the website app - Reinstall the website app - Try to choose another color palette for any website => It does not work Indeed, after the uninstallation, the DB is left in an invalid state: the SCSS customizations attachments of the website that was created by the user are not removed, they just have their website_id field emptied. Some code made at [1] was already there to remove those attachments. The problem is that it only worked for websites which were created by XML data (at website installation), not by the user. Indeed, the `unlink` method is not called during uninstallation to remove records that were created by the user, thus the `unlink` override was not called either. See [2] for some details. This fixes the issues by moving this attachment cleaning code in a dedicated method, called in `unlink` but also in the `uninstall_hook` of the website app. This also takes the opportunity to refactor the code involved, in particular to not even consider customized attachments which do not have a website_id. [1]: https://github.com/odoo/odoo/commit/2f361bec36dff09181b96d140d62c477cdf013a1 [2]: https://github.com/odoo/odoo/pull/97852#pullrequestreview-1067851656 opw-3127531 closes odoo/odoo#110338 X-original-commit: 988eafa03b57be3b3a7f110650c61ce8fda88e31 Signed-off-by: Romain Derie (rde) <rde@odoo.com>
189 lines
8.8 KiB
Python
189 lines
8.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import base64
|
|
import re
|
|
import requests
|
|
|
|
from werkzeug.urls import url_parse
|
|
|
|
from odoo import api, models
|
|
|
|
|
|
class Assets(models.AbstractModel):
|
|
_inherit = 'web_editor.assets'
|
|
|
|
@api.model
|
|
def make_scss_customization(self, url, values):
|
|
"""
|
|
Makes a scss customization of the given file. That file must
|
|
contain a scss map including a line comment containing the word 'hook',
|
|
to indicate the location where to write the new key,value pairs.
|
|
|
|
Params:
|
|
url (str):
|
|
the URL of the scss file to customize (supposed to be a variable
|
|
file which will appear in the assets_frontend bundle)
|
|
|
|
values (dict):
|
|
key,value mapping to integrate in the file's map (containing the
|
|
word hook). If a key is already in the file's map, its value is
|
|
overridden.
|
|
"""
|
|
IrAttachment = self.env['ir.attachment']
|
|
if 'color-palettes-name' in values:
|
|
self.reset_asset('/website/static/src/scss/options/colors/user_color_palette.scss', 'web.assets_frontend')
|
|
self.reset_asset('/website/static/src/scss/options/colors/user_gray_color_palette.scss', 'web.assets_frontend')
|
|
# Do not reset all theme colors for compatibility (not removing alpha -> epsilon colors)
|
|
self.make_scss_customization('/website/static/src/scss/options/colors/user_theme_color_palette.scss', {
|
|
'success': 'null',
|
|
'info': 'null',
|
|
'warning': 'null',
|
|
'danger': 'null',
|
|
})
|
|
# Also reset gradients which are in the "website" values palette
|
|
self.make_scss_customization('/website/static/src/scss/options/user_values.scss', {
|
|
'menu-gradient': 'null',
|
|
'header-boxed-gradient': 'null',
|
|
'footer-gradient': 'null',
|
|
'copyright-gradient': 'null',
|
|
})
|
|
|
|
delete_attachment_id = values.pop('delete-font-attachment-id', None)
|
|
if delete_attachment_id:
|
|
delete_attachment_id = int(delete_attachment_id)
|
|
IrAttachment.search([
|
|
'|', ('id', '=', delete_attachment_id),
|
|
('original_id', '=', delete_attachment_id),
|
|
('name', 'like', '%google-font%')
|
|
]).unlink()
|
|
|
|
google_local_fonts = values.get('google-local-fonts')
|
|
if google_local_fonts and google_local_fonts != 'null':
|
|
# "('font_x': 45, 'font_y': '')" -> {'font_x': '45', 'font_y': ''}
|
|
google_local_fonts = dict(re.findall(r"'([^']+)': '?(\d*)", google_local_fonts))
|
|
# Google is serving different font format (woff, woff2, ttf, eot..)
|
|
# based on the user agent. We need to get the woff2 as this is
|
|
# supported by all the browers we support.
|
|
headers_woff2 = {
|
|
'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36',
|
|
}
|
|
for font_name in google_local_fonts:
|
|
if google_local_fonts[font_name]:
|
|
google_local_fonts[font_name] = int(google_local_fonts[font_name])
|
|
else:
|
|
font_family_attachments = IrAttachment
|
|
font_content = requests.get(
|
|
f'https://fonts.googleapis.com/css?family={font_name}&display=swap',
|
|
timeout=5, headers=headers_woff2,
|
|
).content.decode()
|
|
|
|
def fetch_google_font(src):
|
|
statement = src.group()
|
|
url, font_format = re.match(r'src: url\(([^\)]+)\) (.+)', statement).groups()
|
|
req = requests.get(url, timeout=5, headers=headers_woff2)
|
|
# https://fonts.gstatic.com/s/modak/v18/EJRYQgs1XtIEskMB-hRp7w.woff2
|
|
# -> s-modak-v18-EJRYQgs1XtIEskMB-hRp7w.woff2
|
|
name = url_parse(url).path.lstrip('/').replace('/', '-')
|
|
attachment = IrAttachment.create({
|
|
'name': f'google-font-{name}',
|
|
'type': 'binary',
|
|
'datas': base64.b64encode(req.content),
|
|
'public': True,
|
|
})
|
|
nonlocal font_family_attachments
|
|
font_family_attachments += attachment
|
|
return 'src: url(/web/content/%s/%s) %s' % (
|
|
attachment.id,
|
|
name,
|
|
font_format,
|
|
)
|
|
|
|
font_content = re.sub(r'src: url\(.+\)', fetch_google_font, font_content)
|
|
|
|
attach_font = IrAttachment.create({
|
|
'name': f'{font_name} (google-font)',
|
|
'type': 'binary',
|
|
'datas': base64.encodebytes(font_content.encode()),
|
|
'mimetype': 'text/css',
|
|
'public': True,
|
|
})
|
|
google_local_fonts[font_name] = attach_font.id
|
|
# That field is meant to keep track of the original
|
|
# image attachment when an image is being modified (by the
|
|
# website builder for instance). It makes sense to use it
|
|
# here to link font family attachment to the main font
|
|
# attachment. It will ease the unlink later.
|
|
font_family_attachments.original_id = attach_font.id
|
|
|
|
# {'font_x': 45, 'font_y': 55} -> "('font_x': 45, 'font_y': 55)"
|
|
values['google-local-fonts'] = str(google_local_fonts).replace('{', '(').replace('}', ')')
|
|
|
|
custom_url = self._make_custom_asset_url(url, 'web.assets_frontend')
|
|
updatedFileContent = self._get_content_from_url(custom_url) or self._get_content_from_url(url)
|
|
updatedFileContent = updatedFileContent.decode('utf-8')
|
|
for name, value in values.items():
|
|
pattern = "'%s': %%s,\n" % name
|
|
regex = re.compile(pattern % ".+")
|
|
replacement = pattern % value
|
|
if regex.search(updatedFileContent):
|
|
updatedFileContent = re.sub(regex, replacement, updatedFileContent)
|
|
else:
|
|
updatedFileContent = re.sub(r'( *)(.*hook.*)', r'\1%s\1\2' % replacement, updatedFileContent)
|
|
|
|
self.save_asset(url, 'web.assets_frontend', updatedFileContent, 'scss')
|
|
|
|
@api.model
|
|
def _get_custom_attachment(self, custom_url, op='='):
|
|
"""
|
|
See web_editor.Assets._get_custom_attachment
|
|
Extend to only return the attachments related to the current website.
|
|
"""
|
|
if self.env.user.has_group('website.group_website_designer'):
|
|
self = self.sudo()
|
|
website = self.env['website'].get_current_website()
|
|
res = super()._get_custom_attachment(custom_url, op=op)
|
|
# See _save_asset_attachment_hook -> it is guaranteed that the
|
|
# attachment we are looking for has a website_id. When we serve an
|
|
# attachment we normally serve the ones which have the right website_id
|
|
# or no website_id at all (which means "available to all websites", of
|
|
# course if they are marked "public"). But this does not apply in this
|
|
# case of customized asset files.
|
|
return res.with_context(website_id=website.id).filtered(lambda x: x.website_id == website)
|
|
|
|
@api.model
|
|
def _get_custom_asset(self, custom_url):
|
|
"""
|
|
See web_editor.Assets._get_custom_asset
|
|
Extend to only return the views related to the current website.
|
|
"""
|
|
if self.env.user.has_group('website.group_website_designer'):
|
|
# TODO: Remove me in master, see commit message, ACL added right to
|
|
# unlink to designer but not working without -u in stable
|
|
self = self.sudo()
|
|
website = self.env['website'].get_current_website()
|
|
res = super()._get_custom_asset(custom_url)
|
|
return res.with_context(website_id=website.id).filter_duplicate()
|
|
|
|
@api.model
|
|
def _add_website_id(self, values):
|
|
website = self.env['website'].get_current_website()
|
|
values['website_id'] = website.id
|
|
return values
|
|
|
|
@api.model
|
|
def _save_asset_attachment_hook(self):
|
|
"""
|
|
See web_editor.Assets._save_asset_attachment_hook
|
|
Extend to add website ID at ir.attachment creation.
|
|
"""
|
|
return self._add_website_id(super()._save_asset_attachment_hook())
|
|
|
|
@api.model
|
|
def _save_asset_hook(self):
|
|
"""
|
|
See web_editor.Assets._save_asset_hook
|
|
Extend to add website ID at ir.asset creation.
|
|
"""
|
|
return self._add_website_id(super()._save_asset_hook())
|