summaryrefslogtreecommitdiff
path: root/addons/web_editor/controllers/main.py
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/web_editor/controllers/main.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/web_editor/controllers/main.py')
-rw-r--r--addons/web_editor/controllers/main.py632
1 files changed, 632 insertions, 0 deletions
diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py
new file mode 100644
index 00000000..2cefa652
--- /dev/null
+++ b/addons/web_editor/controllers/main.py
@@ -0,0 +1,632 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import io
+import logging
+import re
+import time
+import requests
+import werkzeug.wrappers
+from PIL import Image, ImageFont, ImageDraw
+from lxml import etree
+from base64 import b64decode, b64encode
+
+from odoo.http import request
+from odoo import http, tools, _, SUPERUSER_ID
+from odoo.addons.http_routing.models.ir_http import slug
+from odoo.exceptions import UserError
+from odoo.modules.module import get_module_path, get_resource_path
+from odoo.tools.misc import file_open
+
+from ..models.ir_attachment import SUPPORTED_IMAGE_MIMETYPES
+
+logger = logging.getLogger(__name__)
+DEFAULT_LIBRARY_ENDPOINT = 'https://media-api.odoo.com'
+
+class Web_Editor(http.Controller):
+ #------------------------------------------------------
+ # convert font into picture
+ #------------------------------------------------------
+ @http.route([
+ '/web_editor/font_to_img/<icon>',
+ '/web_editor/font_to_img/<icon>/<color>',
+ '/web_editor/font_to_img/<icon>/<color>/<int:size>',
+ '/web_editor/font_to_img/<icon>/<color>/<int:size>/<int:alpha>',
+ ], type='http', auth="none")
+ def export_icon_to_png(self, icon, color='#000', size=100, alpha=255, font='/web/static/lib/fontawesome/fonts/fontawesome-webfont.ttf'):
+ """ This method converts an unicode character to an image (using Font
+ Awesome font by default) and is used only for mass mailing because
+ custom fonts are not supported in mail.
+ :param icon : decimal encoding of unicode character
+ :param color : RGB code of the color
+ :param size : Pixels in integer
+ :param alpha : transparency of the image from 0 to 255
+ :param font : font path
+
+ :returns PNG image converted from given font
+ """
+ # Make sure we have at least size=1
+ size = max(1, size)
+ # Initialize font
+ addons_path = http.addons_manifest['web']['addons_path']
+ font_obj = ImageFont.truetype(addons_path + font, size)
+
+ # if received character is not a number, keep old behaviour (icon is character)
+ icon = chr(int(icon)) if icon.isdigit() else icon
+
+ # Determine the dimensions of the icon
+ image = Image.new("RGBA", (size, size), color=(0, 0, 0, 0))
+ draw = ImageDraw.Draw(image)
+
+ boxw, boxh = draw.textsize(icon, font=font_obj)
+ draw.text((0, 0), icon, font=font_obj)
+ left, top, right, bottom = image.getbbox()
+
+ # Create an alpha mask
+ imagemask = Image.new("L", (boxw, boxh), 0)
+ drawmask = ImageDraw.Draw(imagemask)
+ drawmask.text((-left, -top), icon, font=font_obj, fill=alpha)
+
+ # Create a solid color image and apply the mask
+ if color.startswith('rgba'):
+ color = color.replace('rgba', 'rgb')
+ color = ','.join(color.split(',')[:-1])+')'
+ iconimage = Image.new("RGBA", (boxw, boxh), color)
+ iconimage.putalpha(imagemask)
+
+ # Create output image
+ outimage = Image.new("RGBA", (boxw, size), (0, 0, 0, 0))
+ outimage.paste(iconimage, (left, top))
+
+ # output image
+ output = io.BytesIO()
+ outimage.save(output, format="PNG")
+ response = werkzeug.wrappers.Response()
+ response.mimetype = 'image/png'
+ response.data = output.getvalue()
+ response.headers['Cache-Control'] = 'public, max-age=604800'
+ response.headers['Access-Control-Allow-Origin'] = '*'
+ response.headers['Access-Control-Allow-Methods'] = 'GET, POST'
+ response.headers['Connection'] = 'close'
+ response.headers['Date'] = time.strftime("%a, %d-%b-%Y %T GMT", time.gmtime())
+ response.headers['Expires'] = time.strftime("%a, %d-%b-%Y %T GMT", time.gmtime(time.time()+604800*60))
+
+ return response
+
+ #------------------------------------------------------
+ # Update a checklist in the editor on check/uncheck
+ #------------------------------------------------------
+ @http.route('/web_editor/checklist', type='json', auth='user')
+ def update_checklist(self, res_model, res_id, filename, checklistId, checked, **kwargs):
+ record = request.env[res_model].browse(res_id)
+ value = getattr(record, filename, False)
+ htmlelem = etree.fromstring("<div>%s</div>" % value, etree.HTMLParser())
+ checked = bool(checked)
+
+ li = htmlelem.find(".//li[@id='checklist-id-" + str(checklistId) + "']")
+
+ if not li or not self._update_checklist_recursive(li, checked, children=True, ancestors=True):
+ return value
+
+ value = etree.tostring(htmlelem[0][0], encoding='utf-8', method='html')[5:-6]
+ record.write({filename: value})
+
+ return value
+
+ def _update_checklist_recursive (self, li, checked, children=False, ancestors=False):
+ if 'checklist-id-' not in li.get('id', ''):
+ return False
+
+ classname = li.get('class', '')
+ if ('o_checked' in classname) == checked:
+ return False
+
+ # check / uncheck
+ if checked:
+ classname = '%s o_checked' % classname
+ else:
+ classname = re.sub(r"\s?o_checked\s?", '', classname)
+ li.set('class', classname)
+
+ # propagate to children
+ if children:
+ node = li.getnext()
+ ul = None
+ if node is not None:
+ if node.tag == 'ul':
+ ul = node
+ if node.tag == 'li' and len(node.getchildren()) == 1 and node.getchildren()[0].tag == 'ul':
+ ul = node.getchildren()[0]
+
+ if ul is not None:
+ for child in ul.getchildren():
+ if child.tag == 'li':
+ self._update_checklist_recursive(child, checked, children=True)
+
+ # propagate to ancestors
+ if ancestors:
+ allSelected = True
+ ul = li.getparent()
+ if ul.tag == 'li':
+ ul = ul.getparent()
+
+ for child in ul.getchildren():
+ if child.tag == 'li' and 'checklist-id' in child.get('id', '') and 'o_checked' not in child.get('class', ''):
+ allSelected = False
+
+ node = ul.getprevious()
+ if node is None:
+ node = ul.getparent().getprevious()
+ if node is not None and node.tag == 'li':
+ self._update_checklist_recursive(node, allSelected, ancestors=True)
+
+ return True
+
+ @http.route('/web_editor/attachment/add_data', type='json', auth='user', methods=['POST'], website=True)
+ def add_data(self, name, data, quality=0, width=0, height=0, res_id=False, res_model='ir.ui.view', **kwargs):
+ try:
+ data = tools.image_process(data, size=(width, height), quality=quality, verify_resolution=True)
+ except UserError:
+ pass # not an image
+ self._clean_context()
+ attachment = self._attachment_create(name=name, data=data, res_id=res_id, res_model=res_model)
+ return attachment._get_media_info()
+
+ @http.route('/web_editor/attachment/add_url', type='json', auth='user', methods=['POST'], website=True)
+ def add_url(self, url, res_id=False, res_model='ir.ui.view', **kwargs):
+ self._clean_context()
+ attachment = self._attachment_create(url=url, res_id=res_id, res_model=res_model)
+ return attachment._get_media_info()
+
+ @http.route('/web_editor/attachment/remove', type='json', auth='user', website=True)
+ def remove(self, ids, **kwargs):
+ """ Removes a web-based image attachment if it is used by no view (template)
+
+ Returns a dict mapping attachments which would not be removed (if any)
+ mapped to the views preventing their removal
+ """
+ self._clean_context()
+ Attachment = attachments_to_remove = request.env['ir.attachment']
+ Views = request.env['ir.ui.view']
+
+ # views blocking removal of the attachment
+ removal_blocked_by = {}
+
+ for attachment in Attachment.browse(ids):
+ # in-document URLs are html-escaped, a straight search will not
+ # find them
+ url = tools.html_escape(attachment.local_url)
+ views = Views.search([
+ "|",
+ ('arch_db', 'like', '"%s"' % url),
+ ('arch_db', 'like', "'%s'" % url)
+ ])
+
+ if views:
+ removal_blocked_by[attachment.id] = views.read(['name'])
+ else:
+ attachments_to_remove += attachment
+ if attachments_to_remove:
+ attachments_to_remove.unlink()
+ return removal_blocked_by
+
+ @http.route('/web_editor/get_image_info', type='json', auth='user', website=True)
+ def get_image_info(self, src=''):
+ """This route is used to determine the original of an attachment so that
+ it can be used as a base to modify it again (crop/optimization/filters).
+ """
+ attachment = None
+ id_match = re.search('^/web/image/([^/?]+)', src)
+ if id_match:
+ url_segment = id_match.group(1)
+ number_match = re.match('^(\d+)', url_segment)
+ if '.' in url_segment: # xml-id
+ attachment = request.env['ir.http']._xmlid_to_obj(request.env, url_segment)
+ elif number_match: # numeric id
+ attachment = request.env['ir.attachment'].browse(int(number_match.group(1)))
+ else:
+ # Find attachment by url. There can be multiple matches because of default
+ # snippet images referencing the same image in /static/, so we limit to 1
+ attachment = request.env['ir.attachment'].search([
+ ('url', '=like', src),
+ ('mimetype', 'in', SUPPORTED_IMAGE_MIMETYPES),
+ ], limit=1)
+ if not attachment:
+ return {
+ 'attachment': False,
+ 'original': False,
+ }
+ return {
+ 'attachment': attachment.read(['id'])[0],
+ 'original': (attachment.original_id or attachment).read(['id', 'image_src', 'mimetype'])[0],
+ }
+
+ def _attachment_create(self, name='', data=False, url=False, res_id=False, res_model='ir.ui.view'):
+ """Create and return a new attachment."""
+ if name.lower().endswith('.bmp'):
+ # Avoid mismatch between content type and mimetype, see commit msg
+ name = name[:-4]
+
+ if not name and url:
+ name = url.split("/").pop()
+
+ if res_model != 'ir.ui.view' and res_id:
+ res_id = int(res_id)
+ else:
+ res_id = False
+
+ attachment_data = {
+ 'name': name,
+ 'public': res_model == 'ir.ui.view',
+ 'res_id': res_id,
+ 'res_model': res_model,
+ }
+
+ if data:
+ attachment_data['datas'] = data
+ elif url:
+ attachment_data.update({
+ 'type': 'url',
+ 'url': url,
+ })
+ else:
+ raise UserError(_("You need to specify either data or url to create an attachment."))
+
+ attachment = request.env['ir.attachment'].create(attachment_data)
+ return attachment
+
+ def _clean_context(self):
+ # avoid allowed_company_ids which may erroneously restrict based on website
+ context = dict(request.context)
+ context.pop('allowed_company_ids', None)
+ request.context = context
+
+ @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(['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id'])
+
+ scss_files_data_by_bundle = []
+ js_files_data_by_bundle = []
+
+ if get_scss:
+ 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 {
+ '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']
+
+ files_data_by_bundle = []
+ resources_type_info = {'t_call_assets_attribute': 't-js', 'mimetype': 'text/javascript'}
+ if file_type == 'scss':
+ resources_type_info = {'t_call_assets_attribute': 't-css', 'mimetype': 'text/scss'}
+
+ # Compile regex outside of the loop
+ # This will used to exclude library scss files from the result
+ excluded_url_matcher = re.compile("^(.+/lib/.+)|(.+import_bootstrap.+\.scss)$")
+
+ # 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(resources_type_info['t_call_assets_attribute']) == "false":
+ continue
+ asset_name = asset_call_node.get("t-call-assets")
+
+ # Loop through bundle files to search for file info
+ files_data = []
+ for file_info in request.env["ir.qweb"]._get_asset_content(asset_name, {})[0]:
+ if file_info["atype"] != resources_type_info['mimetype']:
+ 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
+ file_data = AssetsUtils.get_asset_info(url)
+ if not file_data:
+ continue
+
+ # Save info according to the filter (arch will be fetched later)
+ url_infos[url] = file_data
+
+ if '/user_custom_' in url \
+ or file_data['customized'] \
+ or file_type == 'scss' and not only_user_custom_files:
+ files_data.append(url)
+
+ # scss data is returned sorted by bundle, with the bundles
+ # names and xmlids
+ if len(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
+ 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)):
+ bundle_2 = files_data_by_bundle[j]
+ # In unwanted bundles, keep only the files which are in wanted bundles too (_assets_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(files_data_by_bundle)):
+ 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
+ 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
+ files_data_by_bundle = [
+ data for data in 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
+ 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 = 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
+
+ @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)
+
+ @http.route("/web_editor/public_render_template", type="json", auth="public", website=True)
+ def public_render_template(self, args):
+ # 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']
+ if xmlid in request.env['web_editor.assets']._get_public_asset_xmlids():
+ # For white listed assets, bypass access verification
+ # TODO in master this part should be removed and simply use the
+ # public group on the related views instead. And then let the normal
+ # flow handle the rendering.
+ return View.sudo()._render_template(xmlid, {k: values[k] for k in values if k in trusted_value_keys})
+ # Otherwise use normal flow
+ 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):
+ """
+ Creates a modified copy of an attachment and returns its image_src to be
+ inserted into the DOM.
+ """
+ fields = {
+ 'original_id': attachment.id,
+ 'datas': data,
+ 'type': 'binary',
+ 'res_model': res_model or 'ir.ui.view',
+ }
+ if fields['res_model'] == 'ir.ui.view':
+ fields['res_id'] = 0
+ elif res_id:
+ fields['res_id'] = res_id
+ if name:
+ fields['name'] = name
+ attachment = attachment.copy(fields)
+ if attachment.url:
+ # Don't keep url if modifying static attachment because static images
+ # are only served from disk and don't fallback to attachments.
+ if re.match(r'^/\w+/static/', attachment.url):
+ attachment.url = None
+ # Uniquify url by adding a path segment with the id before the name.
+ # This allows us to keep the unsplash url format so it still reacts
+ # to the unsplash beacon.
+ else:
+ url_fragments = attachment.url.split('/')
+ url_fragments.insert(-1, str(attachment.id))
+ attachment.url = '/'.join(url_fragments)
+ if attachment.public:
+ return attachment.image_src
+ attachment.generate_access_token()
+ return '%s?access_token=%s' % (attachment.image_src, attachment.access_token)
+
+ @http.route(['/web_editor/shape/<module>/<path:filename>'], type='http', auth="public", website=True)
+ def shape(self, module, filename, **kwargs):
+ """
+ Returns a color-customized svg (background shape or illustration).
+ """
+ svg = None
+ if module == 'illustration':
+ attachment = request.env['ir.attachment'].sudo().search([('url', '=like', request.httprequest.path), ('public', '=', True)], limit=1)
+ if not attachment:
+ raise werkzeug.exceptions.NotFound()
+ svg = b64decode(attachment.datas).decode('utf-8')
+ else:
+ shape_path = get_resource_path(module, 'static', 'shapes', filename)
+ if not shape_path:
+ raise werkzeug.exceptions.NotFound()
+ with tools.file_open(shape_path, 'r') as file:
+ svg = file.read()
+
+ user_colors = []
+ for key, value in kwargs.items():
+ colorMatch = re.match('^c([1-5])$', key)
+ if colorMatch:
+ # Check that color is hex or rgb(a) to prevent arbitrary injection
+ if not re.match(r'(?i)^#[0-9A-F]{6,8}$|^rgba?\(\d{1,3},\d{1,3},\d{1,3}(?:,[0-9.]{1,4})?\)$', value.replace(' ', '')):
+ raise werkzeug.exceptions.BadRequest()
+ user_colors.append([tools.html_escape(value), colorMatch.group(1)])
+ elif key == 'flip':
+ if value == 'x':
+ svg = svg.replace('<svg ', '<svg style="transform: scaleX(-1);" ')
+ elif value == 'y':
+ svg = svg.replace('<svg ', '<svg style="transform: scaleY(-1)" ')
+ elif value == 'xy':
+ svg = svg.replace('<svg ', '<svg style="transform: scale(-1)" ')
+
+ default_palette = {
+ '1': '#3AADAA',
+ '2': '#7C6576',
+ '3': '#F6F6F6',
+ '4': '#FFFFFF',
+ '5': '#383E45',
+ }
+ color_mapping = {default_palette[palette_number]: color for color, palette_number in user_colors}
+ # create a case-insensitive regex to match all the colors to replace, eg: '(?i)(#3AADAA)|(#7C6576)'
+ regex = '(?i)%s' % '|'.join('(%s)' % color for color in color_mapping.keys())
+
+ def subber(match):
+ key = match.group().upper()
+ return color_mapping[key] if key in color_mapping else key
+ svg = re.sub(regex, subber, svg)
+
+ return request.make_response(svg, [
+ ('Content-type', 'image/svg+xml'),
+ ('Cache-control', 'max-age=%s' % http.STATIC_CACHE_LONG),
+ ])
+
+ @http.route(['/web_editor/media_library_search'], type='json', auth="user", website=True)
+ def media_library_search(self, **params):
+ ICP = request.env['ir.config_parameter'].sudo()
+ endpoint = ICP.get_param('web_editor.media_library_endpoint', DEFAULT_LIBRARY_ENDPOINT)
+ params['dbuuid'] = ICP.get_param('database.uuid')
+ response = requests.post('%s/media-library/1/search' % endpoint, data=params)
+ if response.status_code == requests.codes.ok and response.headers['content-type'] == 'application/json':
+ return response.json()
+ else:
+ return {'error': response.status_code}
+
+ @http.route('/web_editor/save_library_media', type='json', auth='user', methods=['POST'])
+ def save_library_media(self, media):
+ """
+ Saves images from the media library as new attachments, making them
+ dynamic SVGs if needed.
+ media = {
+ <media_id>: {
+ 'query': 'space separated search terms',
+ 'is_dynamic_svg': True/False,
+ }, ...
+ }
+ """
+ attachments = []
+ ICP = request.env['ir.config_parameter'].sudo()
+ library_endpoint = ICP.get_param('web_editor.media_library_endpoint', DEFAULT_LIBRARY_ENDPOINT)
+
+ media_ids = ','.join(media.keys())
+ params = {
+ 'dbuuid': ICP.get_param('database.uuid'),
+ 'media_ids': media_ids,
+ }
+ response = requests.post('%s/media-library/1/download_urls' % library_endpoint, data=params)
+ if response.status_code != requests.codes.ok:
+ raise Exception(_("ERROR: couldn't get download urls from media library."))
+
+ for id, url in response.json().items():
+ req = requests.get(url)
+ name = '_'.join([media[id]['query'], url.split('/')[-1]])
+ # Need to bypass security check to write image with mimetype image/svg+xml
+ # ok because svgs come from whitelisted origin
+ context = {'binary_field_real_user': request.env['res.users'].sudo().browse([SUPERUSER_ID])}
+ attachment = request.env['ir.attachment'].sudo().with_context(context).create({
+ 'name': name,
+ 'mimetype': req.headers['content-type'],
+ 'datas': b64encode(req.content),
+ 'public': True,
+ 'res_model': 'ir.ui.view',
+ 'res_id': 0,
+ })
+ if media[id]['is_dynamic_svg']:
+ attachment['url'] = '/web_editor/shape/illustration/%s' % slug(attachment)
+ attachments.append(attachment._get_media_info())
+
+ return attachments