One of the main issue with ormcache is that the invalidation clears everything, meaning that some value, slow to compute but with a long lifetime, can be removed from the cache because an easy to invalidate value is cleared, like after writting or creating a product has an example. Most example in the code will try to invalidate the cache of the models doing something like `env['ir.qweb'].clear_caches()` but it is finally equivalent to `env.registry.clear_cache()`, and cross worker. The idea is to have multiple cache, maybe with specific sizes for a specific purpose. Having one per model is maybe a bad idea because it will be difficult to size the LRU correcly, and it is too dynamic. Checking invalidation may be expensive. The proposed solution is closed allow a limited number of named caches, using onse sequence per cache. This is actually close to the cache_longterm. We want to discourage using a specific cache for one use case in the buisness code. Adding a cache shouldn't be something easy, doable in stable. Note that we could also change the invalisation mecanism using an insert only table. We an check the sequence of this table, but also fetch all invalidation messages. Another possible improvement, especially if we have more than x cache is to have a global sequence, checking signaling would mean to check the main sequence, and only the other ones if the main one changed. Note that this poc is inspired from the long term cache but not all use case where applie yet. Part-of: odoo/odoo#119813
238 lines
8.2 KiB
Python
238 lines
8.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import base64
|
|
import re
|
|
|
|
from odoo import api, models
|
|
from odoo.tools import misc
|
|
from odoo.addons.base.models.assetsbundle import EXTENSIONS
|
|
|
|
_match_asset_file_url_regex = re.compile(r"^(/_custom/([^/]+))?/(\w+)/([/\w]+\.\w+)$")
|
|
|
|
|
|
class Assets(models.AbstractModel):
|
|
_name = 'web_editor.assets'
|
|
_description = 'Assets Utils'
|
|
|
|
@api.model
|
|
def reset_asset(self, url, bundle):
|
|
"""
|
|
Delete the potential customizations made to a given (original) asset.
|
|
|
|
Params:
|
|
url (str): the URL of the original asset (scss / js) file
|
|
|
|
bundle (str):
|
|
the name of the bundle in which the customizations to delete
|
|
were made
|
|
"""
|
|
custom_url = self._make_custom_asset_url(url, bundle)
|
|
|
|
# Simply delete the attachement which contains the modified scss/js file
|
|
# and the xpath view which links it
|
|
self._get_custom_attachment(custom_url).unlink()
|
|
self._get_custom_asset(custom_url).unlink()
|
|
|
|
@api.model
|
|
def save_asset(self, url, bundle, content, file_type):
|
|
"""
|
|
Customize the content of a given asset (scss / js).
|
|
|
|
Params:
|
|
url (src):
|
|
the URL of the original asset to customize (whether or not the
|
|
asset was already customized)
|
|
|
|
bundle (src):
|
|
the name of the bundle in which the customizations will take
|
|
effect
|
|
|
|
content (src): the new content of the asset (scss / js)
|
|
|
|
file_type (src):
|
|
either 'scss' or 'js' according to the file being customized
|
|
"""
|
|
custom_url = self._make_custom_asset_url(url, bundle)
|
|
datas = base64.b64encode((content or "\n").encode("utf-8"))
|
|
|
|
# Check if the file to save had already been modified
|
|
custom_attachment = self._get_custom_attachment(custom_url)
|
|
if custom_attachment:
|
|
# If it was already modified, simply override the corresponding
|
|
# attachment content
|
|
custom_attachment.write({"datas": datas})
|
|
self.env.registry.clear_cache('assets')
|
|
else:
|
|
# If not, create a new attachment to copy the original scss/js file
|
|
# content, with its modifications
|
|
new_attach = {
|
|
'name': url.split("/")[-1],
|
|
'type': "binary",
|
|
'mimetype': (file_type == 'js' and 'text/javascript' or 'text/scss'),
|
|
'datas': datas,
|
|
'url': custom_url,
|
|
**self._save_asset_attachment_hook(),
|
|
}
|
|
self.env["ir.attachment"].create(new_attach)
|
|
|
|
# Create an asset with the new attachment
|
|
IrAsset = self.env['ir.asset']
|
|
new_asset = {
|
|
'path': custom_url,
|
|
'target': url,
|
|
'directive': 'replace',
|
|
**self._save_asset_hook(),
|
|
}
|
|
target_asset = self._get_custom_asset(url)
|
|
if target_asset:
|
|
new_asset['name'] = target_asset.name + ' override'
|
|
new_asset['bundle'] = target_asset.bundle
|
|
new_asset['sequence'] = target_asset.sequence
|
|
else:
|
|
new_asset['name'] = '%s: replace %s' % (bundle, custom_url.split('/')[-1])
|
|
new_asset['bundle'] = IrAsset._get_related_bundle(url, bundle)
|
|
IrAsset.create(new_asset)
|
|
|
|
|
|
@api.model
|
|
def _get_content_from_url(self, url, url_info=None, custom_attachments=None):
|
|
"""
|
|
Fetch the content of an asset (scss / js) file. That content is either
|
|
the one of the related file on the disk or the one of the corresponding
|
|
custom ir.attachment record.
|
|
|
|
Params:
|
|
url (str): the URL of the asset (scss / js) file/ir.attachment
|
|
|
|
url_info (dict, optional):
|
|
the related url info (see _get_data_from_url) (allows to optimize
|
|
some code which already have the info and do not want this
|
|
function to re-get it)
|
|
|
|
custom_attachments (ir.attachment(), optional):
|
|
the related custom ir.attachment records the function might need
|
|
to search into (allows to optimize some code which already have
|
|
that info and do not want this function to re-get it)
|
|
|
|
Returns:
|
|
utf-8 encoded content of the asset (scss / js)
|
|
"""
|
|
if url_info is None:
|
|
url_info = self._get_data_from_url(url)
|
|
|
|
if url_info["customized"]:
|
|
# If the file is already customized, the content is found in the
|
|
# corresponding attachment
|
|
attachment = None
|
|
if custom_attachments is None:
|
|
attachment = self._get_custom_attachment(url)
|
|
else:
|
|
attachment = custom_attachments.filtered(lambda r: r.url == url)
|
|
return attachment and base64.b64decode(attachment.datas) or False
|
|
|
|
# If the file is not yet customized, the content is found by reading
|
|
# the local file
|
|
with misc.file_open(url.strip('/'), 'rb', filter_ext=EXTENSIONS) as f:
|
|
return f.read()
|
|
|
|
@api.model
|
|
def _get_data_from_url(self, url):
|
|
"""
|
|
Return information about an asset (scss / js) file/ir.attachment just by
|
|
looking at its URL.
|
|
|
|
Params:
|
|
url (str): the url of the asset (scss / js) file/ir.attachment
|
|
|
|
Returns:
|
|
dict:
|
|
module (str): the original asset's related app
|
|
|
|
resource_path (str):
|
|
the relative path to the original asset from the related app
|
|
|
|
customized (bool): whether the asset is a customized one or not
|
|
|
|
bundle (str):
|
|
the name of the bundle the asset customizes (False if this
|
|
is not a customized asset)
|
|
"""
|
|
m = _match_asset_file_url_regex.match(url)
|
|
if not m:
|
|
return False
|
|
return {
|
|
'module': m.group(3),
|
|
'resource_path': m.group(4),
|
|
'customized': bool(m.group(1)),
|
|
'bundle': m.group(2) or False
|
|
}
|
|
|
|
@api.model
|
|
def _make_custom_asset_url(self, url, bundle_xmlid):
|
|
"""
|
|
Return the customized version of an asset URL, that is the URL the asset
|
|
would have if it was customized.
|
|
|
|
Params:
|
|
url (str): the original asset's url
|
|
bundle_xmlid (str): the name of the bundle the asset would customize
|
|
|
|
Returns:
|
|
str: the URL the given asset would have if it was customized in the
|
|
given bundle
|
|
"""
|
|
return f"/_custom/{bundle_xmlid}{url}"
|
|
|
|
@api.model
|
|
def _get_custom_attachment(self, custom_url, op='='):
|
|
"""
|
|
Fetch the ir.attachment record related to the given customized asset.
|
|
|
|
Params:
|
|
custom_url (str): the URL of the customized asset
|
|
op (str, default: '='): the operator to use to search the records
|
|
|
|
Returns:
|
|
ir.attachment()
|
|
"""
|
|
assert op in ('in', '='), 'Invalid operator'
|
|
return self.env["ir.attachment"].search([("url", op, custom_url)])
|
|
|
|
@api.model
|
|
def _get_custom_asset(self, custom_url):
|
|
"""
|
|
Fetch the ir.asset record related to the given customized asset (the
|
|
inheriting view which replace the original asset by the customized one).
|
|
|
|
Params:
|
|
custom_url (str): the URL of the customized asset
|
|
|
|
Returns:
|
|
ir.asset()
|
|
"""
|
|
url = custom_url[1:] if custom_url.startswith(('/', '\\')) else custom_url
|
|
return self.env['ir.asset'].search([('path', 'like', url)])
|
|
|
|
@api.model
|
|
def _save_asset_attachment_hook(self):
|
|
"""
|
|
Returns the additional values to use to write the DB on customized
|
|
ir.attachment creation.
|
|
|
|
Returns:
|
|
dict
|
|
"""
|
|
return {}
|
|
|
|
@api.model
|
|
def _save_asset_hook(self):
|
|
"""
|
|
Returns the additional values to use to write the DB on customized
|
|
ir.asset creation.
|
|
|
|
Returns:
|
|
dict
|
|
"""
|
|
return {}
|