[This other commit] introduced a method to serve Google fonts from the local server. Then it has been back-ported to previous versions with [this commit]. Unfortunately, the font was not identical when the user chose to load the font from Google servers versus from the local server. This discrepancy was due to a missing parameter when downloading the font file to serve it from the local server. This commit fixes the issue by adding the missing parameter. Steps to reproduce the issue fixed by this commit: - Drop a text block onto a website page. - Make the text bold. - Go to the theme tab. - Change the font to https://fonts.google.com/specimen/Poppins => The text style changes depending on whether you checked the "Serve font from Google servers" option or not. [This other commit]: https://github.com/odoo/odoo/commit/b06ce21eba6388ce34bbffffadcb489f0e8557dd [this commit]: https://github.com/odoo/odoo/commit/04ab4e255b7fef1608ee2c70a3a005f3064bc4f3 opw-3775683 closes odoo/odoo#157800 X-original-commit: be2d27e69d7928a46d49a7ef0a3c4b93932a36b8 Signed-off-by: Romain Derie (rde) <rde@odoo.com> Signed-off-by: Guillaume Dieleman (gdi) <gdi@odoo.com>
196 lines
9.2 KiB
Python
196 lines
9.2 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',
|
|
'menu-secondary-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}:300,300i,400,400i,700,700i&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():
|
|
# Protect variable names so they cannot be computed as numbers
|
|
# on SCSS compilation (e.g. var(--700) => var(700)).
|
|
if isinstance(value, str):
|
|
value = re.sub(
|
|
r"var\(--([0-9]+)\)",
|
|
lambda matchobj: "var(--#{" + matchobj.group(1) + "})",
|
|
value)
|
|
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())
|