Files
odoo_source/addons/website/models/assets.py
T
qsm-odoo b4fdc6c01d [FIX] website: allow to reinstall website after deleted user-websites
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>
2023-01-20 20:58:34 +01:00

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())