[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:
@@ -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})
|
||||
|
||||
@@ -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>
|
||||
"""
|
||||
Reference in New Issue
Block a user