diff options
Diffstat (limited to 'addons/web_editor/controllers/main.py')
| -rw-r--r-- | addons/web_editor/controllers/main.py | 203 |
1 files changed, 196 insertions, 7 deletions
diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index 325cbdc6d87..054850b0926 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -5,12 +5,16 @@ import cStringIO import io import json import logging +import os +import re import time import werkzeug.wrappers from PIL import Image, ImageFont, ImageDraw +from lxml import etree, html 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__) @@ -223,11 +227,196 @@ class Web_Editor(http.Controller): attachments_to_remove.unlink() return removal_blocked_by - @http.route('/web_editor/get_assets_editor_resources', type='json', auth='user', website=True) - def get_assets_editor_resources(self, key, bundles=False): - """ Get inherit view's informations of the template ``key``. - returns templates info (which can be active or not) - ``bundles=True`` returns also the asset bundles - """ + ## 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_less - True if the style 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 less files (if empty, search in all of them) + ## @returns a dictionary with views info in the views key and style info in the less key + @http.route("/web_editor/get_assets_editor_resources", type="json", auth="user") + def get_assets_editor_resources(self, key, get_views=True, get_less=True, bundles=False, bundles_restriction=[]): + # 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) - return views.read(['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id']) + views = views.read(['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id']) + + less_files_data_by_bundle = [] + + # Load less only if asked by the user + if get_less: + # Compile regex outside of the loop + # This will used to exclude library less files from the result + excluded_url_matcher = re.compile("^(.+/lib/.+)|(.+import_bootstrap.less)$") + + # Load already customized less files attachments + custom_attachments = request.env["ir.attachment"].search([("url", "=like", self._make_custom_less_file_url("%%.%%", "%%"))]) + + # First check the t-call-assets used in the related views + url_infos = dict() + for v in views: + for asset_call_node in etree.fromstring(v["arch"]).xpath("//t[@t-call-assets]"): + if asset_call_node.get("t-css") == "false": + continue + asset_name = asset_call_node.get("t-call-assets") + + # Loop through bundle files to search for LESS file info + less_files_data = [] + for file_info in request.env["ir.qweb"]._get_asset_content(asset_name, {})[0]: + if file_info["atype"] != "text/less": + continue + url = file_info["url"] + + # Exclude library files (see regex above) + if excluded_url_matcher.match(url): + continue + + # Check if the file is customized and get bundle/path info + less_file_data = self._match_less_file_url(url) + if not less_file_data: + continue + + # Save info (arch will be fetched later) + url_infos[url] = less_file_data + less_files_data.append(url) + + # Less data is returned sorted by bundle, with the bundles names and xmlids + if len(less_files_data): + less_files_data_by_bundle.append([dict(xmlid=asset_name, name=request.env.ref(asset_name).name), less_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 + for i in range(0, len(less_files_data_by_bundle)): + bundle_1 = less_files_data_by_bundle[i] + for j in range(0, len(less_files_data_by_bundle)): + bundle_2 = less_files_data_by_bundle[j] + # In unwanted bundles, keep only the files which are in wanted bundles too (less_helpers) + if bundle_1[0]["xmlid"] not in bundles_restriction and bundle_2[0]["xmlid"] in bundles_restriction: + bundle_1[1] = [item_1 for item_1 in bundle_1[1] if item_1 in bundle_2[1]] + for i in range(0, len(less_files_data_by_bundle)): + bundle_1 = less_files_data_by_bundle[i] + for j in range(i+1, len(less_files_data_by_bundle)): + bundle_2 = less_files_data_by_bundle[j] + # 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 + less_files_data_by_bundle = [ + data for data in less_files_data_by_bundle + if (len(data[1]) > 0 and (not bundles_restriction or data[0]["xmlid"] in bundles_restriction)) + ] + + # Fetch the arch of each kept file, in each bundle + for bundle_data in less_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 = custom_attachments.filtered(lambda a: a.url == url).datas.decode("base64") + else: + # If the file is not yet customized, the content is found by reading the local less 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"], + ) + + return dict( + views = get_views and views or [], + less = get_less and less_files_data_by_bundle or [], + ) + + ## The save_less route is in charge of saving a given modification of a LESS file. + ## @param url - the original url of the LESS file which has to be modified + ## @param bundle_xmlid - the xmlid of the bundle in which the LESS file addition can be found + ## @param content - the new content of the LESS file + @http.route("/web_editor/save_less", type="json", auth="user") + def save_less(self, url, bundle_xmlid, content): + IrAttachment = request.env["ir.attachment"] + + custom_url = self._make_custom_less_file_url(url, bundle_xmlid) + + # Check if the file to save had already been modified + custom_attachment = IrAttachment.search([("url", "=", custom_url)]) + if custom_attachment: + # If it was already modified, simply override the corresponding attachment content + custom_attachment.write({"datas": content.encode("utf-8").encode("base64")}) + else: + # If not, create a new attachment to copy the original LESS file content, with its modifications + IrAttachment.create(dict( + name = custom_url, + type = "binary", + mimetype = "text/less", + datas = content.encode("utf-8").encode("base64"), + datas_fname = url.split("/")[-1], + url = custom_url, # Having an attachment of "binary" type with an non empty "url" field + # is quite of an hack. This allows to fetch the "datas" field by adding + # a <link/> with the "url" content in the bundle template (see qweb) + )) + + # 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"] + view_to_xpath = IrUiView.get_related_views(bundle_xmlid, bundles=True).filtered(lambda v: v.arch.find(url) >= 0) + IrUiView.create(dict( + name = custom_url, + mode = "extension", + inherit_id = view_to_xpath.id, + arch = """ + <data inherit_id="%(inherit_xml_id)s" name="%(name)s"> + <xpath expr="//link[@href='%(url_to_replace)s']" position="attributes"> + <attribute name="href">%(new_url)s</attribute> + </xpath> + </data> + """ % dict( + inherit_xml_id = view_to_xpath.xml_id, + name = custom_url, + url_to_replace = url, + new_url = custom_url, + ) + )) + + request.env["ir.qweb"].clear_caches() + + ## The reset_less route is in charge of reverting all the changes that were done to a less file. + ## @param url - the original URL of the LESS file to reset + ## @param bundle_xmlid - the xmlid of the bundle in which the LESS file addition can be found + @http.route("/web_editor/reset_less", type="json", auth="user") + def reset_less(self, url, bundle_xmlid): + IrAttachment = request.env["ir.attachment"] + IrUiView = request.env["ir.ui.view"] + + custom_url = self._make_custom_less_file_url(url, bundle_xmlid) + + # Simply delete the attachement which contains the modified less file and the xpath view which links it + IrAttachment.search([("url", "=", custom_url)]).unlink() + IrUiView.search([("name", "=", custom_url)]).unlink() + + def _make_custom_less_file_url(self, url, bundle): + parts = url.rsplit(".", 1) + return "%s.custom.%s.%s" % (parts[0], bundle, parts[1]) + + _match_less_file_url_regex = re.compile("^/(\w+)/(.+?)(\.custom\.(.+))?\.(\w+)$") + def _match_less_file_url(self, url): + m = self._match_less_file_url_regex.match(url) + if not m: + return False + return dict( + 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 + ) |
