[IMP] website: disable assets for unused snippets

Based on their path, we retrieve snippets assets, assuming that it will
be in the form : /snippets/{snippet_id}/{version}.{scss|js}

Then we check the template definition uses that version, or if any views
or html fields contains a snippet with such version exist.
If not, the asset is deactivated.

For that, we are looking for the data-snippet attribute in snippets
already dropped. For the template definition, we look at the first class
definition if a data version attribute is also defined.

task-2212216

closes odoo/odoo#73005

Signed-off-by: Romain Derie <rdeodoo@users.noreply.github.com>
This commit is contained in:
Younn Olivier
2021-09-02 14:14:56 +00:00
parent 7a96557b83
commit fa1c05e7bb
3 changed files with 228 additions and 0 deletions
+95
View File
@@ -1342,3 +1342,98 @@ class Website(models.Model):
def _get_cached(self, field):
return self._get_cached_values()[field]
def _get_html_fields(self):
html_fields = [('ir_ui_view', 'arch_db')]
cr = self.env.cr
cr.execute(r"""
SELECT f.model,
f.name
FROM ir_model_fields f
JOIN ir_model m
ON m.id = f.model_id
WHERE f.ttype = 'html'
AND f.store = true
AND m.transient = false
AND f.model NOT LIKE 'ir.actions%'
AND f.model != 'mail.message'
""")
for model, name in cr.fetchall():
table = self.env[model]._table
if tools.table_exists(cr, table) and tools.column_exists(cr, table, name):
html_fields.append((table, name))
return html_fields
def _get_snippets_assets(self):
"""Returns every parent snippet asset from the database, filtering out
their potential overrides defined in other modules. As they share the same
snippet_id, asset_version and asset_type, it is possible to do that using
Postgres' DISTINCT ON and ordering by asset_id, as overriden assets will be
created later than their parents.
The assets are returned in the form of a list of tuples :
[(snippet_module, snippet_id, asset_version, asset_type, asset_id)]
"""
self.env.cr.execute(r"""
SELECT DISTINCT ON (snippet_id, asset_version, asset_type)
regexp_matches[1] AS snippet_module,
regexp_matches[2] AS snippet_id,
regexp_matches[3] AS asset_version,
CASE
WHEN regexp_matches[4]='scss' THEN 'css'
ELSE regexp_matches[4]
END AS asset_type,
id AS asset_id
FROM (
SELECT REGEXP_MATCHES(PATH, '(\w*)\/.*\/snippets\/(\w*)\/(\d{3})\.(js|scss)'),
id
FROM ir_asset
) AS regexp
ORDER BY snippet_id, asset_version, asset_type, asset_id;
""")
return self.env.cr.fetchall()
def _is_snippet_used(self, snippet_module, snippet_id, asset_version, asset_type, html_fields):
snippet_occurences = []
# Check snippet template definition to avoid disabling its related assets.
# This special case is needed because snippet template definitions do not
# have a `data-snippet` attribute (which is added during drag&drop).
snippet_template = self.env.ref(f'{snippet_module}.{snippet_id}', raise_if_not_found=False)
if snippet_template:
snippet_template_html = snippet_template._render()
match = re.search('<([^>]*class="[^>]*)>', snippet_template_html)
snippet_occurences.append(match.group())
# As well as every snippet dropped in html fields
snippet_regex = f'<([^>]*data-snippet="{snippet_id}"[^>]*)>'
snippet_dropped = 'UNION '.join(f'SELECT REGEXP_MATCHES({column}, \'{snippet_regex}\') FROM {table} ' for table, column in html_fields)
self.env.cr.execute(snippet_dropped)
results = self.env.cr.fetchall()
for r in results:
snippet_occurences.append(r[0][0])
for snippet in snippet_occurences:
if asset_version == '000':
if f'data-v{asset_type}' not in snippet:
return True
else:
if f'data-v{asset_type}="{asset_version}"' in snippet:
return True
return False
@api.autovacuum
def _disable_unused_snippets_assets(self):
snippets_assets = self._get_snippets_assets()
html_fields = self._get_html_fields()
for snippet_module, snippet_id, asset_version, asset_type, _ in snippets_assets:
is_snippet_used = self._is_snippet_used(snippet_module, snippet_id, asset_version, asset_type, html_fields)
filename_type = 'scss' if asset_type == 'css' else asset_type
assets_path = f'{snippet_id}/{asset_version}.{filename_type}'
# The query will also set to active or inactive assets overrides, as they
# share the same snippet_id, asset_version and filename_type as their parents
self.env.cr.execute("""
UPDATE ir_asset
SET active = %(active)s
WHERE path ~ %(assets_path)s
""", {"active": is_snippet_used, "assets_path": assets_path})
+1
View File
@@ -5,6 +5,7 @@ from . import test_base_url
from . import test_controllers
from . import test_converter
from . import test_crawl
from . import test_disable_unused_snippets_assets
from . import test_get_current_website
from . import test_lang_url
from . import test_menu
@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestDisableSnippetsAssets(TransactionCase):
def setUp(self):
super().setUp()
self.View = self.env['ir.ui.view']
self.WebsiteMenu = self.env['website.menu']
self.Website = self.env['website']
self.IrAsset = self.env['ir.asset']
self.homepage = self.View.create({
'name': 'Home',
'type': 'qweb',
'arch_db': HOMEPAGE_WITH_OUTDATED_S_WEBSITE_FORM,
'key': 'website.homepage',
})
self.mega_menu = self.WebsiteMenu.create({
'name': 'Image Gallery V001',
'mega_menu_content': MEGA_MENU_CONTENT_IMAGE_GALLERY_V001,
})
self.initial_active_snippets_assets = self._get_active_snippets_assets()
def test_homepage_with_outdated_s_website_form(self):
self.Website._disable_unused_snippets_assets()
s_website_form_000 = self._get_snippet_asset('s_website_form', '000', 'scss')
s_website_form_001 = self._get_snippet_asset('s_website_form', '001', 'scss')
s_image_gallery_000 = self._get_snippet_asset('s_image_gallery', '000', 'scss')
s_image_gallery_001 = self._get_snippet_asset('s_image_gallery', '001', 'scss')
self.assertEqual(s_website_form_000.active, True)
self.assertEqual(s_website_form_001.active, True)
self.assertEqual(s_image_gallery_000.active, False)
self.assertEqual(s_image_gallery_001.active, True)
unwanted_snippets_assets_changes = set(self.initial_active_snippets_assets) - set(self._get_active_snippets_assets()) - set([s_image_gallery_000.path])
# The vaccuum should not have activated/deactivated any other snippet asset than the original ones
self.assertEqual(
len(unwanted_snippets_assets_changes),
0,
'Following snippets are not following the snippet versioning system structure, or their previous assets have not been deactivated:\n'
+ '\n'.join(unwanted_snippets_assets_changes))
def test_homepage_with_s_website_form_V001(self):
self.homepage.write({
'arch_db': HOMEPAGE_WITH_S_WEBSITE_FORM_V001,
})
self.mega_menu.write({
'mega_menu_content': MEGA_MENU_CONTENT_IMAGE_GALLERY_OUTDATED,
})
self.Website._disable_unused_snippets_assets()
s_website_form_000 = self._get_snippet_asset('s_website_form', '000', 'scss')
s_website_form_001 = self._get_snippet_asset('s_website_form', '001', 'scss')
s_image_gallery_000 = self._get_snippet_asset('s_image_gallery', '000', 'scss')
s_image_gallery_001 = self._get_snippet_asset('s_image_gallery', '001', 'scss')
self.assertEqual(s_website_form_000.active, False)
self.assertEqual(s_website_form_001.active, True)
self.assertEqual(s_image_gallery_000.active, True)
self.assertEqual(s_image_gallery_001.active, True)
def _get_snippet_asset(self, snippet_id, asset_version, asset_type):
return self.IrAsset.search([('path', '=', 'website/static/src/snippets/' + snippet_id + '/' + asset_version + '.' + asset_type)], limit=1)
def _get_active_snippets_assets(self):
return self.IrAsset.search([('path', 'like', 'snippets'), ('active', '=', True)]).mapped('path')
HOMEPAGE_WITH_S_WEBSITE_FORM_V001 = """
<t name="Homepage" t-name="website.homepage1">
<t t-call="website.layout">
<t t-set="pageName" t-value="'homepage'"/>
<div id="wrap" class="oe_structure oe_empty">
<section class="s_website_form pt16 pb16 o_colored_level" data-vcss="001" data-snippet="s_website_form" data-name="Form">
<div class="container">
<form action="/website_form/" method="post" enctype="multipart/form-data" class="o_mark_required" data-mark="*" data-success-mode="redirect" data-success-page="/contactus-thank-you" data-model_name="mail.mail">
</form>
</div>
</section>
<section class="s_showcase pt48 pb48 o_colored_level" data-vcss="002" data-snippet="s_showcase" data-name="Showcase">
<div class="container">
</div>
</section>
</div>
</t>
</t>
"""
HOMEPAGE_WITH_OUTDATED_S_WEBSITE_FORM = """
<t name="Homepage" t-name="website.homepage1">
<t t-call="website.layout">
<t t-set="pageName" t-value="'homepage'"/>
<div id="wrap" class="oe_structure oe_empty">
<form action="/website_form/" method="post" class="s_website_form container-fluid mt32 o_fake_not_editable" enctype="multipart/form-data" data-name="Form Builder" data-model_name="mail.mail" data-success_page="/contactus-thank-you" data-snippet="s_website_form">
<div class="container">
</div>
</form>
<section class="s_showcase pt48 pb48 o_colored_level" data-vcss="002" data-snippet="s_showcase" data-name="Showcase">
<div class="container">
</div>
</section>
</div>
</t>
</t>
"""
MEGA_MENU_CONTENT_IMAGE_GALLERY_V001 = """
<section class="s_mega_menu_multi_menus py-4 o_colored_level" data-name="Multi-Menus">
<div class="container">
</div>
</section>
<section class="s_image_gallery o_slideshow s_image_gallery_show_indicators s_image_gallery_indicators_rounded pt24 o_colored_level" data-vcss="001" data-columns="3" style="height: 500px; overflow: hidden;" data-snippet="s_image_gallery" data-name="Image Gallery">
<div class="container">
</div>
</section>
"""
MEGA_MENU_CONTENT_IMAGE_GALLERY_OUTDATED = """
<section class="s_mega_menu_multi_menus py-4 o_colored_level" data-name="Multi-Menus">
<div class="container">
</div>
</section>
<section class="s_image_gallery o_slideshow s_image_gallery_show_indicators s_image_gallery_indicators_rounded pt24 o_colored_level" data-columns="3" style="height: 500px; overflow: hidden;" data-snippet="s_image_gallery" data-name="Image Gallery">
<div class="container">
</div>
</section>
"""