diff options
| author | Robot Odoo <xmo+roboto@odoo.com> | 2019-02-08 20:16:33 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-02-08 20:16:33 +0100 |
| commit | f4dc2af71a662a3089ee12e52e2ccdaea6bd6587 (patch) | |
| tree | 81b9a7ec93ff9ee3c6f456b39b26c7682eac7719 /addons/web_editor | |
| parent | e6e206b14e3c3f0274b9d8e723ab1f40c53efe68 (diff) | |
| parent | 52b27c4c6b38fec21872c7a5d6aa155da0827d11 (diff) | |
[MERGE] website, *: improve customize dialog + add logo sizing
* portal, web, web_editor
See sub-commits for details.
- Many improvements to the customize dialog 'framework'
(toggle switches, new widgets, ...)
- Optimization of RPCs (including ace editor which shares the same code)
- Move navbar options of the customize menu to the customize dialog
- Add the possibility to control the header logo size
- Add the possibility to control the header font size
task-1904244
closes odoo/odoo#29624
Diffstat (limited to 'addons/web_editor')
| -rw-r--r-- | addons/web_editor/controllers/main.py | 277 | ||||
| -rw-r--r-- | addons/web_editor/models/__init__.py | 2 | ||||
| -rw-r--r-- | addons/web_editor/models/assets.py | 279 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/common/ace.js | 4 |
4 files changed, 386 insertions, 176 deletions
diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index d4ff41de09d..83320fef24f 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -4,20 +4,18 @@ import base64 import io import json import logging -import os import re import time -import uuid import werkzeug.wrappers from PIL import Image, ImageFont, ImageDraw -from lxml import etree, html +from lxml import etree from odoo.http import request from odoo import http, tools -from odoo.modules.module import get_resource_path, get_module_path logger = logging.getLogger(__name__) + class Web_Editor(http.Controller): #------------------------------------------------------ # convert font into picture @@ -296,40 +294,57 @@ class Web_Editor(http.Controller): result['originalSrc'] = record.url return result - def _get_view_fields_to_read(self): - return ['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id'] - - ## The get_assets_editor_resources route is in charge of transmitting the resources the assets - ## editor needs to work. - ## @param key - the xml_id or id of the view the resources are related to - ## @param get_views - True if the views must be fetched (default to True) - ## @param get_scss - True if the style must be fetched (default to True) - ## @param get_js - True if the javascript must be fetched (default to True) - ## @param bundles - True if the bundles views must be fetched (default to False) - ## @param bundles_restriction - Names of the bundle in which to look for scss files (if empty, search in all of them) - ## @param only_user_custom_files - True if only user custom files are needed (default to True) - ## @returns a dictionary with views info in the views key and style info in the scss key @http.route("/web_editor/get_assets_editor_resources", type="json", auth="user", website=True) def get_assets_editor_resources(self, key, get_views=True, get_scss=True, get_js=True, bundles=False, bundles_restriction=[], only_user_custom_files=True): + """ + Transmit the resources the assets editor needs to work. + + Params: + key (str): the key of the view the resources are related to + + get_views (bool, default=True): + True if the views must be fetched + + get_scss (bool, default=True): + True if the style must be fetched + + get_js (bool, default=True): + True if the javascript must be fetched + + bundles (bool, default=False): + True if the bundles views must be fetched + + bundles_restriction (list, default=[]): + Names of the bundles in which to look for scss files + (if empty, search in all of them) + + only_user_custom_files (bool, default=True): + True if only user custom files must be fetched + + Returns: + dict: views, scss, js + """ # Related views must be fetched if the user wants the views and/or the style views = request.env["ir.ui.view"].get_related_views(key, bundles=bundles) - views = views.read(self._get_view_fields_to_read()) + views = views.read(request.env['web_editor.assets'].get_view_fields_to_read()) - js_files_data_by_bundle = [] scss_files_data_by_bundle = [] + js_files_data_by_bundle = [] - if get_js: - js_files_data_by_bundle = self.load_resources('js', views, bundles_restriction, only_user_custom_files) if get_scss: - scss_files_data_by_bundle = self.load_resources('scss', views, bundles_restriction, only_user_custom_files) + scss_files_data_by_bundle = self._load_resources('scss', views, bundles_restriction, only_user_custom_files) + if get_js: + js_files_data_by_bundle = self._load_resources('js', views, bundles_restriction, only_user_custom_files) - return dict( - views=get_views and views or [], - scss=get_scss and scss_files_data_by_bundle or [], - js=get_js and js_files_data_by_bundle or [], - ) + return { + 'views': get_views and views or [], + 'scss': get_scss and scss_files_data_by_bundle or [], + 'js': get_js and js_files_data_by_bundle or [], + } + + def _load_resources(self, file_type, views, bundles_restriction, only_user_custom_files): + AssetsUtils = request.env['web_editor.assets'] - def load_resources(self, file_type, views, bundles_restriction, only_user_custom_files): files_data_by_bundle = [] resources_type_info = {'t_call_assets_attribute': 't-js', 'mimetype': 'text/javascript'} if file_type == 'scss': @@ -339,10 +354,6 @@ class Web_Editor(http.Controller): # This will used to exclude library scss files from the result excluded_url_matcher = re.compile("^(.+/lib/.+)|(.+import_bootstrap.+\.scss)$") - # Load already customized files attachments - custom_url = self._make_custom_scss_or_js_file_url("%%.%%", "%%") - custom_attachments = self.get_custom_attachment(custom_url, op='=like') - # First check the t-call-assets used in the related views url_infos = dict() for v in views: @@ -363,7 +374,7 @@ class Web_Editor(http.Controller): continue # Check if the file is customized and get bundle/path info - file_data = self._match_scss_or_js_file_url(url) + file_data = AssetsUtils.get_asset_info(url) if not file_data: continue @@ -375,13 +386,19 @@ class Web_Editor(http.Controller): or file_type == 'scss' and not only_user_custom_files: files_data.append(url) - # File data is returned sorted by bundle, with the bundles names and xmlids + # scss data is returned sorted by bundle, with the bundles + # names and xmlids if len(files_data): - files_data_by_bundle.append([dict(xmlid=asset_name, name=request.env.ref(asset_name).name), files_data]) + files_data_by_bundle.append([ + {'xmlid': asset_name, 'name': request.env.ref(asset_name).name}, + files_data + ]) # Filter bundles/files: - # - A file which appears in multiple bundles only appears in the first one (the first in the DOM) - # - Only keep bundles with files which appears in the asked bundles and only keep those files + # - A file which appears in multiple bundles only appears in the + # first one (the first in the DOM) + # - Only keep bundles with files which appears in the asked bundles + # and only keep those files for i in range(0, len(files_data_by_bundle)): bundle_1 = files_data_by_bundle[i] for j in range(0, len(files_data_by_bundle)): @@ -393,7 +410,8 @@ class Web_Editor(http.Controller): bundle_1 = files_data_by_bundle[i] for j in range(i + 1, len(files_data_by_bundle)): bundle_2 = files_data_by_bundle[j] - # In every bundle, keep only the files which were not found in previous bundles + # In every bundle, keep only the files which were not found + # in previous bundles bundle_2[1] = [item_2 for item_2 in bundle_2[1] if item_2 not in bundle_1[1]] # Only keep bundles which still have files and that were requested @@ -403,146 +421,57 @@ class Web_Editor(http.Controller): ] # Fetch the arch of each kept file, in each bundle + urls = [] + for bundle_data in files_data_by_bundle: + urls += bundle_data[1] + custom_attachments = AssetsUtils.get_all_custom_attachments(urls) + for bundle_data in files_data_by_bundle: for i in range(0, len(bundle_data[1])): url = bundle_data[1][i] url_info = url_infos[url] - content = None - if url_info["customized"]: - # If the file is already customized, the content is found in the corresponding attachment - content = base64.b64decode(custom_attachments.filtered(lambda a: a.url == url).datas) - else: - # If the file is not yet customized, the content is found by reading the local scss file - module = url_info["module"] - module_path = get_module_path(module) - module_resource_path = get_resource_path(module, url_info["resource_path"]) - if module_path and module_resource_path: - module_path = os.path.join(os.path.normpath(module_path), '') # join ensures the path ends with '/' - module_resource_path = os.path.normpath(module_resource_path) - if module_resource_path.startswith(module_path): - with open(module_resource_path, "rb") as f: - content = f.read() - - bundle_data[1][i] = dict( - url="/%s/%s" % (url_info["module"], url_info["resource_path"]), - arch=content, - customized=url_info["customized"], - ) + content = AssetsUtils.get_asset_content(url, url_info, custom_attachments) + + bundle_data[1][i] = { + 'url': "/%s/%s" % (url_info["module"], url_info["resource_path"]), + 'arch': content, + 'customized': url_info["customized"], + } + return files_data_by_bundle - def save_scss_or_js_view_hook(self): - return {} - - def save_scss_or_js_attachment_hook(self): - return {} - - ## The save_scss_or_js route is in charge of saving a given modification of a scss/js file. - ## @param url - the original url of the scss/js file which has to be modified - ## @param bundle_xmlid - the xmlid of the bundle in which the scss/js file addition can be found - ## @param content - the new content of the scss/js file - ## @param file_type - scss or js - @http.route("/web_editor/save_scss_or_js", type="json", auth="user", website=True) - def save_scss_or_js(self, url, bundle_xmlid, content, file_type): - IrAttachment = request.env["ir.attachment"] - - custom_url = self._make_custom_scss_or_js_file_url(url, bundle_xmlid) - - # Check if the file to save had already been modified - custom_attachment = self.get_custom_attachment(custom_url) - datas = base64.b64encode((content or "\n").encode("utf-8")) - if custom_attachment: - # If it was already modified, simply override the corresponding attachment content - custom_attachment.write({"datas": datas}) - else: - # If not, create a new attachment to copy the original scss/js file content, with its modifications - new_attach = { - 'name': custom_url, - 'type': "binary", - 'mimetype': (file_type == 'js' and 'text/javascript' or 'text/scss'), - 'datas': datas, - 'datas_fname': url.split("/")[-1], - 'url': custom_url, - } - new_attach.update(self.save_scss_or_js_attachment_hook()) - IrAttachment.create(new_attach) - - # Create a view to extend the template which adds the original file to link the new modified version instead - IrUiView = request.env["ir.ui.view"] - file_type_info = { - 'tag': 'link' if file_type == 'scss' else 'script', - 'attribute': 'href' if file_type == 'scss' else 'src', - } - - def views_linking_url(view): - """ - Returns whether the view arch has some html link tag linked to the url. - - (note: searching for the URL string is not enough as it could appear in a comment or an xpath expression.) - """ - tree = etree.XML(view.arch) - return bool(tree.xpath("//%%(tag)s[@%%(attribute)s='%(url)s']" % { - 'url': url, - } % file_type_info)) - - view_to_xpath = IrUiView.get_related_views(bundle_xmlid, bundles=True).filtered(views_linking_url) - - new_view = { - 'name': custom_url, - 'key': 'web_editor.%s_%s' % (file_type, str(uuid.uuid4())[:6]), - 'mode': "extension", - 'inherit_id': view_to_xpath.id, - 'arch': """ - <data inherit_id="%(inherit_xml_id)s" name="%(name)s"> - <xpath expr="//%%(tag)s[@%%(attribute)s='%(url_to_replace)s']" position="attributes"> - <attribute name="%%(attribute)s">%(new_url)s</attribute> - </xpath> - </data> - """ % { - 'inherit_xml_id': view_to_xpath.xml_id, - 'name': custom_url, - 'url_to_replace': url, - 'new_url': custom_url, - } % file_type_info - } - new_view.update(self.save_scss_or_js_view_hook()) - IrUiView.create(new_view) - - request.env["ir.qweb"].clear_caches() - - ## The reset_scss_or_js route is in charge of reverting all the changes that were done to a scss/js file. - ## @param url - the original URL of the scss file to reset - ## @param bundle_xmlid - the xmlid of the bundle in which the scss file addition can be found - @http.route("/web_editor/reset_scss_or_js", type="json", auth="user", website=True) - def reset_scss_or_js(self, url, bundle_xmlid): - custom_url = self._make_custom_scss_or_js_file_url(url, bundle_xmlid) - - # Simply delete the attachement which contains the modified scss/js file and the xpath view which links it - self.get_custom_attachment(custom_url).unlink() - self.get_custom_view(custom_url).unlink() - - def get_custom_attachment(self, custom_url, op='='): - assert op in ('=like', '='), 'Invalid operator' - IrAttachment = request.env["ir.attachment"] - return IrAttachment.search([("url", op, custom_url)]) - - def get_custom_view(self, custom_url, op='='): - assert op in ('=like', '='), 'Invalid operator' - IrUiView = request.env["ir.ui.view"] - return IrUiView.search([("name", op, custom_url)]) - - def _make_custom_scss_or_js_file_url(self, url, bundle): - parts = url.rsplit(".", 1) - return "%s.custom.%s.%s" % (parts[0], bundle, parts[1]) - - _match_file_url_regex = re.compile("^/(\w+)/(.+?)(\.custom\.(.+))?\.(\w+)$") - def _match_scss_or_js_file_url(self, url): - m = self._match_file_url_regex.match(url) - if not m: - return False - return { - 'module': m.group(1), - 'resource_path': "%s.%s" % (m.group(2), m.group(5)), - 'customized': bool(m.group(3)), - 'bundle': m.group(4) or False - } + @http.route("/web_editor/save_asset", type="json", auth="user", website=True) + def save_asset(self, url, bundle_xmlid, content, file_type): + """ + Save a given modification of a scss/js file. + + Params: + url (str): + the original url of the scss/js file which has to be modified + + bundle_xmlid (str): + the xmlid of the bundle in which the scss/js file addition can + be found + + content (str): the new content of the scss/js file + + file_type (str): 'scss' or 'js' + """ + request.env['web_editor.assets'].save_asset(url, bundle_xmlid, content, file_type) + + @http.route("/web_editor/reset_asset", type="json", auth="user", website=True) + def reset_asset(self, url, bundle_xmlid): + """ + The reset_asset route is in charge of reverting all the changes that + were done to a scss/js file. + + Params: + url (str): + the original URL of the scss/js file to reset + + bundle_xmlid (str): + the xmlid of the bundle in which the scss/js file addition can + be found + """ + request.env['web_editor.assets'].reset_asset(url, bundle_xmlid) diff --git a/addons/web_editor/models/__init__.py b/addons/web_editor/models/__init__.py index b895b36ca54..e9a790f7e61 100644 --- a/addons/web_editor/models/__init__.py +++ b/addons/web_editor/models/__init__.py @@ -7,4 +7,6 @@ from . import ir_ui_view from . import ir_http from . import ir_translation +from . import assets + from . import test_models diff --git a/addons/web_editor/models/assets.py b/addons/web_editor/models/assets.py new file mode 100644 index 00000000000..9406fc495b2 --- /dev/null +++ b/addons/web_editor/models/assets.py @@ -0,0 +1,279 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 +import os +import re +import uuid + +from lxml import etree + +from odoo import models +from odoo.modules.module import get_resource_path, get_module_path + +_match_asset_file_url_regex = re.compile("^/(\w+)/(.+?)(\.custom\.(.+))?\.(\w+)$") + + +class Assets(models.AbstractModel): + _name = 'web_editor.assets' + _description = 'Assets Utils' + + def get_view_fields_to_read(self): + return ['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id'] + + def get_all_custom_attachments(self, urls): + """ + Fetch all the ir.attachment records related to given URLs. + + Params: + urls (str[]): list of urls + + Returns: + ir.attachment(): attachment records related to the given URLs. + """ + return self._get_custom_attachment(urls, op='in') + + def get_asset_content(self, url, url_info=None, custom_attachments=None): + """ + Fetch the content of an asset (scss / js) file. That content is either + the one of the related file on the disk or the one of the corresponding + custom ir.attachment record. + + Params: + url (str): the URL of the asset (scss / js) file/ir.attachment + + url_info (dict, optional): + the related url info (see get_asset_info) (allows to optimize + some code which already have the info and do not want this + function to re-get it) + + custom_attachments (ir.attachment(), optional): + the related custom ir.attachment records the function might need + to search into (allows to optimize some code which already have + that info and do not want this function to re-get it) + + Returns: + utf-8 encoded content of the asset (scss / js) + """ + if url_info is None: + url_info = self.get_asset_info(url) + + if url_info["customized"]: + # If the file is already customized, the content is found in the + # corresponding attachment + attachment = None + if custom_attachments is None: + attachment = self._get_custom_attachment(url) + else: + attachment = custom_attachments.filtered(lambda r: r.url == url) + return attachment and base64.b64decode(attachment.datas) or False + + # If the file is not yet customized, the content is found by reading + # the local scss file + module = url_info["module"] + module_path = get_module_path(module) + module_resource_path = get_resource_path(module, url_info["resource_path"]) + if module_path and module_resource_path: + module_path = os.path.join(os.path.normpath(module_path), '') # join ensures the path ends with '/' + module_resource_path = os.path.normpath(module_resource_path) + if module_resource_path.startswith(module_path): + with open(module_resource_path, "rb") as f: + return f.read() + + def get_asset_info(self, url): + """ + Return information about an asset (scss / js) file/ir.attachment just by + looking at its URL. + + Params: + url (str): the url of the asset (scss / js) file/ir.attachment + + Returns: + dict: + module (str): the original asset's related app + + resource_path (str): + the relative path to the original asset from the related app + + customized (bool): whether the asset is a customized one or not + + bundle (str): + the name of the bundle the asset customizes (False if this + is not a customized asset) + """ + m = _match_asset_file_url_regex.match(url) + if not m: + return False + return { + 'module': m.group(1), + 'resource_path': "%s.%s" % (m.group(2), m.group(5)), + 'customized': bool(m.group(3)), + 'bundle': m.group(4) or False + } + + def make_custom_asset_file_url(self, url, bundle_xmlid): + """ + Return the customized version of an asset URL, that is the URL the asset + would have if it was customized. + + Params: + url (str): the original asset's url + bundle_xmlid (str): the name of the bundle the asset would customize + + Returns: + str: the URL the given asset would have if it was customized in the + given bundle + """ + parts = url.rsplit(".", 1) + return "%s.custom.%s.%s" % (parts[0], bundle_xmlid, parts[1]) + + def reset_asset(self, url, bundle_xmlid): + """ + Delete the potential customizations made to a given (original) asset. + + Params: + url (str): the URL of the original asset (scss / js) file + + bundle_xmlid (str): + the name of the bundle in which the customizations to delete + were made + """ + custom_url = self.make_custom_asset_file_url(url, bundle_xmlid) + + # Simply delete the attachement which contains the modified scss/js file + # and the xpath view which links it + self._get_custom_attachment(custom_url).unlink() + self._get_custom_view(custom_url).unlink() + + def save_asset(self, url, bundle_xmlid, content, file_type): + """ + Customize the content of a given asset (scss / js). + + Params: + url (src): + the URL of the original asset to customize (whether or not the + asset was already customized) + + bundle_xmlid (src): + the name of the bundle in which the customizations will take + effect + + content (src): the new content of the asset (scss / js) + + file_type (src): + either 'scss' or 'js' according to the file being customized + """ + custom_url = self.make_custom_asset_file_url(url, bundle_xmlid) + datas = base64.b64encode((content or "\n").encode("utf-8")) + + # Check if the file to save had already been modified + custom_attachment = self._get_custom_attachment(custom_url) + if custom_attachment: + # If it was already modified, simply override the corresponding + # attachment content + custom_attachment.write({"datas": datas}) + else: + # If not, create a new attachment to copy the original scss/js file + # content, with its modifications + new_attach = { + 'name': custom_url, + 'type': "binary", + 'mimetype': (file_type == 'js' and 'text/javascript' or 'text/scss'), + 'datas': datas, + 'datas_fname': url.split("/")[-1], + 'url': custom_url, + } + new_attach.update(self._save_asset_attachment_hook()) + self.env["ir.attachment"].create(new_attach) + + # Create a view to extend the template which adds the original file + # to link the new modified version instead + file_type_info = { + 'tag': 'link' if file_type == 'scss' else 'script', + 'attribute': 'href' if file_type == 'scss' else 'src', + } + + def views_linking_url(view): + """ + Returns whether the view arch has some html tag linked to + the url. (note: searching for the URL string is not enough as it + could appear in a comment or an xpath expression.) + """ + tree = etree.XML(view.arch) + return bool(tree.xpath("//%%(tag)s[@%%(attribute)s='%(url)s']" % { + 'url': url, + } % file_type_info)) + + IrUiView = self.env["ir.ui.view"] + view_to_xpath = IrUiView.get_related_views(bundle_xmlid, bundles=True).filtered(views_linking_url) + new_view = { + 'name': custom_url, + 'key': 'web_editor.%s_%s' % (file_type, str(uuid.uuid4())[:6]), + 'mode': "extension", + 'inherit_id': view_to_xpath.id, + 'arch': """ + <data inherit_id="%(inherit_xml_id)s" name="%(name)s"> + <xpath expr="//%%(tag)s[@%%(attribute)s='%(url_to_replace)s']" position="attributes"> + <attribute name="%%(attribute)s">%(new_url)s</attribute> + </xpath> + </data> + """ % { + 'inherit_xml_id': view_to_xpath.xml_id, + 'name': custom_url, + 'url_to_replace': url, + 'new_url': custom_url, + } % file_type_info + } + new_view.update(self._save_asset_view_hook()) + IrUiView.create(new_view) + + self.env["ir.qweb"].clear_caches() + + def _get_custom_attachment(self, custom_url, op='='): + """ + Fetch the ir.attachment record related to the given customized asset. + + Params: + custom_url (str): the URL of the customized asset + op (str, default: '='): the operator to use to search the records + + Returns: + ir.attachment() + """ + assert op in ('in', '='), 'Invalid operator' + return self.env["ir.attachment"].search([("url", op, custom_url)]) + + def _get_custom_view(self, custom_url, op='='): + """ + Fetch the ir.ui.view record related to the given customized asset (the + inheriting view which replace the original asset by the customized one). + + Params: + custom_url (str): the URL of the customized asset + op (str, default: '='): the operator to use to search the records + + Returns: + ir.ui.view() + """ + assert op in ('='), 'Invalid operator' + return self.env["ir.ui.view"].search([("name", op, custom_url)]) + + def _save_asset_attachment_hook(self): + """ + Returns the additional values to use to write the DB on customized + attachment creation. + + Returns: + dict + """ + return {} + + def _save_asset_view_hook(self): + """ + Returns the additional values to use to write the DB on customized + asset's related view creation. + + Returns: + dict + """ + return {} diff --git a/addons/web_editor/static/src/js/common/ace.js b/addons/web_editor/static/src/js/common/ace.js index f231bb0aac6..f242129c4a8 100644 --- a/addons/web_editor/static/src/js/common/ace.js +++ b/addons/web_editor/static/src/js/common/ace.js @@ -535,7 +535,7 @@ var ViewEditor = Widget.extend({ } else { var resource = type === 'scss' ? this.scss[resID] : this.js[resID]; return this._rpc({ - route: '/web_editor/reset_scss_or_js', + route: '/web_editor/reset_asset', params: { url: resID, bundle_xmlid: resource.bundle_xmlid, @@ -560,7 +560,7 @@ var ViewEditor = Widget.extend({ var bundleXmlID = sessionIdEndsWithJS ? this.js[session.id].bundle_xmlid : this.scss[session.id].bundle_xmlid; var fileType = sessionIdEndsWithJS ? 'js' : 'scss'; self._rpc({ - route: '/web_editor/save_scss_or_js', + route: '/web_editor/save_asset', params: { url: session.id, bundle_xmlid: bundleXmlID, |
