[IMP] base/web: XML templates are added into the asset bundles.
XML files are now declared in python module manifests. During the qweb 't-call-asset' directive, assetbundle will fetch the declared xml files, apply the inheritance (t-inherit) and create a javascript service (for eg: 'web.assets_backend.bundle.xml') which is added at the end of the *.js mimifier file. When the debug mode is activated, comments are added in the template indicating which file the template comes from as well as the inheritances applied to it. **** JavaScript: assets.js (module @web/core/assets) takes care of loading libraries, javascripts and styles. `loadJS(url)` (loads the javascript and returns a resolved promise when the templates are also loaded via the '*.bundle.xml' service) `loadCSS(url)` (loads the style a resolved promise when the file is loaded) `loadXML(xml, app=assets.defaultApp)` (load template into application/owl, used by the `*.bundle.xml` services) `getBundle(bundleName)` (get the bundle descriptor) `loadBundle(desc)` (load the files and bundle from a descriptor) templates (XML element content all owl templates) A new `ready(serviceName)` method on boot.js lets you know when a service is loaded are the require. The xmlDependencies attribute no longer exists. Python: The xmls taken into account by assetbundle.py, applying `t-inherit` inheritances and adding an `name_of_the_bundle.bundle.xml` service in the generated JavaScript file. **** Every manifest changes is into the next commit, except 'web_tour' in this current commit as example. Part-of: odoo/odoo#95500
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
import odoo
|
||||
from odoo import models
|
||||
from odoo.http import request
|
||||
from odoo.addons.web.controllers.utils import HomeStaticTemplateHelpers
|
||||
|
||||
|
||||
class IrHttp(models.AbstractModel):
|
||||
@@ -14,8 +13,6 @@ class IrHttp(models.AbstractModel):
|
||||
result = super(IrHttp, self).session_info()
|
||||
if self.env.user._is_internal():
|
||||
result['notification_type'] = user.notification_type
|
||||
assets_discuss_public_hash = HomeStaticTemplateHelpers.get_qweb_templates_checksum(debug=request.session.debug, bundle='mail.assets_discuss_public')
|
||||
result['cache_hashes']['assets_discuss_public'] = assets_discuss_public_hash
|
||||
guest = self.env.context.get('guest')
|
||||
if not request.session.uid and guest:
|
||||
user_context = {'lang': guest.lang}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { DiscussPublicViewContainer } from '@mail/components/discuss_public_view
|
||||
import { PopoverManagerContainer } from '@mail/components/popover_manager_container/popover_manager_container';
|
||||
import { messagingService } from '@mail/services/messaging_service';
|
||||
|
||||
import { processTemplates } from '@web/core/assets';
|
||||
import { MainComponentsContainer } from '@web/core/main_components_container';
|
||||
import { registry } from '@web/core/registry';
|
||||
import { makeEnv, startServices } from '@web/env';
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
mapLegacyEnvToWowlEnv,
|
||||
} from '@web/legacy/utils';
|
||||
import { session } from '@web/session';
|
||||
import { templates } from '@web/core/assets';
|
||||
|
||||
import * as AbstractService from 'web.AbstractService';
|
||||
import * as legacyEnv from 'web.env';
|
||||
@@ -69,10 +69,7 @@ Component.env = legacyEnv;
|
||||
isReady: false,
|
||||
});
|
||||
const env = makeEnv();
|
||||
const [, templates] = await Promise.all([
|
||||
startServices(env),
|
||||
odoo.loadTemplatesPromise.then(processTemplates),
|
||||
]);
|
||||
await startServices(env);
|
||||
mapLegacyEnvToWowlEnv(Component.env, env);
|
||||
odoo.isReady = true;
|
||||
await mount(MainComponentsContainer, document.body, { env, templates, dev: env.debug });
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
csrf_token: "<t t-out="request.csrf_token(None)"/>",
|
||||
debug: "<t t-out="debug"/>",
|
||||
};
|
||||
odoo.loadTemplatesPromise = fetch(`/web/webclient/qweb/${odoo.__session_info__.cache_hashes.assets_discuss_public}?bundle=mail.assets_discuss_public`).then(doc => doc.text());
|
||||
{
|
||||
const { user_context, cache_hashes } = odoo.__session_info__;
|
||||
fetch(`/web/webclient/translations/${cache_hashes.translations}?lang=${user_context.lang}`);
|
||||
|
||||
@@ -9,7 +9,6 @@ from odoo.http import request
|
||||
from odoo.exceptions import AccessError, MissingError
|
||||
from odoo.addons.portal.controllers import portal
|
||||
from odoo.addons.portal.controllers.portal import pager as portal_pager
|
||||
from odoo.addons.web.controllers.utils import HomeStaticTemplateHelpers
|
||||
|
||||
|
||||
class CustomerPortal(portal.CustomerPortal):
|
||||
@@ -91,11 +90,9 @@ class CustomerPortal(portal.CustomerPortal):
|
||||
session_info = request.env['ir.http'].session_info()
|
||||
user_context = dict(request.env.context) if request.session.uid else {}
|
||||
mods = conf.server_wide_modules or []
|
||||
qweb_checksum = HomeStaticTemplateHelpers.get_qweb_templates_checksum(debug=request.session.debug, bundle="mrp_subcontracting.assets_qweb")
|
||||
lang = user_context.get("lang")
|
||||
translation_hash = request.env['ir.translation'].get_web_translations_hash(mods, lang)
|
||||
cache_hashes = {
|
||||
"qweb": qweb_checksum,
|
||||
"translations": translation_hash,
|
||||
}
|
||||
production_company = picking.company_id
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
<t t-set="head_subcontracting_portal">
|
||||
<script type="text/javascript">
|
||||
odoo.__session_info__ = <t t-out="json.dumps(session_info)"/>;
|
||||
odoo.loadTemplatesPromise = fetch(`/web/webclient/qweb/${odoo.__session_info__.cache_hashes.qweb}?bundle=mrp_subcontracting.assets_qweb`).then(doc => doc.text());
|
||||
</script>
|
||||
<base target="_parent"/>
|
||||
<t t-call-assets="web.assets_common" t-js="false"/>
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
// Prevent the menu_service to load anything. In an ideal world, POS assets would only contain
|
||||
// what is genuinely necessary, and not the whole backend.
|
||||
odoo.loadMenusPromise = Promise.resolve();
|
||||
odoo.loadTemplatesPromise = fetch(`/web/webclient/qweb/${odoo.__session_info__.cache_hashes.qweb}?bundle=web.assets_qweb`).then(doc => doc.text());
|
||||
</script>
|
||||
|
||||
<t t-call="point_of_sale.assets_common"/>
|
||||
|
||||
@@ -12,8 +12,6 @@ from odoo.tools import groupby as groupbyelem
|
||||
|
||||
from odoo.osv.expression import OR, AND
|
||||
|
||||
from odoo.addons.web.controllers.utils import HomeStaticTemplateHelpers
|
||||
|
||||
|
||||
class ProjectCustomerPortal(CustomerPortal):
|
||||
|
||||
@@ -135,7 +133,6 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||
session_info = request.env['ir.http'].session_info()
|
||||
user_context = dict(request.env.context) if request.session.uid else {}
|
||||
mods = conf.server_wide_modules or []
|
||||
qweb_checksum = HomeStaticTemplateHelpers.get_qweb_templates_checksum(debug=request.session.debug, bundle="project.assets_qweb")
|
||||
if request.env.lang:
|
||||
lang = request.env.lang
|
||||
session_info['user_context']['lang'] = lang
|
||||
@@ -144,7 +141,6 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||
lang = user_context.get("lang")
|
||||
translation_hash = request.env['ir.translation'].get_web_translations_hash(mods, lang)
|
||||
cache_hashes = {
|
||||
"qweb": qweb_checksum,
|
||||
"translations": translation_hash,
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
// Prevent the menu_service to load anything. In an ideal world, Project Sharing assets would only contain
|
||||
// what is genuinely necessary, and not the whole backend.
|
||||
odoo.loadMenusPromise = Promise.resolve();
|
||||
odoo.loadTemplatesPromise = fetch(`/web/webclient/qweb/${odoo.__session_info__.cache_hashes.qweb}?bundle=project.assets_qweb`).then(doc => doc.text());
|
||||
</script>
|
||||
<base target="_parent"/>
|
||||
<t t-call-assets="web.assets_common" t-js="false"/>
|
||||
|
||||
@@ -406,6 +406,7 @@ This module provides the core of the Odoo Web Client.
|
||||
'web/static/lib/clipboard/clipboard.js',
|
||||
'web/static/lib/jSignature/jSignatureCustom.js',
|
||||
'web/static/lib/qweb/qweb2.js',
|
||||
'web/static/src/legacy/js/assets.js',
|
||||
'web/static/src/legacy/js/libs/autocomplete.js',
|
||||
'web/static/src/legacy/js/libs/bootstrap.js',
|
||||
'web/static/src/legacy/js/libs/content-disposition.js',
|
||||
|
||||
@@ -32,7 +32,6 @@ _MOVED_TO_MAP = {
|
||||
'GroupExportXlsxWriter': export,
|
||||
'GroupsTreeNode': export,
|
||||
'Home': home,
|
||||
'HomeStaticTemplateHelpers': utils,
|
||||
'none_values_filtered': export,
|
||||
'OPERATOR_MAPPING': export,
|
||||
'ReportController': report,
|
||||
|
||||
@@ -215,212 +215,3 @@ def _local_web_translations(trans_file):
|
||||
if x.id and x.string and "openerp-web" in x.auto_comments:
|
||||
messages.append({'id': x.id, 'string': x.string})
|
||||
return messages
|
||||
|
||||
|
||||
class HomeStaticTemplateHelpers:
|
||||
r"""
|
||||
Helper Class that wraps the reading of static qweb templates files
|
||||
and xpath inheritance applied to those templates
|
||||
/!\ Template inheritance order is defined by ir.module.module natural order
|
||||
which is "sequence, name"
|
||||
Then a topological sort is applied, which just puts dependencies
|
||||
of a module before that module
|
||||
"""
|
||||
NAME_TEMPLATE_DIRECTIVE = 't-name'
|
||||
STATIC_INHERIT_DIRECTIVE = 't-inherit'
|
||||
STATIC_INHERIT_MODE_DIRECTIVE = 't-inherit-mode'
|
||||
PRIMARY_MODE = 'primary'
|
||||
EXTENSION_MODE = 'extension'
|
||||
DEFAULT_MODE = PRIMARY_MODE
|
||||
COMMENT_PATTERN = r'Modified by [\s\w\-.]+ from [\s\w\-.]+'
|
||||
|
||||
def __init__(self, addons, db, checksum_only=False, debug=False):
|
||||
"""
|
||||
:param str|list addons: plain list or comma separated list of addons
|
||||
:param str db: the current db we are working on
|
||||
:param bool checksum_only: only computes the checksum of all files for addons
|
||||
:param str debug: the debug mode of the session
|
||||
"""
|
||||
self.addons = addons.split(',') if isinstance(addons, str) else addons
|
||||
self.db = db
|
||||
self.debug = debug
|
||||
self.checksum_only = checksum_only
|
||||
self.template_dict = OrderedDict()
|
||||
|
||||
def _get_parent_template(self, addon, template):
|
||||
"""Computes the real addon name and the template name
|
||||
of the parent template (the one that is inherited from)
|
||||
|
||||
:param str addon: the addon the template is declared in
|
||||
:param etree template: the current template we are are handling
|
||||
:returns: (str, str)
|
||||
"""
|
||||
original_template_name = template.attrib[self.STATIC_INHERIT_DIRECTIVE]
|
||||
split_name_attempt = original_template_name.split('.', 1)
|
||||
parent_addon, parent_name = tuple(split_name_attempt) if len(split_name_attempt) == 2 else (addon, original_template_name)
|
||||
if parent_addon not in self.template_dict:
|
||||
if original_template_name in self.template_dict[addon]:
|
||||
parent_addon = addon
|
||||
parent_name = original_template_name
|
||||
else:
|
||||
raise ValueError(_('Module %s not loaded or inexistent, or templates of addon being loaded (%s) are misordered') % (parent_addon, addon))
|
||||
|
||||
if parent_name not in self.template_dict[parent_addon]:
|
||||
raise ValueError(_("No template found to inherit from. Module %s and template name %s") % (parent_addon, parent_name))
|
||||
|
||||
return parent_addon, parent_name
|
||||
|
||||
def _compute_xml_tree(self, addon, file_name, source):
|
||||
"""Computes the xml tree that 'source' contains
|
||||
Applies inheritance specs in the process
|
||||
|
||||
:param str addon: the current addon we are reading files for
|
||||
:param str file_name: the current name of the file we are reading
|
||||
:param str source: the content of the file
|
||||
:returns: etree
|
||||
"""
|
||||
try:
|
||||
all_templates_tree = etree.parse(io.BytesIO(source), parser=etree.XMLParser(remove_comments=True)).getroot()
|
||||
except etree.ParseError as e:
|
||||
_logger.error("Could not parse file %s: %s", file_name, e.msg)
|
||||
raise
|
||||
|
||||
self.template_dict.setdefault(addon, OrderedDict())
|
||||
for template_tree in list(all_templates_tree):
|
||||
if self.NAME_TEMPLATE_DIRECTIVE in template_tree.attrib:
|
||||
template_name = template_tree.attrib[self.NAME_TEMPLATE_DIRECTIVE]
|
||||
dotted_names = template_name.split('.', 1)
|
||||
if len(dotted_names) > 1 and dotted_names[0] == addon:
|
||||
template_name = dotted_names[1]
|
||||
else:
|
||||
# self.template_dict[addon] grows after processing each template
|
||||
template_name = 'anonymous_template_%s' % len(self.template_dict[addon])
|
||||
if self.STATIC_INHERIT_DIRECTIVE in template_tree.attrib:
|
||||
inherit_mode = template_tree.attrib.get(self.STATIC_INHERIT_MODE_DIRECTIVE, self.DEFAULT_MODE)
|
||||
if inherit_mode not in [self.PRIMARY_MODE, self.EXTENSION_MODE]:
|
||||
raise ValueError(_("Invalid inherit mode. Module %s and template name %s") % (addon, template_name))
|
||||
|
||||
parent_addon, parent_name = self._get_parent_template(addon, template_tree)
|
||||
|
||||
# After several performance tests, we found out that deepcopy is the most efficient
|
||||
# solution in this case (compared with copy, xpath with '.' and stringifying).
|
||||
parent_tree = copy.deepcopy(self.template_dict[parent_addon][parent_name])
|
||||
|
||||
xpaths = list(template_tree)
|
||||
# owl chokes on comments, disable debug comments for now
|
||||
# pylint: disable=W0125
|
||||
if False: # self.debug and inherit_mode == self.EXTENSION_MODE:
|
||||
for xpath in xpaths:
|
||||
xpath.insert(0, etree.Comment(" Modified by %s from %s " % (template_name, addon)))
|
||||
elif inherit_mode == self.PRIMARY_MODE:
|
||||
parent_tree.tag = template_tree.tag
|
||||
inherited_template = apply_inheritance_specs(parent_tree, xpaths)
|
||||
|
||||
if inherit_mode == self.PRIMARY_MODE: # New template_tree: A' = B(A)
|
||||
for attr_name, attr_val in template_tree.attrib.items():
|
||||
if attr_name not in ('t-inherit', 't-inherit-mode'):
|
||||
inherited_template.set(attr_name, attr_val)
|
||||
if self.debug:
|
||||
self._remove_inheritance_comments(inherited_template)
|
||||
self.template_dict[addon][template_name] = inherited_template
|
||||
|
||||
else: # Modifies original: A = B(A)
|
||||
self.template_dict[parent_addon][parent_name] = inherited_template
|
||||
else:
|
||||
if template_name in self.template_dict[addon]:
|
||||
raise ValueError(_("Template %s already exists in module %s") % (template_name, addon))
|
||||
self.template_dict[addon][template_name] = template_tree
|
||||
return all_templates_tree
|
||||
|
||||
def _remove_inheritance_comments(self, inherited_template):
|
||||
'''Remove the comments added in the template already, they come from other templates extending
|
||||
the base of this inheritance
|
||||
|
||||
:param inherited_template:
|
||||
'''
|
||||
for comment in inherited_template.xpath('//comment()'):
|
||||
if re.match(self.COMMENT_PATTERN, comment.text.strip()):
|
||||
comment.getparent().remove(comment)
|
||||
|
||||
def _read_addon_file(self, path_or_url):
|
||||
"""Read the content of a file or an ``ir.attachment`` record given by
|
||||
``path_or_url``.
|
||||
|
||||
:param str path_or_url:
|
||||
:returns: bytes
|
||||
:raises FileNotFoundError: if the path does not match a module file
|
||||
or an attachment
|
||||
"""
|
||||
try:
|
||||
with file_open(path_or_url, 'rb') as fp:
|
||||
contents = fp.read()
|
||||
except FileNotFoundError as e:
|
||||
attachment = request.env['ir.attachment'].sudo().search([
|
||||
('url', '=', path_or_url),
|
||||
('type', '=', 'binary'),
|
||||
], limit=1)
|
||||
if attachment:
|
||||
contents = attachment.raw
|
||||
else:
|
||||
raise e
|
||||
return contents
|
||||
|
||||
def _concat_xml(self, file_dict):
|
||||
"""Concatenate xml files
|
||||
|
||||
:param dict(list) file_dict:
|
||||
key: addon name
|
||||
value: list of files for an addon
|
||||
:returns: (concatenation_result, checksum)
|
||||
:rtype: (bytes, str)
|
||||
"""
|
||||
checksum = hashlib.new('sha512') # sha512/256
|
||||
if not file_dict:
|
||||
return b'', checksum.hexdigest()
|
||||
|
||||
root = None
|
||||
for addon, fnames in file_dict.items():
|
||||
for fname in fnames:
|
||||
contents = self._read_addon_file(fname)
|
||||
checksum.update(contents)
|
||||
if not self.checksum_only:
|
||||
self._compute_xml_tree(addon, fname, contents)
|
||||
|
||||
if root is None:
|
||||
root = etree.Element('templates')
|
||||
|
||||
for addon in self.template_dict.values():
|
||||
for template in addon.values():
|
||||
root.append(template)
|
||||
|
||||
return etree.tostring(root, encoding='utf-8') if root is not None else b'', checksum.hexdigest()[:64]
|
||||
|
||||
def _get_asset_paths(self, bundle):
|
||||
"""Proxy for ir_asset._get_asset_paths
|
||||
Useful to make 'self' testable.
|
||||
"""
|
||||
return request.env['ir.asset']._get_asset_paths(addons=self.addons, bundle=bundle, xml=True)
|
||||
|
||||
def _get_qweb_templates(self, bundle):
|
||||
"""One and only entry point that gets and evaluates static qweb templates
|
||||
|
||||
:rtype: (str, str)
|
||||
"""
|
||||
xml_paths = defaultdict(list)
|
||||
|
||||
# group paths by module, keeping them in order
|
||||
for path, addon, _ in self._get_asset_paths(bundle):
|
||||
addon_paths = xml_paths[addon]
|
||||
if path not in addon_paths:
|
||||
addon_paths.append(path)
|
||||
|
||||
content, checksum = self._concat_xml(xml_paths)
|
||||
return content, checksum
|
||||
|
||||
@classmethod
|
||||
def get_qweb_templates_checksum(cls, addons=None, db=None, debug=False, bundle=None):
|
||||
return cls(addons, db, checksum_only=True, debug=debug)._get_qweb_templates(bundle)[1]
|
||||
|
||||
@classmethod
|
||||
def get_qweb_templates(cls, addons=None, db=None, debug=False, bundle=None):
|
||||
return cls(addons, db, debug=debug)._get_qweb_templates(bundle)[0]
|
||||
|
||||
@@ -17,7 +17,7 @@ from odoo.modules import get_manifest, get_resource_path
|
||||
from odoo.http import request
|
||||
from odoo.tools import lazy
|
||||
from odoo.tools.misc import file_open
|
||||
from .utils import HomeStaticTemplateHelpers, _local_web_translations
|
||||
from .utils import _local_web_translations
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -53,17 +53,6 @@ class WebClient(http.Controller):
|
||||
('Cache-Control', f'max-age={http.STATIC_CACHE}'),
|
||||
])
|
||||
|
||||
@http.route('/web/webclient/qweb/<string:unique>', type='http', auth="none", cors="*")
|
||||
def qweb(self, unique, mods=None, db=None, bundle=None):
|
||||
if not request.db and mods is None:
|
||||
mods = odoo.conf.server_wide_modules or []
|
||||
content = HomeStaticTemplateHelpers.get_qweb_templates(mods, db, debug=request.session.debug, bundle=bundle)
|
||||
|
||||
return request.make_response(content, [
|
||||
('Content-Type', 'text/xml'),
|
||||
('Cache-Control', f'public, max-age={http.STATIC_CACHE_LONG}')
|
||||
])
|
||||
|
||||
@http.route('/web/webclient/bootstrap_translations', type='json', auth="none")
|
||||
def bootstrap_translations(self, mods=None):
|
||||
""" Load local translations from *.po files, as a temporary solution
|
||||
@@ -139,14 +128,20 @@ class WebClient(http.Controller):
|
||||
def benchmarks(self, mod=None, **kwargs):
|
||||
return request.render('web.benchmark_suite')
|
||||
|
||||
@http.route('/web/bundle/<string:bundle_name>', auth="user", methods=["GET"])
|
||||
def bundle(self, bundle_name):
|
||||
@http.route('/web/bundle/<string:bundle_name>', auth="public", methods=["GET"])
|
||||
def bundle(self, bundle_name, **bundle_params):
|
||||
"""
|
||||
Request the definition of a bundle, including its javascript and css bundled assets
|
||||
"""
|
||||
files = request.env["ir.qweb"]._get_asset_nodes(bundle_name, debug=request.session.debug, js=True, css=True)
|
||||
data = json.dumps([{
|
||||
if 'lang' in bundle_params:
|
||||
request.update_context(lang=bundle_params['lang'])
|
||||
|
||||
debug = bundle_params.get('debug', request.session.debug)
|
||||
files = request.env["ir.qweb"]._get_asset_nodes(bundle_name, debug=debug, js=True, css=True)
|
||||
data = [{
|
||||
"type": tag,
|
||||
"src": attrs.get("src") or attrs.get('href'),
|
||||
} for tag, attrs, _ in files])
|
||||
return request.make_response(data, [('Content-Type', 'application/json')])
|
||||
"src": attrs.get("src") or attrs.get("data-src") or attrs.get('href'),
|
||||
"content": content,
|
||||
} for tag, attrs, content in files]
|
||||
|
||||
return request.make_json_response(data)
|
||||
|
||||
@@ -9,7 +9,6 @@ from odoo import api, http, models
|
||||
from odoo.http import request
|
||||
from odoo.tools import file_open, image_process, ustr
|
||||
from odoo.tools.misc import str2bool
|
||||
from odoo.addons.web.controllers.utils import HomeStaticTemplateHelpers
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -110,7 +109,12 @@ class Http(models.AbstractModel):
|
||||
) if session_uid else None,
|
||||
},
|
||||
"currencies": self.sudo().get_currencies(),
|
||||
'bundle_params': {
|
||||
'lang': request.session.context['lang'],
|
||||
},
|
||||
}
|
||||
if request.session.debug:
|
||||
session_info['bundle_params']['debug'] = request.session.debug
|
||||
if self.env.user.has_group('base.group_user'):
|
||||
# the following is only useful in the context of a webclient bootstrapping
|
||||
# but is still included in some other calls (e.g. '/web/session/authenticate')
|
||||
@@ -118,13 +122,11 @@ class Http(models.AbstractModel):
|
||||
# with access to the backend ('internal'-type users)
|
||||
if request.db:
|
||||
mods = list(request.registry._init_modules) + mods
|
||||
qweb_checksum = HomeStaticTemplateHelpers.get_qweb_templates_checksum(debug=request.session.debug, bundle="web.assets_qweb")
|
||||
menus = request.env['ir.ui.menu'].load_menus(request.session.debug)
|
||||
ordered_menus = {str(k): v for k, v in menus.items()}
|
||||
menu_json_utf8 = json.dumps(ordered_menus, default=ustr, sort_keys=True).encode()
|
||||
session_info['cache_hashes'].update({
|
||||
"load_menus": hashlib.sha512(menu_json_utf8).hexdigest()[:64], # sha512/256
|
||||
"qweb": qweb_checksum,
|
||||
})
|
||||
session_info.update({
|
||||
# current_company should be default_company
|
||||
@@ -157,7 +159,12 @@ class Http(models.AbstractModel):
|
||||
'profile_collectors': request.session.profile_collectors,
|
||||
'profile_params': request.session.profile_params,
|
||||
'show_effect': bool(request.env['ir.config_parameter'].sudo().get_param('base_setup.show_effect')),
|
||||
'bundle_params': {
|
||||
'lang': request.session.context['lang'],
|
||||
},
|
||||
}
|
||||
if request.session.debug:
|
||||
session_info['bundle_params']['debug'] = request.session.debug
|
||||
if session_uid:
|
||||
version_info = odoo.service.common.exp_version()
|
||||
session_info.update({
|
||||
|
||||
+106
-85
@@ -111,6 +111,93 @@
|
||||
return !!job.error;
|
||||
});
|
||||
},
|
||||
processJobs: function () {
|
||||
var job;
|
||||
|
||||
function processJob(job) {
|
||||
var require = makeRequire(job);
|
||||
|
||||
var jobExec;
|
||||
function onError(e) {
|
||||
job.error = e;
|
||||
console.error(`Error while loading ${job.name}: ${e.message}`, e);
|
||||
}
|
||||
var def = new Promise(function (resolve) {
|
||||
try {
|
||||
jobExec = job.factory.call(null, require);
|
||||
jobs.splice(jobs.indexOf(job), 1);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
if (!job.error) {
|
||||
Promise.resolve(jobExec)
|
||||
.then(function (data) {
|
||||
services[job.name] = data;
|
||||
resolve();
|
||||
odoo.__DEBUG__.processJobs();
|
||||
})
|
||||
.guardedCatch(function (e) {
|
||||
job.rejected = e || true;
|
||||
jobs.push(job);
|
||||
})
|
||||
.catch(function (e) {
|
||||
if (e instanceof Error) {
|
||||
onError(e);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
jobPromises.push(def);
|
||||
def.then(job.resolve);
|
||||
}
|
||||
|
||||
function isReady(job) {
|
||||
return (
|
||||
!job.error &&
|
||||
!job.rejected &&
|
||||
job.factory.deps.every(function (name) {
|
||||
return name in services;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function makeRequire(job) {
|
||||
var deps = {};
|
||||
Object.keys(services)
|
||||
.filter(function (item) {
|
||||
return job.deps.indexOf(item) >= 0;
|
||||
})
|
||||
.forEach(function (key) {
|
||||
deps[key] = services[key];
|
||||
});
|
||||
|
||||
return function require(name) {
|
||||
if (!(name in deps)) {
|
||||
console.error("Undefined dependency: ", name);
|
||||
}
|
||||
return deps[name];
|
||||
};
|
||||
}
|
||||
|
||||
while (jobs.length) {
|
||||
job = undefined;
|
||||
for (var i = 0; i < jobs.length; i++) {
|
||||
if (isReady(jobs[i])) {
|
||||
job = jobs[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!job) {
|
||||
break;
|
||||
}
|
||||
processJob(job);
|
||||
}
|
||||
|
||||
return services;
|
||||
},
|
||||
factories: factories,
|
||||
services: services,
|
||||
};
|
||||
@@ -147,17 +234,21 @@
|
||||
factory.deps = deps;
|
||||
factories[name] = factory;
|
||||
|
||||
let promiseResolve;
|
||||
const promise = new Promise(resolve => {promiseResolve = resolve;});
|
||||
jobs.push({
|
||||
name: name,
|
||||
factory: factory,
|
||||
deps: deps,
|
||||
resolve: promiseResolve,
|
||||
promise: promise,
|
||||
});
|
||||
|
||||
deps.forEach(function (dep) {
|
||||
jobDeps.push({ from: dep, to: name });
|
||||
});
|
||||
|
||||
this.processJobs(jobs, services);
|
||||
odoo.__DEBUG__.processJobs();
|
||||
};
|
||||
odoo.log = function () {
|
||||
var missing = [];
|
||||
@@ -271,91 +362,21 @@
|
||||
};
|
||||
didLogInfoResolve(true);
|
||||
};
|
||||
odoo.processJobs = function (jobs, services) {
|
||||
var job;
|
||||
|
||||
function processJob(job) {
|
||||
var require = makeRequire(job);
|
||||
|
||||
var jobExec;
|
||||
function onError(e) {
|
||||
job.error = e;
|
||||
console.error(`Error while loading ${job.name}: ${e.message}`, e);
|
||||
}
|
||||
var def = new Promise(function (resolve) {
|
||||
try {
|
||||
jobExec = job.factory.call(null, require);
|
||||
jobs.splice(jobs.indexOf(job), 1);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
if (!job.error) {
|
||||
Promise.resolve(jobExec)
|
||||
.then(function (data) {
|
||||
services[job.name] = data;
|
||||
resolve();
|
||||
odoo.processJobs(jobs, services);
|
||||
})
|
||||
.guardedCatch(function (e) {
|
||||
job.rejected = e || true;
|
||||
jobs.push(job);
|
||||
})
|
||||
.catch(function (e) {
|
||||
if (e instanceof Error) {
|
||||
onError(e);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
jobPromises.push(def);
|
||||
/**
|
||||
* Returns a resolved promise when the targeted services are loaded.
|
||||
* If no service is found the promise is used directly.
|
||||
*
|
||||
* @param {string|RegExp} serviceName name of the service to expect
|
||||
* or regular expression matching the service.
|
||||
* @returns {Promise<number>} resolved when the services ares
|
||||
* loaded. The value is equal to the number of services found.
|
||||
*/
|
||||
odoo.ready = async function (serviceName) {
|
||||
function match (name) {
|
||||
return typeof serviceName === 'string' ? name === serviceName : serviceName.test(name);
|
||||
}
|
||||
|
||||
function isReady(job) {
|
||||
return (
|
||||
!job.error &&
|
||||
!job.rejected &&
|
||||
job.factory.deps.every(function (name) {
|
||||
return name in services;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function makeRequire(job) {
|
||||
var deps = {};
|
||||
Object.keys(services)
|
||||
.filter(function (item) {
|
||||
return job.deps.indexOf(item) >= 0;
|
||||
})
|
||||
.forEach(function (key) {
|
||||
deps[key] = services[key];
|
||||
});
|
||||
|
||||
return function require(name) {
|
||||
if (!(name in deps)) {
|
||||
console.error("Undefined dependency: ", name);
|
||||
}
|
||||
return deps[name];
|
||||
};
|
||||
}
|
||||
|
||||
while (jobs.length) {
|
||||
job = undefined;
|
||||
for (var i = 0; i < jobs.length; i++) {
|
||||
if (isReady(jobs[i])) {
|
||||
job = jobs[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!job) {
|
||||
break;
|
||||
}
|
||||
processJob(job);
|
||||
}
|
||||
|
||||
return services;
|
||||
await Promise.all(jobs.filter(job => match(job.name)).map(job => job.promise));
|
||||
return Object.keys(factories).filter(match).length;
|
||||
};
|
||||
|
||||
// Automatically log errors detected when loading modules
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
import { memoize } from "./utils/functions";
|
||||
import { browser } from "./browser/browser";
|
||||
import { registry } from "./registry";
|
||||
|
||||
class AssetsLoadingError extends Error {}
|
||||
import { session } from "@web/session";
|
||||
|
||||
/**
|
||||
* An object describing a bundle to load
|
||||
* @typedef {Object} BundleInfo
|
||||
* @property {'script'|'link'} [type] the type of file in this bundle
|
||||
* @property {string} [src] the url of the file for this bundle, for this type of file
|
||||
* @example `[{"type": "script", "src": "/web/assets/266-d34b0b4/documents_spreadsheet.o_spreadsheet.min.js"}]`
|
||||
* This export is done only in order to modify the behavior of the exported
|
||||
* functions. This is done in order to be able to make a test environment.
|
||||
* Modules should only use the methods exported below.
|
||||
*/
|
||||
export const assets = {};
|
||||
|
||||
class AssetsLoadingError extends Error {}
|
||||
|
||||
/**
|
||||
* Loads the given url inside a script tag.
|
||||
@@ -20,7 +20,7 @@ class AssetsLoadingError extends Error {}
|
||||
* @param {string} url the url of the script
|
||||
* @returns {Promise<true>} resolved when the script has been loaded
|
||||
*/
|
||||
export const loadJS = memoize(function loadJS(url) {
|
||||
assets.loadJS = memoize(function loadJS(url) {
|
||||
if (document.querySelector(`script[src="${url}"]`)) {
|
||||
// Already in the DOM and wasn't loaded through this function
|
||||
// Unfortunately there is no way to check whether a script has loaded
|
||||
@@ -28,6 +28,7 @@ export const loadJS = memoize(function loadJS(url) {
|
||||
// so we assume it is.
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const scriptEl = document.createElement("script");
|
||||
scriptEl.type = "text/javascript";
|
||||
scriptEl.src = url;
|
||||
@@ -39,13 +40,14 @@ export const loadJS = memoize(function loadJS(url) {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Loads the given url as a stylesheet.
|
||||
*
|
||||
* @param {string} url the url of the stylesheet
|
||||
* @returns {Promise<true>} resolved when the stylesheet has been loaded
|
||||
*/
|
||||
export const loadCSS = memoize(function loadCSS(url) {
|
||||
assets.loadCSS = memoize(function loadCSS(url) {
|
||||
if (document.querySelector(`link[href="${url}"]`)) {
|
||||
// Already in the DOM and wasn't loaded through this function
|
||||
// Unfortunately there is no way to check whether a link has loaded
|
||||
@@ -65,110 +67,187 @@ export const loadCSS = memoize(function loadCSS(url) {
|
||||
});
|
||||
});
|
||||
});
|
||||
/**
|
||||
* Loads the qweb templates from a given bundle name.
|
||||
* TODO: merge this into loadBundleDefinition?
|
||||
*
|
||||
* @param {string} bundle the name of the bundle as declared in the manifest.
|
||||
* @returns {Promise<XMLDocument|"">} A Promise of an XML document containing
|
||||
* the owl templates or an empty string if the bundle has none.
|
||||
*/
|
||||
export const fetchAndProcessTemplates = memoize(async function fetchAndProcessTemplates(bundle) {
|
||||
// TODO: quid of the "unique" in the URL? We can't have one cache_hash
|
||||
// for each and every bundle I'm guessing.
|
||||
const bundleURL = `/web/webclient/qweb/${Date.now()}?bundle=${bundle}`;
|
||||
const templates = await (await browser.fetch(bundleURL)).text();
|
||||
if (!templates) {
|
||||
return "";
|
||||
}
|
||||
return processTemplates(templates);
|
||||
});
|
||||
|
||||
/**
|
||||
* Loads the content definition of a bundle.
|
||||
*
|
||||
* @param {string} name the bundleName of the bundle as declared in the manifest.
|
||||
* @returns {Promise<BundleInfo[]>} A promise of the content definition of the bundle
|
||||
* Container dom containing all the owl templates that have been loaded.
|
||||
* This can be imported by the modules in order to use it when loading the
|
||||
* application and the components.
|
||||
*/
|
||||
const loadBundleDefinition = memoize(async function (bundleName) {
|
||||
const request = await browser.fetch(`/web/bundle/${bundleName}`);
|
||||
return await request.json();
|
||||
});
|
||||
|
||||
const bundlesCache = {};
|
||||
export const templates = new DOMParser().parseFromString("<odoo/>", "text/xml");
|
||||
|
||||
let defaultApp;
|
||||
/**
|
||||
* Loads a bundle.
|
||||
* Loads the given xml template.
|
||||
*
|
||||
* @param {string} name the name of the bundle to load
|
||||
* @param {owl.App} [app] the app in which the bundle's templates should be
|
||||
* loaded. Defaults to the app that's written on the function itself, this
|
||||
* is considered the main app, and should be written on the function by the
|
||||
* code that bootstraps the app. In most cases, this will be the webclient,
|
||||
* and is set in start.js
|
||||
* @returns {Promise<void>} a promise that is resolved after the bundle has been
|
||||
* loaded.
|
||||
* @param {string} xml the string defining the templates
|
||||
* @param {App} [app=defaultApp] optional owl App instance (default value
|
||||
* can be changed with setLoadXmlDefaultApp method)
|
||||
* @returns {Promise<true>} resolved when the template xml has been loaded
|
||||
*/
|
||||
export async function loadBundle(name, app = loadBundle.app) {
|
||||
if (!bundlesCache[name]) {
|
||||
bundlesCache[name] = Promise.all([
|
||||
fetchAndProcessTemplates(name).then((templates) => app.addTemplates(templates, app)),
|
||||
loadBundleDefinition(name).then((bundleInfo) =>
|
||||
Promise.all([
|
||||
...bundleInfo.filter((i) => i.type === "script").map((i) => loadJS(i.src)),
|
||||
...bundleInfo.filter((i) => i.type === "link").map((i) => loadCSS(i.src)),
|
||||
])
|
||||
),
|
||||
]).then(() => {});
|
||||
assets.loadXML = function loadXML (xml, app=defaultApp) {
|
||||
const doc = new DOMParser().parseFromString(xml, "text/xml");
|
||||
if (doc.querySelector('parsererror')) {
|
||||
throw doc.querySelector('parsererror div').textContent.split(':')[0];
|
||||
}
|
||||
|
||||
return bundlesCache[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the qweb templates to obtain only the owl templates. This function
|
||||
* does NOT register the templates into Owl.
|
||||
*
|
||||
* @param {string} templates An xml string describing templates
|
||||
* @returns {XMLDocument} An xml document containing only the owl templates
|
||||
*/
|
||||
export function processTemplates(templates) {
|
||||
const doc = new DOMParser().parseFromString(templates, "text/xml");
|
||||
// as we currently have two qweb engines (owl and legacy), owl templates are
|
||||
// flagged with attribute `owl="1"`. The following lines removes the "owl"
|
||||
// attribute from the templates, so that it doesn't appear in the DOM. We
|
||||
// also remove the non-owl templates, as those shouldn't be loaded in the
|
||||
// owl application, and will be loaded separately.
|
||||
for (const template of [...doc.querySelector("templates").children]) {
|
||||
if (template.hasAttribute("owl")) {
|
||||
template.removeAttribute("owl");
|
||||
for (const element of doc.querySelectorAll("templates > [t-name][owl]")) {
|
||||
element.removeAttribute("owl");
|
||||
const name = element.getAttribute('t-name');
|
||||
const previous = templates.querySelector(`[t-name="${name}"]`);
|
||||
if (previous) {
|
||||
console.debug('Override template: ' + name);
|
||||
previous.replaceWith(element);
|
||||
} else {
|
||||
template.remove();
|
||||
templates.documentElement.appendChild(element);
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
if (app || defaultApp) {
|
||||
console.debug('Add templates in Owl app.');
|
||||
app.addTemplates(templates, app || defaultApp);
|
||||
} else {
|
||||
console.debug('Add templates on window Owl container.');
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Update the default app to load templates.
|
||||
*
|
||||
* @param {App} app owl App instance
|
||||
*/
|
||||
export function setLoadXmlDefaultApp(app) {
|
||||
defaultApp = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a public asset template and loads the libraries defined inside of it.
|
||||
* Only loads js and css, template declarations will be ignored. Only loads
|
||||
* scripts and styles that are defined in script src and link href, ignores
|
||||
* inline scripts and styles.
|
||||
* Get the files information as descriptor object from a public asset template.
|
||||
*
|
||||
* @deprecated
|
||||
* @param {string} xmlid The xmlid of the template that defines the public asset
|
||||
* @param {ORM} orm An ORM object capable of calling methods on models
|
||||
* @returns {Promise<void>} Resolved when the contents of the asset is loaded
|
||||
* @param {string} bundleName Name of the bundle containing the list of files
|
||||
* @returns {Promise<{cssLibs, cssContents, jsLibs, jsContents}>}
|
||||
*/
|
||||
export const loadPublicAsset = memoize(async function loadPublicAsset(xmlid, orm) {
|
||||
const xml = await orm.call("ir.ui.view", "render_public_asset", [xmlid]);
|
||||
const doc = new DOMParser().parseFromString(`<xml>${xml}</xml>`, "text/xml");
|
||||
return Promise.all([
|
||||
...[...doc.querySelectorAll("link[href]")].map((el) => loadCSS(el.getAttribute("href"))),
|
||||
...[...doc.querySelectorAll("script[src]")].map((el) => loadJS(el.getAttribute("src"))),
|
||||
]);
|
||||
assets.getBundle = memoize(async function getBundle(bundleName) {
|
||||
const url = new URL(`/web/bundle/${bundleName}`, location.origin);
|
||||
for (const [key, value] of Object.entries(session.bundle_params || {})) {
|
||||
url.searchParams.set(key, value);
|
||||
}
|
||||
const response = await browser.fetch(url.href);
|
||||
const json = await response.json();
|
||||
const assets = {
|
||||
cssLibs: [],
|
||||
cssContents: [],
|
||||
jsLibs: [],
|
||||
jsContents: [],
|
||||
};
|
||||
for (const key in json) {
|
||||
const file = json[key];
|
||||
if (file.type === 'link') {
|
||||
assets.cssLibs.push(file.src);
|
||||
} else if (file.type === 'style') {
|
||||
assets.cssContents.push(file.content);
|
||||
} else {
|
||||
if (file.src) {
|
||||
assets.jsLibs.push(file.src);
|
||||
} else {
|
||||
assets.jsContents.push(file.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
return assets;
|
||||
});
|
||||
|
||||
/**
|
||||
* Loads the given js/css libraries and asset bundles. Note that no library or
|
||||
* asset will be loaded if it was already done before.
|
||||
*
|
||||
* @param {Object} desc
|
||||
* @param {Array<string|string[]>} [desc.assetLibs=[]]
|
||||
* The list of assets to load. Each list item may be a string (the xmlID
|
||||
* of the asset to load) or a list of strings. The first level is loaded
|
||||
* sequentially (so use this if the order matters) while the assets in
|
||||
* inner lists are loaded in parallel (use this for efficiency but only
|
||||
* if the order does not matter, should rarely be the case for assets).
|
||||
* @param {string[]} [desc.cssLibs=[]]
|
||||
* The list of CSS files to load. They will all be loaded in parallel but
|
||||
* put in the DOM in the given order (only the order in the DOM is used
|
||||
* to determine priority of CSS rules, not loaded time).
|
||||
* @param {Array<string|string[]>} [desc.jsLibs=[]]
|
||||
* The list of JS files to load. Each list item may be a string (the URL
|
||||
* of the file to load) or a list of strings. The first level is loaded
|
||||
* sequentially (so use this if the order matters) while the files in inner
|
||||
* lists are loaded in parallel (use this for efficiency but only
|
||||
* if the order does not matter).
|
||||
* @param {string[]} [desc.cssContents=[]]
|
||||
* List of inline styles to add after loading the CSS files.
|
||||
* @param {string[]} [desc.jsContents=[]]
|
||||
* List of inline scripts to add after loading the JS files.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
assets.loadBundle = memoize(async function loadBundle(desc) {
|
||||
// Load css in parallel
|
||||
const promiseCSS = Promise.all((desc.cssLibs || []).map(assets.loadCSS)).then(() => {
|
||||
if (desc.cssContents && desc.cssContents.length) {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = desc.cssContents.join('\n');
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
});
|
||||
// Load JavaScript (don't wait for the css loading)
|
||||
for (const urlData of desc.jsLibs || []) {
|
||||
if (typeof urlData === 'string') {
|
||||
// serial loading
|
||||
await assets.loadJS(urlData);
|
||||
// Wait template if the JavaScript come from bundle.
|
||||
const bundle = urlData.match(/\/web\/assets\/.*\/([^\/]+?)(\.min)?\.js/);
|
||||
if (bundle) {
|
||||
await odoo.ready(bundle[1] + '.bundle.xml');
|
||||
}
|
||||
} else {
|
||||
// parallel loading
|
||||
await Promise.all(urlData.map(loadJS));
|
||||
}
|
||||
}
|
||||
|
||||
if (desc.jsContents && desc.jsContents.length) {
|
||||
const script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.textContent = desc.jsContents.join('\n');
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
// Wait for the scc loading to be completed before loading the other bundle
|
||||
await promiseCSS;
|
||||
// Load other desc
|
||||
for (const bundleName of desc.assetLibs || []) {
|
||||
if (typeof bundleName === 'string') {
|
||||
// serial loading
|
||||
const desc = await assets.getBundle(bundleName);
|
||||
await assets.loadBundle(desc);
|
||||
} else {
|
||||
// parallel loading
|
||||
await Promise.all(bundleName.map(async bundleName => {
|
||||
const desc = await assets.getBundle(bundleName);
|
||||
return assets.loadBundle(desc);
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const loadJS = function (url) {
|
||||
return assets.loadJS(url);
|
||||
}
|
||||
export const loadCSS = function (url) {
|
||||
return assets.loadCSS(url);
|
||||
}
|
||||
export const loadXML = function (xml, app=defaultApp) {
|
||||
return assets.loadXML(xml, app=app);
|
||||
}
|
||||
export const getBundle = function (bundleName) {
|
||||
return assets.getBundle(bundleName);
|
||||
}
|
||||
export const loadBundle = function (desc) {
|
||||
return assets.loadBundle(desc);
|
||||
}
|
||||
|
||||
|
||||
const { Component, xml, onWillStart } = owl;
|
||||
/**
|
||||
* Utility component that loads an asset bundle before instanciating a component
|
||||
@@ -176,7 +255,8 @@ const { Component, xml, onWillStart } = owl;
|
||||
export class LazyComponent extends Component {
|
||||
setup() {
|
||||
onWillStart(async () => {
|
||||
await loadBundle(this.props.bundle);
|
||||
const bundle = await getBundle(this.props.bundle);
|
||||
await loadBundle(bundle);
|
||||
this.Component = registry.category("lazy_components").get(this.props.Component);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
odoo.define('web.assets.qweb', function (require) {
|
||||
"use strict";
|
||||
|
||||
const {assets} = require('@web/core/assets');
|
||||
|
||||
const loadXML = assets.loadXML;
|
||||
assets.loadXML = function (xml, app) {
|
||||
loadXML(xml, app);
|
||||
|
||||
const doc = new DOMParser().parseFromString(xml, "text/xml");
|
||||
const qwebTemplates = document.createElement("templates");
|
||||
for (const element of doc.querySelectorAll("templates > [t-name]:not([owl]), templates > [t-extend]:not([owl])")) {
|
||||
qwebTemplates.appendChild(element);
|
||||
}
|
||||
|
||||
// don't use require to apply the patch before the first template loading.
|
||||
odoo.ready('web.core').then(function () {
|
||||
const core = odoo.__DEBUG__.services['web.core'];
|
||||
core.qweb.add_template(qwebTemplates);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
@@ -2,7 +2,6 @@ odoo.define('web.ajax', function (require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var concurrency = require('web.concurrency');
|
||||
var core = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var time = require('web.time');
|
||||
@@ -137,90 +136,6 @@ function rpc(url, params, settings) {
|
||||
return jsonRpc(url, 'call', params, settings);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load css asynchronously: fetch it from the url parameter and add a link tag
|
||||
* to <head>.
|
||||
* If the url has already been requested and loaded, the promise will resolve
|
||||
* immediately.
|
||||
*
|
||||
* @param {String} url of the css to be fetched
|
||||
* @returns {Promise} resolved when the css has been loaded.
|
||||
*/
|
||||
var loadCSS = (function () {
|
||||
var urlDefs = {};
|
||||
|
||||
return function loadCSS(url) {
|
||||
if (url in urlDefs) {
|
||||
// nothing to do here
|
||||
} else if ($('link[href="' + url + '"]').length) {
|
||||
// the link is already in the DOM, the promise can be resolved
|
||||
urlDefs[url] = Promise.resolve();
|
||||
} else {
|
||||
var $link = $('<link>', {
|
||||
'href': url,
|
||||
'rel': 'stylesheet',
|
||||
'type': 'text/css'
|
||||
});
|
||||
urlDefs[url] = new Promise(function (resolve, reject) {
|
||||
$link.on('load', function () {
|
||||
resolve();
|
||||
}).on('error', function () {
|
||||
reject(new Error("Couldn't load css dependency: " + $link[0].href));
|
||||
});
|
||||
});
|
||||
$('head').append($link);
|
||||
}
|
||||
return urlDefs[url];
|
||||
};
|
||||
})();
|
||||
|
||||
var loadJS = (function () {
|
||||
var dependenciesPromise = {};
|
||||
|
||||
var load = function loadJS(url) {
|
||||
// Check the DOM to see if a script with the specified url is already there
|
||||
var alreadyRequired = ($('script[src="' + url + '"]').length > 0);
|
||||
|
||||
// If loadJS was already called with the same URL, it will have a registered promise indicating if
|
||||
// the script has been fully loaded. If not, the promise has to be initialized.
|
||||
// This is initialized as already resolved if the script was already there without the need of loadJS.
|
||||
if (url in dependenciesPromise) {
|
||||
return dependenciesPromise[url];
|
||||
}
|
||||
var scriptLoadedPromise = new Promise(function (resolve, reject) {
|
||||
if (alreadyRequired) {
|
||||
resolve();
|
||||
} else {
|
||||
// Get the script associated promise and returns it after initializing the script if needed. The
|
||||
// promise is marked to be resolved on script load and rejected on script error.
|
||||
var script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src = url;
|
||||
script.onload = script.onreadystatechange = function() {
|
||||
if ((script.readyState && script.readyState !== "loaded" && script.readyState !== "complete") || script.onload_done) {
|
||||
return;
|
||||
}
|
||||
script.onload_done = true;
|
||||
resolve(url);
|
||||
};
|
||||
script.onerror = function () {
|
||||
console.error("Error loading file", script.src);
|
||||
reject(url);
|
||||
};
|
||||
var head = document.head || document.getElementsByTagName('head')[0];
|
||||
head.appendChild(script);
|
||||
}
|
||||
});
|
||||
|
||||
dependenciesPromise[url] = scriptLoadedPromise;
|
||||
return scriptLoadedPromise;
|
||||
};
|
||||
|
||||
return load;
|
||||
})();
|
||||
|
||||
|
||||
/**
|
||||
* Cooperative file download implementation, for ajaxy APIs.
|
||||
*
|
||||
@@ -335,87 +250,6 @@ function post (controller_url, data) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an XML file according to the given URL and adds its associated qweb
|
||||
* templates to the given qweb engine. The function can also be used to get
|
||||
* the promise which indicates when all the calls to the function are finished.
|
||||
*
|
||||
* Note: "all the calls" = the calls that happened before the current no-args
|
||||
* one + the calls that will happen after but when the previous ones are not
|
||||
* finished yet.
|
||||
*
|
||||
* @param {string} [url] - an URL where to find qweb templates
|
||||
* @param {QWeb} [qweb] - the engine to which the templates need to be added
|
||||
* @returns {Promise}
|
||||
* If no argument is given to the function, the promise's state
|
||||
* indicates if "all the calls" are finished (see main description).
|
||||
* Otherwise, it indicates when the templates associated to the given
|
||||
* url have been loaded.
|
||||
*/
|
||||
var loadXML = (function () {
|
||||
// Some "static" variables associated to the loadXML function
|
||||
var isLoading = false;
|
||||
var loadingsData = [];
|
||||
var seenURLs = [];
|
||||
|
||||
return function (url, qweb) {
|
||||
function _load() {
|
||||
isLoading = true;
|
||||
if (loadingsData.length) {
|
||||
// There is something to load, load it, resolve the associated
|
||||
// promise then start loading the next one
|
||||
var loadingData = loadingsData[0];
|
||||
loadingData.qweb.add_template(loadingData.url, function () {
|
||||
// Remove from array only now so that multiple calls to
|
||||
// loadXML with the same URL returns the right promise
|
||||
loadingsData.shift();
|
||||
loadingData.resolve();
|
||||
_load();
|
||||
});
|
||||
} else {
|
||||
// There is nothing to load anymore, so resolve the
|
||||
// "all the calls" promise
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If no argument, simply returns the promise which indicates when
|
||||
// "all the calls" are finished
|
||||
if (!url || !qweb) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// If the given URL has already been seen, do nothing but returning the
|
||||
// associated promise
|
||||
if (_.contains(seenURLs, url)) {
|
||||
var oldLoadingData = _.findWhere(loadingsData, {url: url});
|
||||
return oldLoadingData ? oldLoadingData.def : Promise.resolve();
|
||||
}
|
||||
seenURLs.push(url);
|
||||
|
||||
|
||||
// Add the information about the new data to load: the url, the qweb
|
||||
// engine and the associated promise
|
||||
var newLoadingData = {
|
||||
url: url,
|
||||
qweb: qweb,
|
||||
};
|
||||
newLoadingData.def = new Promise(function (resolve, reject) {
|
||||
newLoadingData.resolve = resolve;
|
||||
newLoadingData.reject = reject;
|
||||
});
|
||||
loadingsData.push(newLoadingData);
|
||||
|
||||
// If not already started, start the loading loop (reinitialize the
|
||||
// "all the calls" promise to an unresolved state)
|
||||
if (!isLoading) {
|
||||
_load();
|
||||
}
|
||||
|
||||
// Return the promise associated to the new given URL
|
||||
return newLoadingData.def;
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* Loads a template file according to the given xmlId.
|
||||
@@ -478,92 +312,10 @@ var loadAsset = (function () {
|
||||
return load;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Loads the given js/css libraries and asset bundles. Note that no library or
|
||||
* asset will be loaded if it was already done before.
|
||||
*
|
||||
* @param {Object} libs
|
||||
* @param {Array<string|string[]>} [libs.assetLibs=[]]
|
||||
* The list of assets to load. Each list item may be a string (the xmlID
|
||||
* of the asset to load) or a list of strings. The first level is loaded
|
||||
* sequentially (so use this if the order matters) while the assets in
|
||||
* inner lists are loaded in parallel (use this for efficiency but only
|
||||
* if the order does not matter, should rarely be the case for assets).
|
||||
* @param {string[]} [libs.cssLibs=[]]
|
||||
* The list of CSS files to load. They will all be loaded in parallel but
|
||||
* put in the DOM in the given order (only the order in the DOM is used
|
||||
* to determine priority of CSS rules, not loaded time).
|
||||
* @param {Array<string|string[]>} [libs.jsLibs=[]]
|
||||
* The list of JS files to load. Each list item may be a string (the URL
|
||||
* of the file to load) or a list of strings. The first level is loaded
|
||||
* sequentially (so use this if the order matters) while the files in inner
|
||||
* lists are loaded in parallel (use this for efficiency but only
|
||||
* if the order does not matter).
|
||||
* @param {string[]} [libs.cssContents=[]]
|
||||
* List of inline styles to add after loading the CSS files.
|
||||
* @param {string[]} [libs.jsContents=[]]
|
||||
* List of inline scripts to add after loading the JS files.
|
||||
* @param {Object} [context]
|
||||
* additionnal rpc context to be merged with the default one
|
||||
* @param {string} [tplRoute]
|
||||
* Custom route to use for template rendering of the potential assets
|
||||
* to load (see libs.assetLibs).
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function loadLibs(libs, context, tplRoute) {
|
||||
var mutex = new concurrency.Mutex();
|
||||
mutex.exec(function () {
|
||||
var defs = [];
|
||||
var cssLibs = [libs.cssLibs || []]; // Force loading in parallel
|
||||
defs.push(_loadArray(cssLibs, ajax.loadCSS).then(function () {
|
||||
if (libs.cssContents && libs.cssContents.length) {
|
||||
$('head').append($('<style/>', {
|
||||
html: libs.cssContents.join('\n'),
|
||||
}));
|
||||
}
|
||||
}));
|
||||
defs.push(_loadArray(libs.jsLibs || [], ajax.loadJS).then(function () {
|
||||
if (libs.jsContents && libs.jsContents.length) {
|
||||
$('head').append($('<script/>', {
|
||||
html: libs.jsContents.join('\n'),
|
||||
}));
|
||||
}
|
||||
}));
|
||||
return Promise.all(defs);
|
||||
});
|
||||
mutex.exec(function () {
|
||||
return _loadArray(libs.assetLibs || [], function (xmlID) {
|
||||
return ajax.loadAsset(xmlID, context, tplRoute).then(function (asset) {
|
||||
return ajax.loadLibs(asset);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function _loadArray(array, loadCallback) {
|
||||
var _mutex = new concurrency.Mutex();
|
||||
array.forEach(function (urlData) {
|
||||
_mutex.exec(function () {
|
||||
if (typeof urlData === 'string') {
|
||||
return loadCallback(urlData);
|
||||
}
|
||||
return Promise.all(urlData.map(loadCallback));
|
||||
});
|
||||
});
|
||||
return _mutex.getUnlockedDef();
|
||||
}
|
||||
|
||||
return mutex.getUnlockedDef();
|
||||
}
|
||||
|
||||
_.extend(ajax, {
|
||||
jsonRpc: jsonRpc,
|
||||
rpc: rpc,
|
||||
loadCSS: loadCSS,
|
||||
loadJS: loadJS,
|
||||
loadXML: loadXML,
|
||||
loadAsset: loadAsset,
|
||||
loadLibs: loadLibs,
|
||||
get_file: get_file,
|
||||
post: post,
|
||||
});
|
||||
|
||||
@@ -125,16 +125,6 @@ var ServicesMixin = {
|
||||
});
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} libs - @see ajax.loadLibs
|
||||
* @param {Object} [context] - @see ajax.loadLibs
|
||||
* @param {Object} [tplRoute=this._loadLibsTplRoute] - @see ajax.loadLibs
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_loadLibs: function (libs, context, tplRoute) {
|
||||
return this.call('ajax', 'loadLibs', libs, context, tplRoute || this._loadLibsTplRoute);
|
||||
},
|
||||
/**
|
||||
* Builds and executes RPC query. Returns a promise resolved with
|
||||
* the RPC result.
|
||||
|
||||
@@ -2,7 +2,6 @@ odoo.define('web.Session', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var concurrency = require('web.concurrency');
|
||||
var core = require('web.core');
|
||||
var mixins = require('web.mixins');
|
||||
var utils = require('web.utils');
|
||||
@@ -39,7 +38,6 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
|
||||
// remove it totally (but need to make sure the cookies are properly set)
|
||||
this.name = "instance0";
|
||||
// TODO: session store in cookie should be optional
|
||||
this.qweb_mutex = new concurrency.Mutex();
|
||||
this.currencies = {};
|
||||
this._groups_def = {};
|
||||
core.bus.on('invalidate_session', this, this._onInvalidateSession);
|
||||
@@ -211,29 +209,8 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
|
||||
}
|
||||
});
|
||||
},
|
||||
load_qweb: function () {
|
||||
return this.qweb_mutex.exec(async () => {
|
||||
let templates;
|
||||
if (odoo.loadTemplatesPromise) {
|
||||
templates = await odoo.loadTemplatesPromise;
|
||||
} else {
|
||||
var cacheId = this.cache_hashes && this.cache_hashes.qweb;
|
||||
const route = `/web/webclient/qweb/${(cacheId ? cacheId : Date.now())}?bundle=web.assets_qweb`;
|
||||
templates = await (await fetch(route)).text();
|
||||
}
|
||||
const doc = new DOMParser().parseFromString(templates, "text/xml");
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
const owlTemplates = [];
|
||||
for (let child of doc.querySelectorAll("templates > [owl]")) {
|
||||
child.removeAttribute('owl');
|
||||
owlTemplates.push(child.outerHTML);
|
||||
child.remove();
|
||||
}
|
||||
qweb.add_template(doc);
|
||||
this.owlTemplates = `<templates> ${owlTemplates.join('\n')} </templates>`;
|
||||
});
|
||||
load_qweb: async function () {
|
||||
await odoo.ready(/\.bundle\.xml$/);
|
||||
},
|
||||
get_currency: function (currency_id) {
|
||||
return this.currencies[currency_id];
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
odoo.define('web.Widget', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var mixins = require('web.mixins');
|
||||
var ServicesMixin = require('web.ServicesMixin');
|
||||
const { loadBundle } = require("@web/core/assets");
|
||||
|
||||
|
||||
/**
|
||||
* Base class for all visual components. Provides a lot of functions helpful
|
||||
@@ -76,14 +77,6 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, {
|
||||
* @type {null|string}
|
||||
*/
|
||||
template: null,
|
||||
/**
|
||||
* List of paths to xml files that need to be loaded before the widget can
|
||||
* be rendered. This will not induce loading anything that has already been
|
||||
* loaded.
|
||||
*
|
||||
* @type {null|string[]}
|
||||
*/
|
||||
xmlDependencies: null,
|
||||
/**
|
||||
* List of paths to css files that need to be loaded before the widget can
|
||||
* be rendered. This will not induce loading anything that has already been
|
||||
@@ -142,13 +135,8 @@ var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, {
|
||||
*/
|
||||
willStart: function () {
|
||||
var proms = [];
|
||||
if (this.xmlDependencies) {
|
||||
proms.push.apply(proms, _.map(this.xmlDependencies, function (xmlPath) {
|
||||
return ajax.loadXML(xmlPath, core.qweb);
|
||||
}));
|
||||
}
|
||||
if (this.jsLibs || this.cssLibs || this.assetLibs) {
|
||||
proms.push(this._loadLibs(this));
|
||||
proms.push(loadBundle(this));
|
||||
}
|
||||
return Promise.all(proms);
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ odoo.define('web.FieldWrapper', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { ComponentWrapper } = require('web.OwlCompatibility');
|
||||
const { templates } = require("@web/core/assets");
|
||||
const field_utils = require('web.field_utils');
|
||||
|
||||
const { App, reactive } = owl;
|
||||
@@ -127,7 +128,7 @@ odoo.define('web.FieldWrapper', function (require) {
|
||||
// (only the wrapper is instantiated), so we instantiate one
|
||||
// with the same props, get its 'isSet' status, and destroy it.
|
||||
const app = new App(null, {
|
||||
templates: window.__OWL_TEMPLATES__,
|
||||
templates,
|
||||
env: this.env,
|
||||
dev: this.env.isDebug(),
|
||||
translateFn: owl.Component.env._t,
|
||||
@@ -147,7 +148,7 @@ odoo.define('web.FieldWrapper', function (require) {
|
||||
// (only the wrapper is instantiated), so we instantiate one
|
||||
// with the same props, get its 'isValid' status, and destroy it.
|
||||
const app = new App(null, {
|
||||
templates: window.__OWL_TEMPLATES__,
|
||||
templates,
|
||||
env: this.env,
|
||||
dev: this.env.isDebug(),
|
||||
translateFn: owl.Component.env._t,
|
||||
|
||||
@@ -2,6 +2,7 @@ odoo.define('web.OwlCompatibility', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { LegacyComponent } = require("@web/legacy/legacy_component");
|
||||
const { templates } = require("@web/core/assets");
|
||||
|
||||
/**
|
||||
* This file defines the necessary tools for the transition phase where Odoo
|
||||
@@ -293,7 +294,7 @@ odoo.define('web.OwlCompatibility', function (require) {
|
||||
function standaloneAdapter(props = {}, ref = bodyRef) {
|
||||
const env = owl.Component.env;
|
||||
const app = new App(null, {
|
||||
templates: window.__OWL_TEMPLATES__,
|
||||
templates,
|
||||
env,
|
||||
dev: env.isDebug(),
|
||||
translatableAttributes: ["data-tooltip"],
|
||||
@@ -548,7 +549,7 @@ odoo.define('web.OwlCompatibility', function (require) {
|
||||
const env = owl.Component.env;
|
||||
const appConfig = {
|
||||
env,
|
||||
templates: window.__OWL_TEMPLATES__,
|
||||
templates,
|
||||
dev: env.isDebug(),
|
||||
translatableAttributes: ["data-tooltip"],
|
||||
translateFn: env._t,
|
||||
|
||||
@@ -21,8 +21,8 @@ import {
|
||||
} from "../../utils";
|
||||
import { standaloneAdapter } from "web.OwlCompatibility";
|
||||
|
||||
import { fetchAndProcessTemplates, loadBundle } from "@web/core/assets";
|
||||
import { makeEnv, startServices } from "@web/env";
|
||||
import { assets, templates } from '@web/core/assets';
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { browser } from '@web/core/browser/browser';
|
||||
import { jsonrpc } from '@web/core/network/rpc_service';
|
||||
@@ -263,8 +263,6 @@ export const PublicRoot = publicWidget.RootWidget.extend({
|
||||
}
|
||||
params.kwargs.context = _computeContext.call(this, params.kwargs.context, noContextKeys);
|
||||
}
|
||||
} else if (payload.service === 'ajax' && payload.method === 'loadLibs') {
|
||||
args[1] = _computeContext.call(this, args[1]);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -408,20 +406,18 @@ export async function createPublicRoot(RootWidget) {
|
||||
|
||||
const wowlEnv = makeEnv();
|
||||
|
||||
const templates = await fetchAndProcessTemplates("web.assets_frontend");
|
||||
window.__OWL_TEMPLATES__ = templates;
|
||||
await startServices(wowlEnv);
|
||||
mapLegacyEnvToWowlEnv(legacyEnv, wowlEnv);
|
||||
// The root widget's parent is a standalone adapter so that it has _trigger_up
|
||||
const publicRoot = new RootWidget(standaloneAdapter({ Component }));
|
||||
const app = new App(MainComponentsContainer, {
|
||||
templates,
|
||||
env: wowlEnv,
|
||||
dev: wowlEnv.debug,
|
||||
templates: window.__OWL_TEMPLATES__,
|
||||
translateFn: _t,
|
||||
translatableAttributes: ["data-tooltip"],
|
||||
});
|
||||
loadBundle.app = app;
|
||||
assets.defaultApp = app;
|
||||
await Promise.all([
|
||||
app.mount(document.body),
|
||||
publicRoot.attachTo(document.body),
|
||||
|
||||
@@ -2,19 +2,10 @@ odoo.define('web.AjaxService', function (require) {
|
||||
"use strict";
|
||||
|
||||
var AbstractService = require('web.AbstractService');
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
|
||||
var AjaxService = AbstractService.extend({
|
||||
/**
|
||||
* @param {Object} libs - @see ajax.loadLibs
|
||||
* @param {Object} [context] - @see ajax.loadLibs
|
||||
* @param {Object} [tplRoute] - @see ajax.loadLibs
|
||||
*/
|
||||
loadLibs: function (libs, context, tplRoute) {
|
||||
return ajax.loadLibs(libs, context, tplRoute);
|
||||
},
|
||||
rpc: function (route, args, options, target) {
|
||||
var rpcPromise;
|
||||
var promise = new Promise(function (resolve, reject) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import * as AbstractService from "web.AbstractService";
|
||||
import legacyEnv from "web.env";
|
||||
import * as session from "web.session";
|
||||
import * as makeLegacyWebClientService from "web.pseudo_web_client";
|
||||
import { templates } from "@web/core/assets";
|
||||
|
||||
const { Component, whenReady } = owl;
|
||||
|
||||
@@ -51,6 +52,6 @@ export const legacySetupProm = new Promise((resolve) => {
|
||||
serviceRegistry.add(legacyServiceName, wowlToLegacyServiceMapper(legacyEnv));
|
||||
}
|
||||
await Promise.all([whenReady(), session.is_bound]);
|
||||
legacyEnv.templates = session.owlTemplates;
|
||||
legacyEnv.templates = templates;
|
||||
legacySetupResolver(legacyEnv);
|
||||
})();
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
getLocalState,
|
||||
searchModelStateToLegacy,
|
||||
} from "./backend_utils";
|
||||
import { getBundle, loadBundle } from "@web/core/assets";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { loadPublicAsset } from "@web/core/assets";
|
||||
import { LegacyComponent } from "./legacy_component";
|
||||
|
||||
const { xml, onWillStart } = owl;
|
||||
@@ -182,14 +182,7 @@ for (const [name, action] of Object.entries(legacyViewRegistry.entries())) {
|
||||
}
|
||||
legacyViewRegistry.onAdd(registerView);
|
||||
|
||||
export async function loadLegacyViews({ orm, rpc }) {
|
||||
if (!orm && rpc) {
|
||||
orm = {
|
||||
call: (...callArgs) => {
|
||||
const [model, method, args = [], kwargs = {}] = callArgs;
|
||||
return rpc({ model, method, args, kwargs });
|
||||
},
|
||||
};
|
||||
}
|
||||
await loadPublicAsset("web.assets_backend_legacy_lazy", orm);
|
||||
export async function loadLegacyViews() {
|
||||
const assets = await getBundle("web.assets_backend_legacy_lazy");
|
||||
await loadBundle(assets);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import { makeEnv, startServices } from "./env";
|
||||
import { legacySetupProm } from "./legacy/legacy_setup";
|
||||
import { mapLegacyEnvToWowlEnv } from "./legacy/utils";
|
||||
import { processTemplates, loadBundle } from "./core/assets";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { session } from "@web/session";
|
||||
import { renderToString } from "./core/utils/render";
|
||||
import { assets, templates } from "@web/core/assets";
|
||||
|
||||
const { App, whenReady } = owl;
|
||||
|
||||
@@ -29,25 +29,21 @@ export async function startWebClient(Webclient) {
|
||||
|
||||
// setup environment
|
||||
const env = makeEnv();
|
||||
const [, templates] = await Promise.all([
|
||||
startServices(env),
|
||||
odoo.loadTemplatesPromise.then(processTemplates),
|
||||
]);
|
||||
await startServices(env);
|
||||
|
||||
// start web client
|
||||
await whenReady();
|
||||
window.__OWL_TEMPLATES__ = templates;
|
||||
const legacyEnv = await legacySetupProm;
|
||||
mapLegacyEnvToWowlEnv(legacyEnv, env);
|
||||
const app = new App(Webclient, {
|
||||
env,
|
||||
templates,
|
||||
dev: env.debug,
|
||||
templates: window.__OWL_TEMPLATES__,
|
||||
translatableAttributes: ["data-tooltip"],
|
||||
translateFn: env._t,
|
||||
});
|
||||
renderToString.app = app;
|
||||
loadBundle.app = app;
|
||||
assets.defaultApp = app;
|
||||
const root = await app.mount(document.body);
|
||||
const classList = document.body.classList;
|
||||
if (localization.direction === "rtl") {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
triggerHotkey,
|
||||
} from "../helpers/utils";
|
||||
import { makeParent } from "./tooltip/tooltip_service_tests";
|
||||
import { templates } from "@web/core/assets";
|
||||
|
||||
const { App, Component, xml } = owl;
|
||||
const serviceRegistry = registry.category("services");
|
||||
@@ -392,7 +393,7 @@ QUnit.module("Components", ({ beforeEach }) => {
|
||||
env = await makeTestEnv();
|
||||
const app = new App(Parent, {
|
||||
env,
|
||||
templates: window.__OWL_TEMPLATES__,
|
||||
templates,
|
||||
test: true,
|
||||
});
|
||||
registerCleanup(() => app.destroy());
|
||||
|
||||
@@ -9,6 +9,7 @@ import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env";
|
||||
import { getFixture, nextTick, patchWithCleanup, triggerEvent } from "../../helpers/utils";
|
||||
import { registerCleanup } from "../../helpers/cleanup";
|
||||
import { makeFakeLocalizationService } from "../../helpers/mock_services";
|
||||
import { templates } from "@web/core/assets";
|
||||
|
||||
const { App, Component, useState, xml } = owl;
|
||||
|
||||
@@ -82,7 +83,7 @@ export async function makeParent(Child, options = {}) {
|
||||
const app = new App(Parent, {
|
||||
env,
|
||||
target,
|
||||
templates: window.__OWL_TEMPLATES__,
|
||||
templates,
|
||||
test: true,
|
||||
});
|
||||
registerCleanup(() => app.destroy());
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { loadJS, loadCSS, fetchAndProcessTemplates } from "@web/core/assets";
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { loadJS, loadCSS } from "@web/core/assets";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("Assets");
|
||||
@@ -30,22 +28,4 @@ QUnit.module("utils", () => {
|
||||
"Document contains a link with the href we asked to load"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("fetchAndProcessTemplates: load invalid bundle", function (assert) {
|
||||
let lastFetchedURL;
|
||||
patchWithCleanup(browser, {
|
||||
fetch: function (url) {
|
||||
lastFetchedURL = url;
|
||||
return Promise.reject(`Failed to load ressource at "${url}"`);
|
||||
},
|
||||
});
|
||||
assert.rejects(
|
||||
fetchAndProcessTemplates("web.some_invalid_bundle"),
|
||||
"Trying to load an invalid bundle rejects the promise"
|
||||
);
|
||||
assert.ok(
|
||||
/\/web\/webclient\/qweb\/.*\?bundle=web\.some_invalid_bundle$/.test(lastFetchedURL),
|
||||
"Loading a bundle calls the /web/webclient/qweb route with the corresponding bundle query parameter"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
serializeDate,
|
||||
serializeDateTime,
|
||||
} from "@web/core/l10n/dates";
|
||||
import { assets } from "@web/core/assets";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
@@ -2200,12 +2201,11 @@ export async function makeMockServer(serverData, mockRPC) {
|
||||
// simulates that we serialized the call to be passed in a real request
|
||||
args = JSON.parse(JSON.stringify(args));
|
||||
}
|
||||
const performRPC = (route, args) => mockServer.performRPC(route, args);
|
||||
if (mockRPC) {
|
||||
res = await mockRPC(route, args, performRPC);
|
||||
res = await mockRPC(route, args, mockServer.performRPC.bind(mockServer));
|
||||
}
|
||||
if (res === undefined) {
|
||||
res = await performRPC(route, args);
|
||||
res = await mockServer.performRPC(route, args);
|
||||
}
|
||||
return res;
|
||||
};
|
||||
@@ -2213,6 +2213,29 @@ export async function makeMockServer(serverData, mockRPC) {
|
||||
patchWithCleanup(browser, {
|
||||
fetch: makeMockFetch(_mockRPC),
|
||||
});
|
||||
if (mockRPC) {
|
||||
const { loadJS, loadCSS } = assets;
|
||||
patchWithCleanup(assets, {
|
||||
loadJS: async function (ressource) {
|
||||
let res = await mockRPC(ressource, {});
|
||||
if (res === undefined) {
|
||||
res = await loadJS(ressource);
|
||||
} else {
|
||||
console.log("%c[assets] fetch (mock) JS ressource " + ressource, "color: #66e; font-weight: bold;");
|
||||
}
|
||||
return res;
|
||||
},
|
||||
loadCSS: async function (ressource) {
|
||||
let res = await mockRPC(ressource, {});
|
||||
if (res === undefined) {
|
||||
res = await loadCSS(ressource);
|
||||
} else {
|
||||
console.log("%c[assets] fetch (mock) CSS ressource " + ressource, "color: #66e; font-weight: bold;");
|
||||
}
|
||||
return res;
|
||||
},
|
||||
});
|
||||
}
|
||||
// Replace RPC service
|
||||
serviceRegistry.add("rpc", rpcService, { force: true });
|
||||
return mockServer;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Deferred } from "@web/core/utils/concurrency";
|
||||
import { patch, unpatch } from "@web/core/utils/patch";
|
||||
import { isVisible } from "@web/core/utils/ui";
|
||||
import { registerCleanup } from "./cleanup";
|
||||
import { templates } from "@web/core/assets";
|
||||
|
||||
const { App, onMounted, onPatched, useComponent } = owl;
|
||||
|
||||
@@ -587,7 +588,7 @@ export async function mount(Comp, target, config = {}) {
|
||||
env = env || {};
|
||||
const configuration = {
|
||||
env,
|
||||
templates: window.__OWL_TEMPLATES__,
|
||||
templates,
|
||||
test: true,
|
||||
props,
|
||||
};
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
odoo.define('web.ajax_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
|
||||
QUnit.module('core', function () {
|
||||
|
||||
var test_css_url = '/test_assetsbundle/static/src/css/test_cssfile1.css';
|
||||
var test_link_selector = 'link[href="' + test_css_url + '"]';
|
||||
|
||||
QUnit.module('ajax', {
|
||||
beforeEach: function () {
|
||||
$(test_link_selector).remove();
|
||||
},
|
||||
afterEach: function () {
|
||||
$(test_link_selector).remove();
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('loadCSS', function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(2);
|
||||
ajax.loadCSS(test_css_url).then(function () {
|
||||
var $links = $(test_link_selector);
|
||||
assert.strictEqual($links.length, 1, "The css should be added to the dom.");
|
||||
ajax.loadCSS(test_css_url).then(function () {
|
||||
var $links = $(test_link_selector);
|
||||
assert.strictEqual($links.length, 1, "The css should have been added only once.");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -469,6 +469,7 @@ odoo.define('web.owl_dialog_tests', function (require) {
|
||||
`,
|
||||
"event,2,form": `<form><sheet><field name="display_name" /></sheet></form>`,
|
||||
},
|
||||
activateMockServer: true,
|
||||
};
|
||||
|
||||
const target = getFixture();
|
||||
|
||||
@@ -1955,6 +1955,14 @@ var MockServer = Class.extend({
|
||||
* @returns {any}
|
||||
*/
|
||||
_performFetch(resource, init) {
|
||||
if (resource.match(/\/static(\/\S+\/|\/)libs?/)) {
|
||||
// every lib must be includes into the test bundle.
|
||||
return true;
|
||||
}
|
||||
if (resource.match(/\/web\/bundle\/[^.]+\.[^.]+/)) {
|
||||
// every asset must be includes into the test bundle.
|
||||
return true;
|
||||
}
|
||||
throw new Error("Unimplemented resource: " + resource);
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@ odoo.define('web.test_env', async function (require) {
|
||||
const Bus = require('web.Bus');
|
||||
const session = require('web.session');
|
||||
const { makeTestEnvServices } = require('@web/../tests/legacy/helpers/test_services');
|
||||
|
||||
const { templates } = require("@web/core/assets");
|
||||
const { renderToString } = require('@web/core/utils/render');
|
||||
const { App, Component } = owl;
|
||||
|
||||
@@ -21,7 +21,7 @@ odoo.define('web.test_env', async function (require) {
|
||||
*/
|
||||
function makeTestEnvironment(env = {}, providedRPC = null) {
|
||||
if (!app) {
|
||||
app = new App(null, { templates: window.__OWL_TEMPLATES__, test: true });
|
||||
app = new App(null, { templates, test: true });
|
||||
renderToString.app = app;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ function makeTestEnvServices(env) {
|
||||
rpc() {
|
||||
return env.session.rpc(...arguments); // Compatibility Legacy Widgets
|
||||
},
|
||||
loadLibs() {}
|
||||
},
|
||||
rpc(params, options) {
|
||||
const query = buildQuery(params);
|
||||
|
||||
@@ -22,7 +22,9 @@ const makeTestEnvironment = require('web.test_env');
|
||||
const MockServer = require('web.MockServer');
|
||||
const RamStorage = require('web.RamStorage');
|
||||
const session = require('web.session');
|
||||
const { patchDate } = require("@web/../tests/helpers/utils");
|
||||
const { patchWithCleanup, patchDate } = require("@web/../tests/helpers/utils");
|
||||
const { browser } = require("@web/core/browser/browser");
|
||||
const { assets } = require("@web/core/assets");
|
||||
const { processArch } = require("@web/legacy/legacy_load_views");
|
||||
|
||||
const { Component } = owl;
|
||||
@@ -333,7 +335,7 @@ async function addMockEnvironmentOwl(Component, params, mockServer) {
|
||||
if (!mockServer) {
|
||||
let Server = MockServer;
|
||||
if (params.mockFetch) {
|
||||
Server = MockServer.extend({ _performFetch: params.mockFetch });
|
||||
Server = Server.extend({ _performFetch: params.mockFetch });
|
||||
}
|
||||
if (params.mockRPC) {
|
||||
Server = Server.extend({ _performRpc: params.mockRPC });
|
||||
@@ -346,6 +348,40 @@ async function addMockEnvironmentOwl(Component, params, mockServer) {
|
||||
});
|
||||
}
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
fetch: async (url, args) => {
|
||||
const result = await mockServer.performFetch(url, args || {});
|
||||
return {
|
||||
json: () => result,
|
||||
text: () => result,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
if (params.mockFetch) {
|
||||
const { loadJS, loadCSS } = assets;
|
||||
patchWithCleanup(assets, {
|
||||
loadJS: async function (ressource) {
|
||||
let res = await params.mockFetch(ressource, {});
|
||||
if (res === undefined) {
|
||||
res = await loadJS(ressource);
|
||||
} else {
|
||||
console.log("%c[assets] fetch (mock) JS ressource " + ressource, "color: #66e; font-weight: bold;");
|
||||
}
|
||||
return res;
|
||||
},
|
||||
loadCSS: async function (ressource) {
|
||||
let res = await params.mockFetch(ressource, {});
|
||||
if (res === undefined) {
|
||||
res = await loadCSS(ressource);
|
||||
} else {
|
||||
console.log("%c[assets] fetch (mock) CSS ressource " + ressource, "color: #66e; font-weight: bold;");
|
||||
}
|
||||
return res;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// remove the multi-click delay for the quick edit in form view
|
||||
const initialQuickEditDelay = FormController.prototype.multiClickTime;
|
||||
FormController.prototype.multiClickTime = params.formMultiClickTime || 0;
|
||||
|
||||
@@ -41,13 +41,6 @@ QUnit.module('LegacyViews', {
|
||||
var prom = testUtils.makeTestPromise();
|
||||
var loadJS = ajax.loadJS;
|
||||
const libraryLoadingStartedPromise = testUtils.makeTestPromise();
|
||||
ajax.loadJS = function (url) {
|
||||
libraryLoadingStartedPromise.resolve();
|
||||
assert.step(url);
|
||||
return prom.then(function () {
|
||||
assert.step(url + ' loaded');
|
||||
});
|
||||
};
|
||||
|
||||
var View = AbstractView.extend({
|
||||
jsLibs: [['a', 'b']],
|
||||
@@ -57,6 +50,14 @@ QUnit.module('LegacyViews', {
|
||||
arch: '<fake/>',
|
||||
data: this.data,
|
||||
model: 'fake_model',
|
||||
mockFetch: function (url) {
|
||||
libraryLoadingStartedPromise.resolve();
|
||||
assert.step(url);
|
||||
return prom.then(function () {
|
||||
assert.step(url + ' loaded');
|
||||
return true;
|
||||
});
|
||||
},
|
||||
}).then(function (view) {
|
||||
assert.verifySteps(['a loaded', 'b loaded'],
|
||||
"should wait for both libs to be loaded");
|
||||
@@ -81,13 +82,6 @@ QUnit.module('LegacyViews', {
|
||||
};
|
||||
var loadJS = ajax.loadJS;
|
||||
const libraryLoadingStartedPromise = testUtils.makeTestPromise();
|
||||
ajax.loadJS = function (url) {
|
||||
libraryLoadingStartedPromise.resolve();
|
||||
assert.step(url);
|
||||
return proms[url].then(function () {
|
||||
assert.step(url + ' loaded');
|
||||
});
|
||||
};
|
||||
|
||||
var View = AbstractView.extend({
|
||||
jsLibs: [
|
||||
@@ -100,6 +94,14 @@ QUnit.module('LegacyViews', {
|
||||
arch: '<fake/>',
|
||||
data: this.data,
|
||||
model: 'fake_model',
|
||||
mockFetch: function (url) {
|
||||
libraryLoadingStartedPromise.resolve();
|
||||
assert.step(url);
|
||||
return proms[url].then(function () {
|
||||
assert.step(url + ' loaded');
|
||||
return true;
|
||||
});
|
||||
},
|
||||
}).then(function (view) {
|
||||
assert.verifySteps(['c loaded'], "should wait for all libs to be loaded");
|
||||
ajax.loadJS = loadJS;
|
||||
|
||||
@@ -10277,12 +10277,11 @@ QUnit.module('LegacyViews', {
|
||||
arch: `<form>
|
||||
<widget name="pie_chart" title="qux by product" attrs="{'measure': 'qux', 'groupby': 'product_id'}"/>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "render_public_asset") {
|
||||
assert.deepEqual(args.args, ["web.assets_backend_legacy_lazy"]);
|
||||
mockFetch(route) {
|
||||
if (route.includes("/web/bundle/")) {
|
||||
assert.deepEqual(route, location.origin + "/web/bundle/web.assets_backend_legacy_lazy");
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
import core, { _t } from "web.core";
|
||||
import session from "web.session";
|
||||
import { assets, templates } from "@web/core/assets";
|
||||
import { browser, makeRAMLocalStorage } from "@web/core/browser/browser";
|
||||
import { patchTimeZone, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { nextTick, patchTimeZone, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { memoize } from "@web/core/utils/functions";
|
||||
import { legacyProm } from "web.test_legacy";
|
||||
import { registerCleanup } from "./helpers/cleanup";
|
||||
import { utils } from "./helpers/mock_env";
|
||||
@@ -15,8 +17,7 @@ import { loadLanguages } from "@web/core/l10n/translation";
|
||||
transitionConfig.disabled = true;
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { processTemplates } from "@web/core/assets";
|
||||
const { App, whenReady, loadFile } = owl;
|
||||
const { App, whenReady } = owl;
|
||||
|
||||
const { prepareRegistriesWithCleanup } = utils;
|
||||
|
||||
@@ -263,7 +264,7 @@ function patchSessionInfo() {
|
||||
* @param {Document} templates Document containing the templates to
|
||||
* process.
|
||||
*/
|
||||
function removeUnwantedAttrsFromTemplates(templates, attrs) {
|
||||
function removeUnwantedAttrsFromTemplates(attrs) {
|
||||
function replaceAttr(attrName, prefix, element) {
|
||||
const attrKey = `${prefix}${attrName}`;
|
||||
const attrValue = element.getAttribute(attrKey);
|
||||
@@ -280,6 +281,45 @@ function removeUnwantedAttrsFromTemplates(templates, attrs) {
|
||||
}
|
||||
}
|
||||
|
||||
// alt attribute causes issues with scroll tests. Indeed, alt is
|
||||
// displayed between the time we scroll programatically and the time
|
||||
// we assert for the scroll position. The src attribute is removed
|
||||
// as well to make sure images won't trigger a GET request on the
|
||||
// server.
|
||||
|
||||
// Clean up templates that have already been added.
|
||||
removeUnwantedAttrsFromTemplates(['alt', 'src']);
|
||||
|
||||
const { loadXML, getBundle, loadJS, loadCSS } = assets;
|
||||
patch(assets, 'TestAssetsLoadXML', {
|
||||
loadXML: function (templates) {
|
||||
console.log("%c[assets] fetch XML ressource", "color: #66e; font-weight: bold;");
|
||||
// Clean up new templates that might be added later.
|
||||
loadXML(templates);
|
||||
removeUnwantedAttrsFromTemplates(['alt', 'src']);
|
||||
},
|
||||
getBundle: memoize(async function (xmlID) {
|
||||
console.log("%c[assets] fetch libs from xmlID: " + xmlID, "color: #66e; font-weight: bold;");
|
||||
return getBundle(xmlID);
|
||||
}),
|
||||
loadJS: memoize(async function (ressource) {
|
||||
if (ressource.match(/\/static(\/\S+\/|\/)libs?/)) {
|
||||
console.log("%c[assets] fetch (mock) JS ressource: " + ressource, "color: #66e; font-weight: bold;");
|
||||
return nextTick();
|
||||
}
|
||||
console.log("%c[assets] fetch JS ressource: " + ressource, "color: #66e; font-weight: bold;");
|
||||
return loadJS(ressource);
|
||||
}),
|
||||
loadCSS: memoize(async function (ressource) {
|
||||
if (ressource.match(/\/static(\/\S+\/|\/)libs?/)) {
|
||||
console.log("%c[assets] fetch (mock) CSS ressource: " + ressource, "color: #66e; font-weight: bold;");
|
||||
return nextTick();
|
||||
}
|
||||
console.log("%c[assets] fetch CSS ressource: " + ressource, "color: #66e; font-weight: bold;");
|
||||
return loadCSS(ressource);
|
||||
}),
|
||||
});
|
||||
|
||||
export async function setupTests() {
|
||||
QUnit.testStart(() => {
|
||||
checkGlobalObjectsIntegrity();
|
||||
@@ -294,16 +334,6 @@ export async function setupTests() {
|
||||
patchSessionInfo();
|
||||
});
|
||||
|
||||
const templatesUrl = `/web/webclient/qweb/${new Date().getTime()}?bundle=web.assets_qweb`;
|
||||
const templates = await loadFile(templatesUrl);
|
||||
window.__OWL_TEMPLATES__ = processTemplates(templates);
|
||||
// alt attribute causes issues with scroll tests. Indeed, alt is
|
||||
// displayed between the time we scroll programatically and the time
|
||||
// we assert for the scroll position. The src attribute is removed
|
||||
// as well to make sure images won't trigger a GET request on the
|
||||
// server.
|
||||
removeUnwantedAttrsFromTemplates(window.__OWL_TEMPLATES__, ["alt", "src"]);
|
||||
session.owlTemplates = window.__OWL_TEMPLATES__;
|
||||
await Promise.all([whenReady(), legacyProm]);
|
||||
|
||||
// make sure images do not trigger a GET on the server
|
||||
|
||||
@@ -6,7 +6,6 @@ from . import test_image
|
||||
from . import test_ir_model
|
||||
from . import test_js
|
||||
from . import test_menu
|
||||
from . import test_serving_base
|
||||
from . import test_click_everywhere
|
||||
from . import test_base_document_layout
|
||||
from . import test_load_menus
|
||||
@@ -14,4 +13,5 @@ from . import test_profiler
|
||||
from . import test_session_info
|
||||
from . import test_read_progress_bar
|
||||
from . import test_assets
|
||||
from . import test_assets_xml
|
||||
from . import test_login
|
||||
|
||||
@@ -39,20 +39,19 @@ class TestAssetsGenerateTimeCommon(odoo.tests.TransactionCase):
|
||||
_logger.info('Error detected while generating bundle %r %s', bundle, assets_type)
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
@odoo.tests.tagged('post_install', '-at_install', 'assets_bundle')
|
||||
class TestLogsAssetsGenerateTime(TestAssetsGenerateTimeCommon):
|
||||
|
||||
def test_logs_assets_generate_time(self):
|
||||
"""
|
||||
The purpose of this test is to monitor the time of assets bundle generation.
|
||||
This is not meant to test the generation failure, hence the try/except and the mute logger.
|
||||
For example, 'web.assets_qweb' is contains only static xml.
|
||||
"""
|
||||
for bundle, duration in self.generate_bundles():
|
||||
_logger.info('Bundle %r generated in %.2fs', bundle, duration)
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install', '-standard', 'bundle_generation')
|
||||
@odoo.tests.tagged('post_install', '-at_install', '-standard', 'assets_bundle')
|
||||
class TestAssetsGenerateTime(TestAssetsGenerateTimeCommon):
|
||||
"""
|
||||
This test is meant to be run nightly to ensure bundle generation does not exceed
|
||||
|
||||
@@ -8,57 +8,23 @@ from datetime import datetime
|
||||
from lxml import etree
|
||||
import logging
|
||||
|
||||
import odoo
|
||||
from odoo.tests.common import BaseCase, HttpCase, tagged
|
||||
from odoo.tools import topological_sort
|
||||
from odoo.addons.web.controllers.utils import HomeStaticTemplateHelpers
|
||||
from odoo.addons.base.models.assetsbundle import AssetsBundle, WebAsset
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
def sample(population):
|
||||
return random.sample(
|
||||
population,
|
||||
random.randint(0, min(len(population), 5)))
|
||||
|
||||
|
||||
class TestModulesLoading(BaseCase):
|
||||
def setUp(self):
|
||||
self.mods = [str(i) for i in range(1000)]
|
||||
|
||||
def test_topological_sort(self):
|
||||
random.shuffle(self.mods)
|
||||
modules = [
|
||||
(k, sample(self.mods[:i]))
|
||||
for i, k in enumerate(self.mods)]
|
||||
random.shuffle(modules)
|
||||
ms = dict(modules)
|
||||
|
||||
seen = set()
|
||||
sorted_modules = topological_sort(ms)
|
||||
for module in sorted_modules:
|
||||
deps = ms[module]
|
||||
self.assertGreaterEqual(
|
||||
seen, set(deps),
|
||||
'Module %s (index %d), ' \
|
||||
'missing dependencies %s from loaded modules %s' % (
|
||||
module, sorted_modules.index(module), deps, seen
|
||||
))
|
||||
seen.add(module)
|
||||
|
||||
|
||||
class TestStaticInheritanceCommon(BaseCase):
|
||||
|
||||
class TestStaticInheritanceCommon(odoo.tests.TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# output is "manifest_glob" return
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
('module_2_file_1', 'module_2', 'bundle_1'),
|
||||
]
|
||||
|
||||
self.template_files = {
|
||||
'module_1_file_1': b"""
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<span>Ho !</span>
|
||||
<div>At first I was afraid</div>
|
||||
<div>Kept thinking I could never live without you by my side</div>
|
||||
</form>
|
||||
@@ -66,14 +32,16 @@ class TestStaticInheritanceCommon(BaseCase):
|
||||
<div>And I grew strong</div>
|
||||
</t>
|
||||
</templates>
|
||||
""",
|
||||
|
||||
'module_2_file_1': b"""
|
||||
""",
|
||||
'/module_2/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_2_1" t-inherit="module_1.template_1_1" t-inherit-mode="primary">
|
||||
<xpath expr="//div[1]" position="after">
|
||||
<div>I was petrified</div>
|
||||
</xpath>
|
||||
<xpath expr="//span" position="attributes">
|
||||
<attribute name="type">Scary screams</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[2]" position="after">
|
||||
<div>But then I spent so many nights thinking how you did me wrong</div>
|
||||
</xpath>
|
||||
@@ -87,58 +55,82 @@ class TestStaticInheritanceCommon(BaseCase):
|
||||
</xpath>
|
||||
</form>
|
||||
</templates>
|
||||
""",
|
||||
""",
|
||||
}
|
||||
self._set_patchers()
|
||||
self._toggle_patchers('start')
|
||||
self._reg_replace_ws = r"\s|\t"
|
||||
self._patch = patch.object(WebAsset, '_fetch_content', lambda asset: self.template_files[asset.url])
|
||||
self._patch.start()
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self._toggle_patchers('stop')
|
||||
self._patch.stop()
|
||||
|
||||
def renderBundle(self, debug=False):
|
||||
files = []
|
||||
for url in self.template_files:
|
||||
atype = 'text/xml'
|
||||
if '.js' in url:
|
||||
atype = 'text/javascript'
|
||||
files.append({
|
||||
'atype': atype,
|
||||
'url': url,
|
||||
'filename': url,
|
||||
'content': None,
|
||||
'media': None,
|
||||
})
|
||||
asset = AssetsBundle('web.test_bundle', files, env=self.env, css=False, js=True)
|
||||
# to_node return the files descriptions and generate attachments.
|
||||
asset.to_node(css=False, js=False, debug=debug and 'assets' or '')
|
||||
content = asset.xml(show_inherit_info=debug)
|
||||
return f'<templates xml:space="preserve">\n{content}\n</templates>'
|
||||
|
||||
# Custom Assert
|
||||
def assertXMLEqual(self, output, expected):
|
||||
self.assertTrue(output)
|
||||
self.assertTrue(expected)
|
||||
output = textwrap.dedent(output.decode('UTF-8')).strip()
|
||||
output = re.sub(self._reg_replace_ws, '', output)
|
||||
self.assertEqual(etree.fromstring(output), etree.fromstring(expected))
|
||||
|
||||
expected = textwrap.dedent(expected.decode('UTF-8')).strip()
|
||||
expected = re.sub(self._reg_replace_ws, '', expected)
|
||||
self.assertEqual(output, expected)
|
||||
|
||||
# Private methods
|
||||
def _get_module_names(self):
|
||||
return ','.join([asset_path[1] for asset_path in self.asset_paths])
|
||||
|
||||
def _set_patchers(self):
|
||||
def _patched_for_get_asset_paths(*args, **kwargs):
|
||||
# Ordered by module
|
||||
return self.asset_paths
|
||||
|
||||
def _patch_for_read_addon_file(*args, **kwargs):
|
||||
return self.template_files[args[1]]
|
||||
|
||||
self.patchers = [
|
||||
patch.object(HomeStaticTemplateHelpers, '_get_asset_paths', _patched_for_get_asset_paths),
|
||||
patch.object(HomeStaticTemplateHelpers, '_read_addon_file', _patch_for_read_addon_file),
|
||||
]
|
||||
|
||||
def _toggle_patchers(self, mode):
|
||||
self.assertTrue(mode in ('start', 'stop'))
|
||||
for p in self.patchers:
|
||||
getattr(p, mode)()
|
||||
|
||||
|
||||
@tagged('static_templates')
|
||||
@tagged('assets_bundle', 'static_templates')
|
||||
class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
# Actual test cases
|
||||
def test_static_inheritance_01(self):
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
def test_static_with_debug_mode(self):
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!-- Filepath: /module_1/static/xml/file_1.xml -->
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<span>Ho !</span>
|
||||
<div>At first I was afraid</div>
|
||||
<div>Kept thinking I could never live without you by my side</div>
|
||||
</form>
|
||||
|
||||
<!-- Filepath: /module_1/static/xml/file_1.xml => /module_2/static/xml/file_1.xml -->
|
||||
<t t-name="template_1_2">
|
||||
<div>And I grew strong</div>
|
||||
<!-- Filepath: /module_2/static/xml/file_1.xml ; position="after" ; {'expr': '//div[1]'} --><div>And I learned how to get along</div>
|
||||
</t>
|
||||
|
||||
<!-- Filepath: /module_1/static/xml/file_1.xml => /module_2/static/xml/file_1.xml -->
|
||||
<form t-name="template_2_1" random-attr="gloria"><!-- Filepath: /module_2/static/xml/file_1.xml ; position="attributes" ; {'expr': '//span'} -->
|
||||
<span type="Scary screams">Ho !</span>
|
||||
<div>At first I was afraid</div>
|
||||
<!-- Filepath: /module_2/static/xml/file_1.xml ; position="after" ; {'expr': '//div[1]'} --><div>I was petrified</div>
|
||||
<!-- Filepath: /module_2/static/xml/file_1.xml ; position="after" ; {'expr': '//div[2]'} --><div>But then I spent so many nights thinking how you did me wrong</div>
|
||||
<div>Kept thinking I could never live without you by my side</div>
|
||||
</form>
|
||||
|
||||
<!-- Filepath: /module_2/static/xml/file_1.xml -->
|
||||
<div t-name="template_2_2">
|
||||
<div>And I learned how to get along</div>
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
self.assertXMLEqual(self.renderBundle(debug=True), expected)
|
||||
|
||||
def test_static_inheritance_01(self):
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<span>Ho !</span>
|
||||
<div>At first I was afraid</div>
|
||||
<div>Kept thinking I could never live without you by my side</div>
|
||||
</form>
|
||||
@@ -147,6 +139,7 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
<div>And I learned how to get along</div>
|
||||
</t>
|
||||
<form t-name="template_2_1" random-attr="gloria">
|
||||
<span type="Scary screams">Ho !</span>
|
||||
<div>At first I was afraid</div>
|
||||
<div>I was petrified</div>
|
||||
<div>But then I spent so many nights thinking how you did me wrong</div>
|
||||
@@ -157,12 +150,11 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_static_inheritance_02(self):
|
||||
self.template_files = {
|
||||
'module_1_file_1': b'''
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -174,14 +166,10 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</xpath>
|
||||
</form>
|
||||
</templates>
|
||||
'''
|
||||
""",
|
||||
}
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
]
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
<div>Kept thinking I could never live without you by my side</div>
|
||||
@@ -193,13 +181,11 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</form>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_static_inheritance_03(self):
|
||||
self.maxDiff = None
|
||||
self.template_files = {
|
||||
'module_1_file_1': b'''
|
||||
'/module_1/static/xml/file_1.xml': '''
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -216,12 +202,8 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
'''
|
||||
}
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
]
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="template_1_1">
|
||||
<div>At first I was afraid</div>
|
||||
<div>Kept thinking I could never live without you by my side</div>
|
||||
@@ -237,17 +219,11 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</form>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_static_inheritance_in_same_module(self):
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
('module_1_file_2', 'module_1', 'bundle_1'),
|
||||
]
|
||||
|
||||
self.template_files = {
|
||||
'module_1_file_1': b'''
|
||||
'/module_1/static/xml/file_1.xml': '''
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -256,7 +232,7 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
''',
|
||||
|
||||
'module_1_file_2': b'''
|
||||
'/module_1/static/xml/file_2.xml': '''
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_2" t-inherit="template_1_1" t-inherit-mode="primary">
|
||||
<xpath expr="//div[1]" position="after">
|
||||
@@ -266,9 +242,8 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
'''
|
||||
}
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="template_1_1">
|
||||
<div>At first I was afraid</div>
|
||||
<div>Kept thinking I could never live without you by my side</div>
|
||||
@@ -280,16 +255,11 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</form>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_static_inheritance_in_same_file(self):
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
]
|
||||
|
||||
self.template_files = {
|
||||
'module_1_file_1': b'''
|
||||
'/module_1/static/xml/file_1.xml': '''
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -303,9 +273,8 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
''',
|
||||
}
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="template_1_1">
|
||||
<div>At first I was afraid</div>
|
||||
<div>Kept thinking I could never live without you by my side</div>
|
||||
@@ -317,15 +286,11 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</form>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_static_inherit_extended_template(self):
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
]
|
||||
self.template_files = {
|
||||
'module_1_file_1': b'''
|
||||
'/module_1/static/xml/file_1.xml': '''
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -344,9 +309,8 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
''',
|
||||
}
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="template_1_1">
|
||||
<div>At first I was afraid</div>
|
||||
<div>I was petrified</div>
|
||||
@@ -360,17 +324,11 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</form>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_sibling_extension(self):
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
('module_2_file_1', 'module_2', 'bundle_1'),
|
||||
('module_3_file_1', 'module_3', 'bundle_1'),
|
||||
]
|
||||
self.template_files = {
|
||||
'module_1_file_1': b'''
|
||||
'/module_1/static/xml/file_1.xml': '''
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1">
|
||||
<div>I am a man of constant sorrow</div>
|
||||
@@ -379,7 +337,7 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
''',
|
||||
|
||||
'module_2_file_1': b'''
|
||||
'/module_2/static/xml/file_1.xml': '''
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_2_1" t-inherit="module_1.template_1_1" t-inherit-mode="extension">
|
||||
<xpath expr="//div[1]" position="after">
|
||||
@@ -389,7 +347,7 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
''',
|
||||
|
||||
'module_3_file_1': b'''
|
||||
'/module_3/static/xml/file_1.xml': '''
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_3_1" t-inherit="module_1.template_1_1" t-inherit-mode="extension">
|
||||
<xpath expr="//div[2]" position="after">
|
||||
@@ -399,10 +357,8 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
'''
|
||||
}
|
||||
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="template_1_1">
|
||||
<div>I am a man of constant sorrow</div>
|
||||
<div>In constant sorrow all through his days</div>
|
||||
@@ -411,21 +367,24 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</form>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_static_misordered_modules(self):
|
||||
self.asset_paths.reverse()
|
||||
files = self.template_files
|
||||
self.template_files = {
|
||||
'/module_2/static/xml/file_1.xml': files['/module_2/static/xml/file_1.xml'],
|
||||
'/module_1/static/xml/file_1.xml': files['/module_1/static/xml/file_1.xml'],
|
||||
}
|
||||
with self.assertRaises(ValueError) as ve:
|
||||
HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
self.renderBundle(debug=False)
|
||||
|
||||
self.assertEqual(
|
||||
str(ve.exception),
|
||||
'Module module_1 not loaded or inexistent, or templates of addon being loaded (module_2) are misordered'
|
||||
"Module 'module_1' not loaded or inexistent (try to inherit 'template_1_1'), or templates of addon being loaded 'module_2' are misordered (template 'template_2_1')"
|
||||
)
|
||||
|
||||
def test_static_misordered_templates(self):
|
||||
self.template_files['module_2_file_1'] = b"""
|
||||
self.template_files['/module_2/static/xml/file_1.xml'] = """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_2_1" t-inherit="module_2.template_2_2" t-inherit-mode="primary">
|
||||
<xpath expr="//div[1]" position="after">
|
||||
@@ -438,22 +397,19 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
"""
|
||||
with self.assertRaises(ValueError) as ve:
|
||||
HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
self.renderBundle(debug=False)
|
||||
|
||||
self.assertEqual(
|
||||
str(ve.exception),
|
||||
'No template found to inherit from. Module module_2 and template name template_2_2'
|
||||
"Cannot create 'module_2.template_2_1' because the template to inherit 'module_2.template_2_2' is not found.",
|
||||
)
|
||||
|
||||
def test_replace_in_debug_mode(self):
|
||||
"""
|
||||
Replacing a template's meta definition in place doesn't keep the original attrs of the template
|
||||
"""
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
]
|
||||
self.template_files = {
|
||||
'module_1_file_1': b"""
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -466,24 +422,18 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
""",
|
||||
}
|
||||
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<div overriden-attr="overriden" t-name="template_1_1">
|
||||
And I grew strong
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_replace_in_debug_mode2(self):
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
]
|
||||
self.template_files = {
|
||||
'module_1_file_1': b"""
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -500,10 +450,8 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
""",
|
||||
}
|
||||
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<div t-name="template_1_1">
|
||||
And I grew strong
|
||||
<p>And I learned how to get along</p>
|
||||
@@ -511,19 +459,15 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_replace_in_debug_mode3(self):
|
||||
"""Text outside of a div which will replace a whole template
|
||||
becomes outside of the template
|
||||
This doesn't mean anything in terms of the business of template inheritance
|
||||
But it is in the XPATH specs"""
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
]
|
||||
self.template_files = {
|
||||
'module_1_file_1': b"""
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -540,10 +484,8 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
""",
|
||||
}
|
||||
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<div t-name="template_1_1">
|
||||
And I grew strong
|
||||
<p>And I learned how to get along</p>
|
||||
@@ -551,18 +493,14 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
And so you're back
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_replace_root_node_tag(self):
|
||||
"""
|
||||
Root node IS targeted by //NODE_TAG in xpath
|
||||
"""
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
]
|
||||
self.template_files = {
|
||||
'module_1_file_1': b"""
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -578,28 +516,21 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
""",
|
||||
}
|
||||
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<div t-name="template_1_1">
|
||||
Form replacer
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_replace_root_node_tag_in_primary(self):
|
||||
"""
|
||||
Root node IS targeted by //NODE_TAG in xpath
|
||||
"""
|
||||
self.maxDiff = None
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
]
|
||||
self.template_files = {
|
||||
'module_1_file_1': b"""
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -613,10 +544,8 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
""",
|
||||
}
|
||||
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
<form>Inner Form</form>
|
||||
@@ -626,19 +555,15 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_inherit_primary_replace_debug(self):
|
||||
"""
|
||||
The inheriting template has got both its own defining attrs
|
||||
and new ones if one is to replace its defining root node
|
||||
"""
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
]
|
||||
self.template_files = {
|
||||
'module_1_file_1': b"""
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -654,10 +579,8 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
""",
|
||||
}
|
||||
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
</form>
|
||||
@@ -667,16 +590,12 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_replace_in_nodebug_mode1(self):
|
||||
"""Comments already in the arch are ignored"""
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
]
|
||||
self.template_files = {
|
||||
'module_1_file_1': b"""
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -694,10 +613,8 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
""",
|
||||
}
|
||||
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=False)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<div t-name="template_1_1">
|
||||
And I grew strong
|
||||
<p>And I learned how to get along</p>
|
||||
@@ -705,15 +622,11 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_inherit_from_dotted_tname_1(self):
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
]
|
||||
self.template_files = {
|
||||
'module_1_file_1': b"""
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="module_1.template_1_1.dot" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -729,10 +642,8 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
""",
|
||||
}
|
||||
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="module_1.template_1_1.dot" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
</form>
|
||||
@@ -742,15 +653,11 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_inherit_from_dotted_tname_2(self):
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
]
|
||||
self.template_files = {
|
||||
'module_1_file_1': b"""
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1.dot" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -766,10 +673,8 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
""",
|
||||
}
|
||||
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="template_1_1.dot" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
</form>
|
||||
@@ -779,15 +684,11 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_inherit_from_dotted_tname_2bis(self):
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
]
|
||||
self.template_files = {
|
||||
'module_1_file_1': b"""
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1.dot" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -803,10 +704,8 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
""",
|
||||
}
|
||||
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="template_1_1.dot" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
</form>
|
||||
@@ -816,15 +715,11 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_inherit_from_dotted_tname_2ter(self):
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
]
|
||||
self.template_files = {
|
||||
'module_1_file_1': b"""
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="module_1" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -840,10 +735,8 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
""",
|
||||
}
|
||||
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="module_1" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
</form>
|
||||
@@ -853,16 +746,11 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_inherit_from_dotted_tname_3(self):
|
||||
self.asset_paths = [
|
||||
('module_1_file_1', 'module_1', 'bundle_1'),
|
||||
('module_2_file_1', 'module_2', 'bundle_1'),
|
||||
]
|
||||
self.template_files = {
|
||||
'module_1_file_1': b"""
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="module_1.template_1_1.dot" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
@@ -870,7 +758,7 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
""",
|
||||
|
||||
'module_2_file_1': b"""
|
||||
'/module_2/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="template_2_1" t-inherit="module_1.template_1_1.dot" t-inherit-mode="primary">
|
||||
<xpath expr="." position="replace">
|
||||
@@ -883,10 +771,8 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</templates>
|
||||
"""
|
||||
}
|
||||
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
expected = b"""
|
||||
<templates>
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="module_1.template_1_1.dot" random-attr="gloria">
|
||||
<div>At first I was afraid</div>
|
||||
</form>
|
||||
@@ -896,40 +782,68 @@ class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
self.assertXMLEqual(contents, expected)
|
||||
|
||||
@tagged('static_templates')
|
||||
class TestHttpStaticInheritance(HttpCase):
|
||||
def test_static_attachments(self):
|
||||
url = '/test_module/test_file.xml'
|
||||
self.env['ir.attachment'].create({
|
||||
'name': 'test_attachment',
|
||||
'url': url,
|
||||
'res_model': 'ir.ui.view',
|
||||
'type': 'binary',
|
||||
'raw': b"""
|
||||
<templates>
|
||||
<t t-name="test_template">
|
||||
<div class="test_div" />
|
||||
def test_inherit_and_qweb_extend(self):
|
||||
self.template_files['/module_1/static/xml/file_2.xml'] = """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="template_qw_1">
|
||||
<div>111</div>
|
||||
</t>
|
||||
<form t-inherit="template_1_1" t-inherit-mode="extension">
|
||||
<xpath expr="//span[1]" position="replace">
|
||||
<article>!!!</article>
|
||||
</xpath>
|
||||
</form>
|
||||
<t t-name="template_qw_2">
|
||||
<div>222</div>
|
||||
</t>
|
||||
<t t-extend="template_qw_1">
|
||||
<t t-jquery="div" t-operation="after">
|
||||
<div>333</div>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
"""
|
||||
})
|
||||
self.env['ir.asset'].create({
|
||||
'name': 'test_asset',
|
||||
'path': url,
|
||||
'bundle': 'test.bundle',
|
||||
})
|
||||
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<article>!!!</article>
|
||||
<div>At first I was afraid</div>
|
||||
<div>Kept thinking I could never live without you by my side</div>
|
||||
</form>
|
||||
<t t-name="template_1_2">
|
||||
<div>And I grew strong</div>
|
||||
<div>And I learned how to get along</div>
|
||||
</t>
|
||||
<t t-name="template_qw_1">
|
||||
<div>111</div>
|
||||
</t>
|
||||
<t t-name="template_qw_2">
|
||||
<div>222</div>
|
||||
</t>
|
||||
<t t-extend="template_qw_1">
|
||||
<t t-jquery="div" t-operation="after">
|
||||
<div>333</div>
|
||||
</t>
|
||||
</t>
|
||||
<form t-name="template_2_1" random-attr="gloria">
|
||||
<span type="Scary screams">Ho !</span>
|
||||
<div>At first I was afraid</div>
|
||||
<div>I was petrified</div>
|
||||
<div>But then I spent so many nights thinking how you did me wrong</div>
|
||||
<div>Kept thinking I could never live without you by my side</div>
|
||||
</form>
|
||||
<div t-name="template_2_2">
|
||||
<div>And I learned how to get along</div>
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
|
||||
res = self.url_open('/web/webclient/qweb/HASH_BIDON?bundle=test.bundle')
|
||||
[template] = etree.fromstring(res.text)
|
||||
|
||||
self.assertEqual(template.get('t-name'), 'test_template')
|
||||
self.assertEqual(template[0].get('class'), 'test_div')
|
||||
|
||||
@tagged('-standard', 'static_templates_performance')
|
||||
@tagged('-standard', 'assets_bundle', 'static_templates_performance')
|
||||
class TestStaticInheritancePerformance(TestStaticInheritanceCommon):
|
||||
def _sick_script(self, nMod, nFilePerMod, nTemplatePerFile, stepInheritInModule=2, stepInheritPreviousModule=3):
|
||||
"""
|
||||
@@ -943,7 +857,7 @@ class TestStaticInheritancePerformance(TestStaticInheritanceCommon):
|
||||
for m in range(nMod):
|
||||
for f in range(nFilePerMod):
|
||||
mname = 'mod_%s' % m
|
||||
fname = 'mod_%s_file_%s' % (m, f)
|
||||
fname = 'mod_%s/folder/file_%s.xml' % (m, f)
|
||||
self.asset_paths.append((fname, mname, 'bundle_1'))
|
||||
|
||||
_file = '<templates id="template" xml:space="preserve">'
|
||||
@@ -991,7 +905,7 @@ class TestStaticInheritancePerformance(TestStaticInheritanceCommon):
|
||||
}
|
||||
_file += '</templates>'
|
||||
|
||||
self.template_files[fname] = _file.encode()
|
||||
self.template_files[fname] = _file
|
||||
self.assertEqual(number_templates, nMod * nFilePerMod * nTemplatePerFile)
|
||||
|
||||
def test_static_templates_treatment_linearity(self):
|
||||
@@ -1000,7 +914,7 @@ class TestStaticInheritancePerformance(TestStaticInheritanceCommon):
|
||||
self._sick_script(nMod, nFilePerMod, nTemplatePerFile)
|
||||
|
||||
before = datetime.now()
|
||||
contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
contents = self.renderBundle(debug=False)
|
||||
after = datetime.now()
|
||||
delta2500 = after - before
|
||||
_logger.runbot('Static Templates Inheritance: 2500 templates treated in %s seconds' % delta2500.total_seconds())
|
||||
@@ -1013,7 +927,7 @@ class TestStaticInheritancePerformance(TestStaticInheritanceCommon):
|
||||
self._sick_script(nMod, nFilePerMod, nTemplatePerFile)
|
||||
|
||||
before = datetime.now()
|
||||
HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
|
||||
self.renderBundle(debug=False)
|
||||
after = datetime.now()
|
||||
delta25000 = after - before
|
||||
|
||||
@@ -297,7 +297,6 @@
|
||||
odoo.__session_info__ = <t t-out="json.dumps(session_info)"/>;
|
||||
odoo.reloadMenus = () => fetch(`/web/webclient/load_menus/${odoo.__session_info__.cache_hashes.load_menus}`).then(res => res.json());
|
||||
odoo.loadMenusPromise = odoo.reloadMenus();
|
||||
odoo.loadTemplatesPromise = fetch(`/web/webclient/qweb/${odoo.__session_info__.cache_hashes.qweb}?bundle=web.assets_qweb`).then(doc => doc.text());
|
||||
// Block to avoid leaking variables in the script scope
|
||||
{
|
||||
const { user_context, cache_hashes } = odoo.__session_info__;
|
||||
|
||||
@@ -494,21 +494,6 @@ class Web_Editor(http.Controller):
|
||||
|
||||
return files_data_by_bundle
|
||||
|
||||
@http.route("/web_editor/public_render_template", type="json", auth="public", website=True)
|
||||
def public_render_template(self, args, kwargs): # pylint: disable=unused-argument
|
||||
# args[0]: xml id of the template to render
|
||||
# args[1]: optional dict of rendering values, only trusted keys are supported
|
||||
len_args = len(args)
|
||||
assert len_args >= 1 and len_args <= 2, 'Need a xmlID and potential rendering values to render a template'
|
||||
|
||||
trusted_value_keys = ('debug',)
|
||||
|
||||
xmlid = args[0]
|
||||
values = len_args > 1 and args[1] or {}
|
||||
|
||||
View = request.env['ir.ui.view']
|
||||
return View.render_public_asset(xmlid, {k: values[k] for k in values if k in trusted_value_keys})
|
||||
|
||||
@http.route('/web_editor/modify_image/<model("ir.attachment"):attachment>', type="json", auth="user", website=True)
|
||||
def modify_image(self, attachment, res_model=None, res_id=None, name=None, data=None, original_id=None, mimetype=None):
|
||||
"""
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
<t t-call-assets="web_editor.assets_wysiwyg" t-css="false" />
|
||||
</template>
|
||||
|
||||
<template id="compiled_assets_wysiwyg" name="Wysiwyg Editor" groups="base.group_public,base.group_portal,base.group_user">
|
||||
<t t-call-assets="web_editor.assets_wysiwyg"/>
|
||||
</template>
|
||||
|
||||
<template id="assets_edit_html_field" name="Wysiwyg Editor Assets for html field (style-inline)" groups="base.group_user,base.group_portal">
|
||||
<t t-call-assets="web.assets_common" t-js="false"/>
|
||||
<t t-call-assets="web_editor.assets_wysiwyg" t-js="false"/>
|
||||
|
||||
@@ -17,9 +17,6 @@ Odoo Web tours.
|
||||
'views/tour_views.xml'
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_qweb': [
|
||||
'web_tour/static/src/debug/tour_dialog_component.xml',
|
||||
],
|
||||
'web.assets_common': [
|
||||
'web_tour/static/src/scss/**/*',
|
||||
'web_tour/static/src/js/running_tour_action_helper.js',
|
||||
@@ -28,11 +25,13 @@ Odoo Web tours.
|
||||
'web_tour/static/src/js/tour_service.js',
|
||||
'web_tour/static/src/js/tour_step_utils.js',
|
||||
'web_tour/static/src/js/tour_utils.js',
|
||||
'/web_tour/static/src/xml/tip.xml',
|
||||
],
|
||||
'web.assets_backend': [
|
||||
'web_tour/static/src/debug/debug_manager.js',
|
||||
'web_tour/static/src/debug/tour_dialog_component.js',
|
||||
'web_tour/static/src/services/*.js',
|
||||
'web_tour/static/src/debug/tour_dialog_component.xml',
|
||||
],
|
||||
'web.assets_frontend': [
|
||||
'web_tour/static/src/js/public/**/*',
|
||||
|
||||
@@ -8,7 +8,6 @@ var _t = core._t;
|
||||
|
||||
var Tip = Widget.extend({
|
||||
template: "Tip",
|
||||
xmlDependencies: ['/web_tour/static/src/xml/tip.xml'],
|
||||
events: {
|
||||
click: '_onTipClicked',
|
||||
mouseenter: '_onMouseEnter',
|
||||
|
||||
@@ -7,12 +7,6 @@ odoo.define('web_tour.tour_manager_tests', async function (require) {
|
||||
const testUtils = require('web.test_utils');
|
||||
const createView = testUtils.createView;
|
||||
|
||||
const ajax = require('web.ajax');
|
||||
const { qweb } = require('web.core');
|
||||
|
||||
// Pre-load the Tip widget template
|
||||
await ajax.loadXML('/web_tour/static/src/xml/tip.xml', qweb);
|
||||
|
||||
/**
|
||||
* Create a widget and a TourManager instance with a list of given Tour objects.
|
||||
* @see `TourManager.register()` for more details on the Tours registry system.
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
from . import backend
|
||||
from . import form
|
||||
from . import main
|
||||
from . import webclient
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.web.controllers.webclient import WebClient
|
||||
|
||||
|
||||
class WebsiteWebClient(WebClient):
|
||||
@http.route()
|
||||
def bundle(self, bundle_name, **bundle_params):
|
||||
if 'website_id' in bundle_params:
|
||||
request.update_context(website_id=int(bundle_params['website_id']))
|
||||
return super().bundle(bundle_name, **bundle_params)
|
||||
@@ -377,6 +377,7 @@ class Http(models.AbstractModel):
|
||||
'website_id': request.website.id,
|
||||
'website_company_id': request.website._get_cached('company_id'),
|
||||
})
|
||||
session_info['bundle_params']['website_id'] = request.website.id
|
||||
return session_info
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export default createPublicRoot(WebsiteRoot).then(rootInstance => {
|
||||
// This data attribute is set by the WebsitePreview client action for a
|
||||
// restricted editor user.
|
||||
if (window.frameElement && window.frameElement.dataset.loadWysiwyg === 'true') {
|
||||
loadWysiwyg(['website.compiled_assets_wysiwyg']).then(() => {
|
||||
loadWysiwyg(['website.assets_wysiwyg']).then(() => {
|
||||
window.dispatchEvent(new CustomEvent('PUBLIC-ROOT-READY', {detail: {rootInstance}}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="website.compiled_assets_wysiwyg" name="Website Editor Assets (used in website editor)" groups="website.group_website_restricted_editor">
|
||||
<t t-call-assets="website.assets_wysiwyg"/>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -1,9 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from contextlib import closing
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from lxml import etree
|
||||
from subprocess import Popen, PIPE
|
||||
import base64
|
||||
import copy
|
||||
import hashlib
|
||||
import io
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
@@ -20,16 +24,19 @@ except ImportError:
|
||||
# `sassc` executable in the path.
|
||||
libsass = None
|
||||
|
||||
from odoo import release, SUPERUSER_ID
|
||||
from odoo import release, SUPERUSER_ID, _
|
||||
from odoo.http import request
|
||||
from odoo.modules.module import get_resource_path
|
||||
from odoo.tools import func, misc, transpile_javascript, is_odoo_module, SourceMapGenerator, profiler
|
||||
from odoo.tools import (func, misc, transpile_javascript,
|
||||
is_odoo_module, SourceMapGenerator, profiler,
|
||||
apply_inheritance_specs)
|
||||
from odoo.tools.misc import file_open, html_escape as escape
|
||||
from odoo.tools.pycompat import to_text
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
EXTENSIONS = (".js", ".css", ".scss", ".sass", ".less")
|
||||
EXTENSIONS = (".js", ".css", ".scss", ".sass", ".less", ".xml")
|
||||
|
||||
|
||||
class CompileError(RuntimeError): pass
|
||||
def rjsmin(script):
|
||||
@@ -117,12 +124,14 @@ class AssetsBundle(object):
|
||||
self.name = name
|
||||
self.env = request.env if env is None else env
|
||||
self.javascripts = []
|
||||
self.templates = []
|
||||
self.stylesheets = []
|
||||
self.css_errors = []
|
||||
self.files = files
|
||||
self.user_direction = self.env['res.lang']._lang_get(
|
||||
self.env.context.get('lang') or self.env.user.lang
|
||||
).direction
|
||||
|
||||
# asset-wide html "media" attribute
|
||||
for f in files:
|
||||
if css:
|
||||
@@ -134,8 +143,11 @@ class AssetsBundle(object):
|
||||
self.stylesheets.append(LessStylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'], direction=self.user_direction))
|
||||
elif f['atype'] == 'text/css':
|
||||
self.stylesheets.append(StylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'], direction=self.user_direction))
|
||||
if js and f['atype'] == 'text/javascript':
|
||||
self.javascripts.append(JavascriptAsset(self, url=f['url'], filename=f['filename'], inline=f['content']))
|
||||
if js:
|
||||
if f['atype'] == 'text/javascript':
|
||||
self.javascripts.append(JavascriptAsset(self, url=f['url'], filename=f['filename'], inline=f['content']))
|
||||
elif f['atype'] == 'text/xml':
|
||||
self.templates.append(XMLAsset(self, url=f['url'], filename=f['filename'], inline=f['content']))
|
||||
|
||||
def to_node(self, css=True, js=True, debug=False, async_load=False, defer_load=False, lazy_load=False):
|
||||
"""
|
||||
@@ -185,7 +197,7 @@ class AssetsBundle(object):
|
||||
"""Returns last modified date of linked files"""
|
||||
assets = [WebAsset(self, url=f['url'], filename=f['filename'], inline=f['content'])
|
||||
for f in self.files
|
||||
if f['atype'] in ['text/sass', "text/scss", "text/less", "text/css", "text/javascript"]]
|
||||
if f['atype'] in ['text/sass', "text/scss", "text/less", "text/css", "text/javascript", "text/xml"]]
|
||||
return max(itertools.chain(
|
||||
(asset.last_modified for asset in assets),
|
||||
))
|
||||
@@ -312,7 +324,7 @@ class AssetsBundle(object):
|
||||
|
||||
:return the ir.attachment records for a given bundle.
|
||||
"""
|
||||
assert extension in ('js', 'min.js', 'js.map', 'css', 'min.css', 'css.map')
|
||||
assert extension in ('js', 'min.js', 'js.map', 'css', 'min.css', 'css.map', 'xml', 'min.xml')
|
||||
ira = self.env['ir.attachment']
|
||||
|
||||
# Set user direction in name to store two bundles
|
||||
@@ -322,6 +334,7 @@ class AssetsBundle(object):
|
||||
fname = '%s.%s' % (self.name, extension)
|
||||
mimetype = (
|
||||
'text/css' if extension in ['css', 'min.css'] else
|
||||
'text/xml' if extension in ['xml', 'min.xml'] else
|
||||
'application/json' if extension in ['js.map', 'css.map'] else
|
||||
'application/javascript'
|
||||
)
|
||||
@@ -365,31 +378,51 @@ class AssetsBundle(object):
|
||||
|
||||
def js(self, is_minified=True):
|
||||
extension = 'min.js' if is_minified else 'js'
|
||||
attachments = self.get_attachments(extension)
|
||||
js_attachment = self.get_attachments(extension)
|
||||
|
||||
if not js_attachment:
|
||||
template_bundle = ''
|
||||
if self.templates:
|
||||
content = ['<?xml version="1.0" encoding="UTF-8"?>']
|
||||
content.append('<templates xml:space="preserve">')
|
||||
content.append(self.xml(show_inherit_info=not is_minified))
|
||||
content.append('</templates>')
|
||||
templates = '\n'.join(content).replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${")
|
||||
template_bundle = textwrap.dedent(f"""
|
||||
|
||||
/*******************************************
|
||||
* Templates *
|
||||
*******************************************/
|
||||
|
||||
odoo.define('{self.name}.bundle.xml', function(require){{
|
||||
'use strict';
|
||||
const {{ loadXML }} = require('@web/core/assets');
|
||||
const templates = `{templates}`;
|
||||
return loadXML(templates);
|
||||
}});""")
|
||||
|
||||
if not attachments:
|
||||
if is_minified:
|
||||
content = ';\n'.join(asset.minify() for asset in self.javascripts)
|
||||
return self.save_attachment(extension, content)
|
||||
content_bundle = ';\n'.join(asset.minify() for asset in self.javascripts)
|
||||
content_bundle += template_bundle
|
||||
js_attachment = self.save_attachment(extension, content_bundle)
|
||||
else:
|
||||
return self.js_with_sourcemap()
|
||||
js_attachment = self.js_with_sourcemap(template_bundle=template_bundle)
|
||||
|
||||
return attachments[0]
|
||||
return js_attachment[0]
|
||||
|
||||
def js_with_sourcemap(self):
|
||||
def js_with_sourcemap(self, template_bundle=None):
|
||||
"""Create the ir.attachment representing the not-minified content of the bundleJS
|
||||
and create/modify the ir.attachment representing the linked sourcemap.
|
||||
|
||||
:return ir.attachment representing the un-minified content of the bundleJS
|
||||
"""
|
||||
sourcemap_attachment = self.get_attachments('js.map') \
|
||||
or self.save_attachment('js.map', '')
|
||||
or self.save_attachment('js.map', '')
|
||||
generator = SourceMapGenerator(
|
||||
source_root="/".join(
|
||||
[".." for i in range(0, len(self.get_debug_asset_url(name=self.name).split("/")) - 2)]
|
||||
) + "/",
|
||||
)
|
||||
|
||||
content_bundle_list = []
|
||||
content_line_count = 0
|
||||
line_header = 6 # number of lines added by with_header()
|
||||
@@ -405,7 +438,11 @@ class AssetsBundle(object):
|
||||
content_bundle_list.append(asset.with_header(asset.content, minimal=False))
|
||||
content_line_count += len(asset.content.split("\n")) + line_header
|
||||
|
||||
content_bundle = ';\n'.join(content_bundle_list) + "\n//# sourceMappingURL=" + sourcemap_attachment.url
|
||||
content_bundle = ';\n'.join(content_bundle_list)
|
||||
if template_bundle:
|
||||
content_bundle += template_bundle
|
||||
|
||||
content_bundle += "\n\n//# sourceMappingURL=" + sourcemap_attachment.url
|
||||
js_attachment = self.save_attachment('js', content_bundle)
|
||||
|
||||
generator._file = js_attachment.url
|
||||
@@ -415,6 +452,138 @@ class AssetsBundle(object):
|
||||
|
||||
return js_attachment
|
||||
|
||||
def xml(self, show_inherit_info=False):
|
||||
"""
|
||||
Create the ir.attachment representing the content of the bundle XML.
|
||||
The xml contents are loaded and parsed with etree. Inheritances are
|
||||
applied in the order of files and templates.
|
||||
|
||||
Used parsed attributes:
|
||||
* `t-name`: template name
|
||||
* `t-inherit`: inherited template name. The template use the
|
||||
`apply_inheritance_specs` method from `ir.ui.view` to apply
|
||||
inheritance (with xpath and position).
|
||||
* 't-inherit-mode': 'primary' to create a new template with the
|
||||
update, or 'extension' to apply the update on the inherited
|
||||
template.
|
||||
* `t-extend` deprecated attribute, used by the JavaScript Qweb.
|
||||
|
||||
:param show_inherit_info: if true add the file url and inherit
|
||||
information in the template.
|
||||
:return ir.attachment representing the content of the bundle XML
|
||||
"""
|
||||
template_dict = OrderedDict()
|
||||
parser = etree.XMLParser(ns_clean=True, recover=True, remove_comments=True)
|
||||
|
||||
for asset in self.templates:
|
||||
# Load content.
|
||||
try:
|
||||
content = asset.content.strip()
|
||||
template = content if content.startswith('<odoo>') else f'<templates>{asset.content}</templates>'
|
||||
io_content = io.BytesIO(template.encode('utf-8'))
|
||||
content_templates_tree = etree.parse(io_content, parser=parser).getroot()
|
||||
except etree.ParseError as e:
|
||||
_logger.error("Could not parse file %s: %s", asset.url, e.msg)
|
||||
raise
|
||||
addon = asset.url.split('/')[1]
|
||||
template_dict.setdefault(addon, OrderedDict())
|
||||
# Process every templates.
|
||||
for template_tree in list(content_templates_tree):
|
||||
template_name = None
|
||||
if 't-name' in template_tree.attrib:
|
||||
template_name = template_tree.attrib['t-name']
|
||||
dotted_names = template_name.split('.', 1)
|
||||
if len(dotted_names) > 1 and dotted_names[0] == addon:
|
||||
template_name = dotted_names[1]
|
||||
|
||||
if 't-inherit' in template_tree.attrib:
|
||||
inherit_mode = template_tree.attrib.get('t-inherit-mode', 'primary')
|
||||
if inherit_mode not in ['primary', 'extension']:
|
||||
raise ValueError(_("Invalid inherit mode. Module %r and template name %r", addon, template_name))
|
||||
|
||||
# Get inherited template, the identifier can be "addon.name", just "name" or (silly) "just.name.with.dots"
|
||||
parent_dotted_name = template_tree.attrib['t-inherit']
|
||||
split_name_attempt = parent_dotted_name.split('.', 1)
|
||||
parent_addon, parent_name = split_name_attempt if len(split_name_attempt) == 2 else (addon, parent_dotted_name)
|
||||
if parent_addon not in template_dict:
|
||||
if parent_dotted_name in template_dict[addon]:
|
||||
parent_addon = addon
|
||||
parent_name = parent_dotted_name
|
||||
else:
|
||||
raise ValueError(_("Module %r not loaded or inexistent (try to inherit %r), or templates of addon being loaded %r are misordered (template %r)", parent_addon, parent_name, addon, template_name))
|
||||
if parent_name not in template_dict[parent_addon]:
|
||||
raise ValueError(_("Cannot create %r because the template to inherit %r is not found.") % (f'{addon}.{template_name}', f'{parent_addon}.{parent_name}'))
|
||||
|
||||
# After several performance tests, we found out that deepcopy is the most efficient
|
||||
# solution in this case (compared with copy, xpath with '.' and stringifying).
|
||||
parent_tree, parent_urls = template_dict[parent_addon][parent_name]
|
||||
parent_tree = copy.deepcopy(parent_tree)
|
||||
|
||||
if show_inherit_info:
|
||||
# Add inheritance information as xml comment for debugging.
|
||||
xpaths = []
|
||||
for item in template_tree:
|
||||
position = item.get('position')
|
||||
attrib = dict(**item.attrib)
|
||||
attrib.pop('position', None)
|
||||
comment = etree.Comment(f""" Filepath: {asset.url} ; position="{position}" ; {attrib} """)
|
||||
if position == "attributes":
|
||||
if item.get('expr'):
|
||||
comment_node = etree.Element('xpath', {'expr': item.get('expr'), 'position': 'before'})
|
||||
else:
|
||||
comment_node = etree.Element(item.tag, item.attrib)
|
||||
comment_node.attrib['position'] = 'before'
|
||||
comment_node.append(comment)
|
||||
xpaths.append(comment_node)
|
||||
else:
|
||||
if len(item) > 0:
|
||||
item[0].addprevious(comment)
|
||||
else:
|
||||
item.append(comment)
|
||||
xpaths.append(item)
|
||||
else:
|
||||
xpaths = list(template_tree)
|
||||
|
||||
# Apply inheritance.
|
||||
if inherit_mode == 'primary':
|
||||
parent_tree.tag = template_tree.tag
|
||||
inherited_template = apply_inheritance_specs(parent_tree, xpaths)
|
||||
if inherit_mode == 'primary': # New template_tree: A' = B(A)
|
||||
for attr_name, attr_val in template_tree.attrib.items():
|
||||
if attr_name not in ('t-inherit', 't-inherit-mode'):
|
||||
inherited_template.set(attr_name, attr_val)
|
||||
if not template_name:
|
||||
raise ValueError(_("Template name is missing in file %r.", asset.url))
|
||||
template_dict[addon][template_name] = (inherited_template, parent_urls + [asset.url])
|
||||
else: # Modifies original: A = B(A)
|
||||
template_dict[parent_addon][parent_name] = (inherited_template, parent_urls + [asset.url])
|
||||
elif template_name:
|
||||
if template_name in template_dict[addon]:
|
||||
raise ValueError(_("Template %r already exists in module %r", template_name, addon))
|
||||
template_dict[addon][template_name] = (template_tree, [asset.url])
|
||||
elif template_tree.attrib.get('t-extend'):
|
||||
template_name = '%s__extend_%s' % (template_tree.attrib.get('t-extend'), len(template_dict[addon]))
|
||||
template_dict[addon][template_name] = (template_tree, [asset.url])
|
||||
else:
|
||||
raise ValueError(_("Template name is missing in file %r.", asset.url))
|
||||
|
||||
# Concat and render inherited templates
|
||||
root = etree.Element('root')
|
||||
for addon in template_dict.values():
|
||||
for template, urls in addon.values():
|
||||
if show_inherit_info:
|
||||
tail = "\n"
|
||||
if len(root) > 0:
|
||||
tail = root[-1].tail
|
||||
root[-1].tail = "\n\n"
|
||||
comment = etree.Comment(f""" Filepath: {' => '.join(urls)} """)
|
||||
comment.tail = tail
|
||||
root.append(comment)
|
||||
root.append(template)
|
||||
|
||||
# Returns the string by removing the <root> tag.
|
||||
return etree.tostring(root, encoding='unicode')[6:-7]
|
||||
|
||||
def css(self, is_minified=True):
|
||||
extension = 'min.css' if is_minified else 'css'
|
||||
attachments = self.get_attachments(extension)
|
||||
@@ -719,6 +888,7 @@ class AssetsBundle(object):
|
||||
error += "This error occurred while compiling the bundle '%s' containing:" % self.name
|
||||
return error
|
||||
|
||||
|
||||
class WebAsset(object):
|
||||
html_url_format = '%s'
|
||||
_content = None
|
||||
@@ -829,7 +999,7 @@ class JavascriptAsset(WebAsset):
|
||||
|
||||
def _fetch_content(self):
|
||||
try:
|
||||
return super(JavascriptAsset, self)._fetch_content()
|
||||
return super()._fetch_content()
|
||||
except AssetError as e:
|
||||
return u"console.error(%s);" % json.dumps(to_text(e))
|
||||
|
||||
@@ -859,10 +1029,11 @@ class JavascriptAsset(WebAsset):
|
||||
# * Bundle: <name> *
|
||||
# * Lines: 42 *
|
||||
# **************************/
|
||||
line_count = content.count('\n')
|
||||
lines = [
|
||||
f"Filepath: {self.url}",
|
||||
f"Bundle: {self.bundle.name}",
|
||||
f"Lines: {len(content.splitlines())}",
|
||||
f"Lines: {line_count}",
|
||||
]
|
||||
length = max(map(len, lines))
|
||||
return "\n".join([
|
||||
@@ -874,6 +1045,56 @@ class JavascriptAsset(WebAsset):
|
||||
])
|
||||
|
||||
|
||||
class XMLAsset(WebAsset):
|
||||
def _fetch_content(self):
|
||||
try:
|
||||
content = super()._fetch_content()
|
||||
except AssetError as e:
|
||||
return f'<error data-asset-bundle={self.bundle.name!r} data-asset-version={self.bundle.version!r}>{json.dumps(to_text(e))}</error>'
|
||||
|
||||
parser = etree.XMLParser(ns_clean=True, recover=True, remove_comments=True)
|
||||
root = etree.parse(io.BytesIO(content.encode('utf-8')), parser=parser).getroot()
|
||||
if root.tag in ('templates', 'template'):
|
||||
return ''.join(etree.tostring(el, encoding='unicode') for el in root)
|
||||
return etree.tostring(root, encoding='unicode')
|
||||
|
||||
def to_node(self):
|
||||
attributes = {
|
||||
'async': 'async',
|
||||
'defer': 'defer',
|
||||
'type': 'text/xml',
|
||||
'data-src': self.html_url,
|
||||
'data-asset-bundle': self.bundle.name,
|
||||
'data-asset-version': self.bundle.version,
|
||||
}
|
||||
return ("script", attributes, None)
|
||||
|
||||
def with_header(self, content=None):
|
||||
if content is None:
|
||||
content = self.content
|
||||
|
||||
# format the header like
|
||||
# <!--=========================-->
|
||||
# <!-- Filepath: <asset_url> -->
|
||||
# <!-- Bundle: <name> -->
|
||||
# <!-- Lines: 42 -->
|
||||
# <!--=========================-->
|
||||
line_count = content.count('\n')
|
||||
lines = [
|
||||
f"Filepath: {self.url}",
|
||||
f"Bundle: {self.bundle.name}",
|
||||
f"Lines: {line_count}",
|
||||
]
|
||||
length = max(map(len, lines))
|
||||
return "\n".join([
|
||||
"",
|
||||
"<!-- " + "=" * length + " -->",
|
||||
*(f"<!-- {line:<{length}} -->" for line in lines),
|
||||
"<!-- " + "=" * length + " -->",
|
||||
content,
|
||||
])
|
||||
|
||||
|
||||
class StylesheetAsset(WebAsset):
|
||||
rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
|
||||
rx_url = re.compile(r"""(?<!")url\s*\(\s*('|"|)(?!'|"|/|https?://|data:|#{str)""", re.U)
|
||||
@@ -883,7 +1104,7 @@ class StylesheetAsset(WebAsset):
|
||||
def __init__(self, *args, **kw):
|
||||
self.media = kw.pop('media', None)
|
||||
self.direction = kw.pop('direction', None)
|
||||
super(StylesheetAsset, self).__init__(*args, **kw)
|
||||
super().__init__(*args, **kw)
|
||||
if self.direction == 'rtl' and self.url:
|
||||
self.html_url_args = self.url.rsplit('.', 1)
|
||||
self.html_url_format = '%%s/%s/%s.%%s' % ('rtl', self.bundle.name)
|
||||
@@ -891,14 +1112,14 @@ class StylesheetAsset(WebAsset):
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
content = super(StylesheetAsset, self).content
|
||||
content = super().content
|
||||
if self.media:
|
||||
content = '@media %s { %s }' % (self.media, content)
|
||||
return content
|
||||
|
||||
def _fetch_content(self):
|
||||
try:
|
||||
content = super(StylesheetAsset, self)._fetch_content()
|
||||
content = super()._fetch_content()
|
||||
web_dir = os.path.dirname(self.url)
|
||||
|
||||
if self.rx_import:
|
||||
@@ -961,7 +1182,7 @@ class PreprocessedCSS(StylesheetAsset):
|
||||
rx_import = None
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
super(PreprocessedCSS, self).__init__(*args, **kw)
|
||||
super().__init__(*args, **kw)
|
||||
self.html_url_args = tuple(self.url.rsplit('/', 1))
|
||||
self.html_url_format = '%%s/%s%s/%%s.css' % ('rtl/' if self.direction == 'rtl' else '', self.bundle.name)
|
||||
|
||||
@@ -1030,7 +1251,7 @@ class ScssStylesheetAsset(PreprocessedCSS):
|
||||
|
||||
def compile(self, source):
|
||||
if libsass is None:
|
||||
return super(ScssStylesheetAsset, self).compile(source)
|
||||
return super().compile(source)
|
||||
|
||||
try:
|
||||
profiler.force_hook()
|
||||
|
||||
@@ -87,7 +87,7 @@ class IrAsset(models.Model):
|
||||
active = fields.Boolean(string='active', default=True)
|
||||
sequence = fields.Integer(string="Sequence", default=DEFAULT_SEQUENCE, required=True)
|
||||
|
||||
def _get_asset_paths(self, bundle, addons=None, css=False, js=False, xml=False):
|
||||
def _get_asset_paths(self, bundle, addons=None, css=False, js=False):
|
||||
"""
|
||||
Fetches all asset file paths from a given list of addons matching a
|
||||
certain bundle. The returned list is composed of tuples containing the
|
||||
@@ -108,8 +108,8 @@ class IrAsset(models.Model):
|
||||
:param addons: list of addon names as strings. The files returned will
|
||||
only be contained in the given addons.
|
||||
:param css: boolean: whether or not to include style files
|
||||
:param js: boolean: whether or not to include script files
|
||||
:param xml: boolean: whether or not to include template files
|
||||
:param js: boolean: whether or not to include script files and template
|
||||
files
|
||||
:returns: the list of tuples (path, addon, bundle)
|
||||
"""
|
||||
installed = self._get_installed_addons_list()
|
||||
@@ -117,10 +117,10 @@ class IrAsset(models.Model):
|
||||
addons = self._get_active_addons_list()
|
||||
|
||||
asset_paths = AssetPaths()
|
||||
self._fill_asset_paths(bundle, addons, installed, css, js, xml, asset_paths, [])
|
||||
self._fill_asset_paths(bundle, addons, installed, css, js, asset_paths, [])
|
||||
return asset_paths.list
|
||||
|
||||
def _fill_asset_paths(self, bundle, addons, installed, css, js, xml, asset_paths, seen):
|
||||
def _fill_asset_paths(self, bundle, addons, installed, css, js, asset_paths, seen):
|
||||
"""
|
||||
Fills the given AssetPaths instance by applying the operations found in
|
||||
the matching bundle of the given addons manifests.
|
||||
@@ -140,10 +140,9 @@ class IrAsset(models.Model):
|
||||
exts = []
|
||||
if js:
|
||||
exts += SCRIPT_EXTENSIONS
|
||||
exts += TEMPLATE_EXTENSIONS
|
||||
if css:
|
||||
exts += STYLE_EXTENSIONS
|
||||
if xml:
|
||||
exts += TEMPLATE_EXTENSIONS
|
||||
|
||||
# this index is used for prepending: files are inserted at the beginning
|
||||
# of the CURRENT bundle.
|
||||
@@ -164,7 +163,7 @@ class IrAsset(models.Model):
|
||||
"""
|
||||
if directive == INCLUDE_DIRECTIVE:
|
||||
# recursively call this function for each INCLUDE_DIRECTIVE directive.
|
||||
self._fill_asset_paths(path_def, addons, installed, css, js, xml, asset_paths, seen + [bundle])
|
||||
self._fill_asset_paths(path_def, addons, installed, css, js, asset_paths, seen + [bundle])
|
||||
return
|
||||
|
||||
addon, paths = self._get_paths(path_def, installed, exts)
|
||||
@@ -235,10 +234,9 @@ class IrAsset(models.Model):
|
||||
target_path = self._get_paths(target_path_def, installed)[1][0]
|
||||
|
||||
css = ext in STYLE_EXTENSIONS
|
||||
js = ext in SCRIPT_EXTENSIONS
|
||||
xml = ext in TEMPLATE_EXTENSIONS
|
||||
js = ext in SCRIPT_EXTENSIONS or ext in TEMPLATE_EXTENSIONS
|
||||
|
||||
asset_paths = self._get_asset_paths(root_bundle, css=css, js=js, xml=xml)
|
||||
asset_paths = self._get_asset_paths(root_bundle, css=css, js=js)
|
||||
|
||||
for path, _, bundle in asset_paths:
|
||||
if path == target_path:
|
||||
@@ -344,12 +342,8 @@ class IrAsset(models.Model):
|
||||
paths = list(filter(is_safe_path, paths))
|
||||
safe_path = safe_path and len_paths == len(paths)
|
||||
|
||||
# When fetching template file paths, we need the full paths since xml
|
||||
# files are read from the file system. But web assets (scripts and
|
||||
# stylesheets) must be loaded using relative paths, hence the trimming
|
||||
# for non-xml file paths.
|
||||
paths = [path if path.split('.')[-1] in TEMPLATE_EXTENSIONS else fs2web(path[len(addons_path):]) for path in paths]
|
||||
|
||||
# Web assets must be loaded using relative paths.
|
||||
paths = [fs2web(path[len(addons_path):]) for path in paths]
|
||||
else:
|
||||
addon = None
|
||||
|
||||
|
||||
@@ -385,12 +385,12 @@ from odoo.tools.json import scriptsafe
|
||||
from odoo.tools.misc import str2bool
|
||||
from odoo.tools.image import image_data_uri
|
||||
from odoo.http import request
|
||||
from odoo.modules.module import get_resource_path
|
||||
from odoo.modules.module import get_resource_path, get_module_path
|
||||
from odoo.tools.profiler import QwebTracker
|
||||
from odoo.exceptions import UserError, AccessDenied, AccessError, MissingError, ValidationError
|
||||
|
||||
from odoo.addons.base.models.assetsbundle import AssetsBundle
|
||||
from odoo.addons.base.models.ir_asset import can_aggregate, STYLE_EXTENSIONS, SCRIPT_EXTENSIONS
|
||||
from odoo.addons.base.models.ir_asset import can_aggregate, STYLE_EXTENSIONS, SCRIPT_EXTENSIONS, TEMPLATE_EXTENSIONS
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -2446,13 +2446,13 @@ class IrQWeb(models.AbstractModel):
|
||||
# in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
|
||||
# by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
|
||||
'xml' not in tools.config['dev_mode'],
|
||||
tools.ormcache_context('bundle', 'css', 'js', 'debug', 'async_load', 'defer_load', 'lazy_load', keys=("website_id", "lang")),
|
||||
tools.ormcache('bundle', 'css', 'js', 'debug', 'async_load', 'defer_load', 'lazy_load', 'media', 'tuple(self.env.context.get(k) for k in self._get_template_cache_keys())'),
|
||||
)
|
||||
def _generate_asset_nodes_cache(self, bundle, css=True, js=True, debug=False, async_load=False, defer_load=False, lazy_load=False, media=None):
|
||||
return self._generate_asset_nodes(bundle, css, js, debug, async_load, defer_load, lazy_load, media)
|
||||
|
||||
@tools.ormcache_context('bundle', 'nodeAttrs and nodeAttrs.get("media")', 'defer_load', 'lazy_load', keys=("website_id", "lang"))
|
||||
def _get_asset_content(self, bundle, nodeAttrs=None, defer_load=False, lazy_load=False):
|
||||
@tools.ormcache('bundle', 'defer_load', 'lazy_load', 'media', 'tuple(self.env.context.get(k) for k in self._get_template_cache_keys())')
|
||||
def _get_asset_content(self, bundle, defer_load=False, lazy_load=False, media=None):
|
||||
asset_paths = self.env['ir.asset']._get_asset_paths(bundle=bundle, css=True, js=True)
|
||||
|
||||
files = []
|
||||
@@ -2460,11 +2460,24 @@ class IrQWeb(models.AbstractModel):
|
||||
for path, *_ in asset_paths:
|
||||
ext = path.split('.')[-1]
|
||||
is_js = ext in SCRIPT_EXTENSIONS
|
||||
is_xml = ext in TEMPLATE_EXTENSIONS
|
||||
is_css = ext in STYLE_EXTENSIONS
|
||||
if not is_js and not is_css:
|
||||
if not is_js and not is_xml and not is_css:
|
||||
continue
|
||||
|
||||
mimetype = 'text/javascript' if is_js else f'text/{ext}'
|
||||
if is_xml:
|
||||
base = get_module_path(bundle.split('.')[0]).rsplit('/', 1)[0]
|
||||
if path.startswith(base):
|
||||
path = path[len(base):]
|
||||
|
||||
mimetype = None
|
||||
if is_js:
|
||||
mimetype = 'text/javascript'
|
||||
elif is_css:
|
||||
mimetype = f'text/{ext}'
|
||||
elif is_xml:
|
||||
mimetype = 'text/xml'
|
||||
|
||||
if can_aggregate(path):
|
||||
segments = [segment for segment in path.split('/') if segment]
|
||||
files.append({
|
||||
@@ -2472,7 +2485,7 @@ class IrQWeb(models.AbstractModel):
|
||||
'url': path,
|
||||
'filename': get_resource_path(*segments) if segments else None,
|
||||
'content': '',
|
||||
'media': nodeAttrs and nodeAttrs.get('media'),
|
||||
'media': media,
|
||||
})
|
||||
else:
|
||||
if is_js:
|
||||
@@ -2483,15 +2496,23 @@ class IrQWeb(models.AbstractModel):
|
||||
attributes["data-src" if lazy_load else "src"] = path
|
||||
if defer_load or lazy_load:
|
||||
attributes["defer"] = "defer"
|
||||
else:
|
||||
elif is_css:
|
||||
tag = 'link'
|
||||
attributes = {
|
||||
"type": mimetype,
|
||||
"rel": "stylesheet",
|
||||
"href": path,
|
||||
'media': nodeAttrs and nodeAttrs.get('media'),
|
||||
'media': media,
|
||||
}
|
||||
remains.append((tag, attributes, ''))
|
||||
elif is_xml:
|
||||
tag = 'script'
|
||||
attributes = {
|
||||
"type": mimetype,
|
||||
"async": "async",
|
||||
"rel": "prefetch",
|
||||
"data-src": path,
|
||||
}
|
||||
remains.append((tag, attributes, None))
|
||||
|
||||
return (files, remains)
|
||||
|
||||
@@ -2499,8 +2520,7 @@ class IrQWeb(models.AbstractModel):
|
||||
return AssetsBundle(bundle_name, files, env=env, css=css, js=js)
|
||||
|
||||
def _generate_asset_nodes(self, bundle, css=True, js=True, debug=False, async_load=False, defer_load=False, lazy_load=False, media=None):
|
||||
nodeAttrs = {'media': media} if css and media else None
|
||||
files, remains = self._get_asset_content(bundle, nodeAttrs, defer_load=defer_load, lazy_load=lazy_load)
|
||||
files, remains = self._get_asset_content(bundle, defer_load=defer_load, lazy_load=lazy_load, media=css and media or None)
|
||||
asset = self._get_asset_bundle(bundle, files, env=self.env, css=css, js=js)
|
||||
remains = [node for node in remains if (css and node[0] == 'link') or (js and node[0] == 'script')]
|
||||
return remains + asset.to_node(css=css, js=js, debug=debug, async_load=async_load, defer_load=defer_load, lazy_load=lazy_load)
|
||||
|
||||
@@ -1773,7 +1773,7 @@ class TestAssetsManifest(AddonManifestPatched):
|
||||
'path': '/test_assetsbundle/%s' % path_to_dummy,
|
||||
})
|
||||
|
||||
files = self.env['ir.asset']._get_asset_paths('test_assetsbundle.irassetsec', addons=list(self.installed_modules), xml=False)
|
||||
files = self.env['ir.asset']._get_asset_paths('test_assetsbundle.irassetsec', addons=list(self.installed_modules))
|
||||
self.assertFalse(files)
|
||||
|
||||
def test_33(self):
|
||||
@@ -1822,7 +1822,7 @@ class TestAssetsManifest(AddonManifestPatched):
|
||||
'bundle': 'test_assetsbundle.irassetsec',
|
||||
'path': '/test_assetsbundle/data/ir_asset.xml',
|
||||
})
|
||||
files = self.env['ir.asset']._get_asset_paths('test_assetsbundle.irassetsec', addons=list(self.installed_modules), xml=False)
|
||||
files = self.env['ir.asset']._get_asset_paths('test_assetsbundle.irassetsec', addons=list(self.installed_modules))
|
||||
self.assertFalse(files)
|
||||
|
||||
def test_36(self):
|
||||
@@ -1831,7 +1831,7 @@ class TestAssetsManifest(AddonManifestPatched):
|
||||
'bundle': 'test_assetsbundle.irassetsec',
|
||||
'path': '/test_assetsbundle/static/accessible.xml',
|
||||
})
|
||||
files = self.env['ir.asset']._get_asset_paths('test_assetsbundle.irassetsec', addons=list(self.installed_modules), xml=False)
|
||||
files = self.env['ir.asset']._get_asset_paths('test_assetsbundle.irassetsec', addons=list(self.installed_modules))
|
||||
self.assertEqual(len(files), 1)
|
||||
self.assertTrue('test_assetsbundle/static/accessible.xml' in files[0][0])
|
||||
|
||||
|
||||
+1
-1
@@ -534,7 +534,7 @@ class Stream:
|
||||
'conditional': self.conditional,
|
||||
'etag': self.etag,
|
||||
'last_modified': self.last_modified,
|
||||
'max_age': self.max_age,
|
||||
'max_age': STATIC_CACHE_LONG if immutable else self.max_age,
|
||||
'environ': request.httprequest.environ,
|
||||
'response_class': Response,
|
||||
**send_file_kwargs,
|
||||
|
||||
Reference in New Issue
Block a user