From 7f22d9c2233adbc312c1e08e9f24f2ea87dbcce7 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sat, 13 Aug 2016 06:03:12 +0100 Subject: [PATCH] add files http_endpoint --- openlp/core/api/endpoint/__init__.py | 25 ++++ openlp/core/api/endpoint/controller.py | 131 +++++++++++++++++ openlp/core/api/endpoint/core.py | 169 ++++++++++++++++++++++ openlp/core/api/endpoint/pluginhelpers.py | 134 +++++++++++++++++ openlp/core/api/endpoint/service.py | 100 +++++++++++++ openlp/core/api/http/endpoint.py | 77 ++++++++++ 6 files changed, 636 insertions(+) create mode 100644 openlp/core/api/endpoint/__init__.py create mode 100644 openlp/core/api/endpoint/controller.py create mode 100644 openlp/core/api/endpoint/core.py create mode 100644 openlp/core/api/endpoint/pluginhelpers.py create mode 100644 openlp/core/api/endpoint/service.py create mode 100644 openlp/core/api/http/endpoint.py diff --git a/openlp/core/api/endpoint/__init__.py b/openlp/core/api/endpoint/__init__.py new file mode 100644 index 000000000..b1824951e --- /dev/null +++ b/openlp/core/api/endpoint/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The Endpoint class, which provides plugins with a way to serve their own portion of the API +""" +from .pluginhelpers import search, live, service diff --git a/openlp/core/api/endpoint/controller.py b/openlp/core/api/endpoint/controller.py new file mode 100644 index 000000000..d6b156aa9 --- /dev/null +++ b/openlp/core/api/endpoint/controller.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +import logging +import os +import urllib.request +import urllib.error +import json + +from openlp.core.api.http.endpoint import Endpoint +from openlp.core.api.http import requires_auth +from openlp.core.common import Registry, AppLocation, Settings +from openlp.core.lib import ItemCapabilities, create_thumb + +log = logging.getLogger(__name__) + +controller_endpoint = Endpoint('controller') +api_controller_endpoint = Endpoint('api') + + +@api_controller_endpoint.route('controller/live/text') +@controller_endpoint.route('live/text') +def controller_text(request): + """ + Perform an action on the slide controller. + + :param request: the http request - not used + """ + log.debug("controller_text ") + live_controller = Registry().get('live_controller') + current_item = live_controller.service_item + data = [] + if current_item: + for index, frame in enumerate(current_item.get_frames()): + item = {} + # Handle text (songs, custom, bibles) + if current_item.is_text(): + if frame['verseTag']: + item['tag'] = str(frame['verseTag']) + else: + item['tag'] = str(index + 1) + item['text'] = str(frame['text']) + item['html'] = str(frame['html']) + # Handle images, unless a custom thumbnail is given or if thumbnails is disabled + elif current_item.is_image() and not frame.get('image', '') and Settings().value('api/thumbnails'): + item['tag'] = str(index + 1) + thumbnail_path = os.path.join('images', 'thumbnails', frame['title']) + full_thumbnail_path = os.path.join(AppLocation.get_data_path(), thumbnail_path) + # Create thumbnail if it doesn't exists + if not os.path.exists(full_thumbnail_path): + create_thumb(current_item.get_frame_path(index), full_thumbnail_path, False) + Registry().get('image_manager').add_image(full_thumbnail_path, frame['title'], None, 88, 88) + item['img'] = urllib.request.pathname2url(os.path.sep + thumbnail_path) + item['text'] = str(frame['title']) + item['html'] = str(frame['title']) + else: + # Handle presentation etc. + item['tag'] = str(index + 1) + if current_item.is_capable(ItemCapabilities.HasDisplayTitle): + item['title'] = str(frame['display_title']) + if current_item.is_capable(ItemCapabilities.HasNotes): + item['slide_notes'] = str(frame['notes']) + if current_item.is_capable(ItemCapabilities.HasThumbnails) and \ + Settings().value('api/thumbnails'): + # If the file is under our app directory tree send the portion after the match + data_path = AppLocation.get_data_path() + if frame['image'][0:len(data_path)] == data_path: + item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):]) + Registry().get('image_manager').add_image(frame['image'], frame['title'], None, 88, 88) + item['text'] = str(frame['title']) + item['html'] = str(frame['title']) + item['selected'] = (live_controller.selected_row == index) + data.append(item) + json_data = {'results': {'slides': data}} + if current_item: + json_data['results']['item'] = live_controller.service_item.unique_identifier + return json_data + + +@api_controller_endpoint.route('controller/live/set') +@controller_endpoint.route('live/set') +@requires_auth +def controller_set(request): + """ + Perform an action on the slide controller. + + :param request: The action to perform. + """ + event = getattr(Registry().get('live_controller'), 'slidecontroller_live_set') + try: + json_data = request.GET.get('data') + data = int(json.loads(json_data)['request']['id']) + event.emit([data]) + except KeyError: + log.error("Endpoint controller/live/set request id not found") + return {'results': {'success': True}} + + +@api_controller_endpoint.route('/controller/{controller}/{action:next|previous}') +@controller_endpoint.route('/{controller}/{action:next|previous}') +@requires_auth +def controller_direction(request, controller, action): + """ + Handles requests for setting service items in the slide controller +11 + :param request: The http request object. + :param controller: the controller slides forward or backward. + :param action: the controller slides forward or backward. + """ + event = getattr(Registry().get('live_controller'), 'slidecontroller_{controller}_{action}'. + format(controller=controller, action=action)) + event.emit() + return {'results': {'success': True}} diff --git a/openlp/core/api/endpoint/core.py b/openlp/core/api/endpoint/core.py new file mode 100644 index 000000000..bfe8a0e54 --- /dev/null +++ b/openlp/core/api/endpoint/core.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +import logging +import os + +from openlp.core.api.http.endpoint import Endpoint +from openlp.core.api.http import register_endpoint, requires_auth, ROOT_DIR +from openlp.core.common import Registry, UiStrings, translate +from openlp.core.lib import image_to_byte, PluginStatus, StringContent + + +template_dir = os.path.join(ROOT_DIR, 'templates') +static_dir = os.path.join(ROOT_DIR, 'static') +blank_dir = os.path.join(static_dir, 'index') + + +log = logging.getLogger(__name__) + +stage_endpoint = Endpoint('stage', template_dir=template_dir, static_dir=static_dir) +main_endpoint = Endpoint('main', template_dir=template_dir, static_dir=static_dir) +blank_endpoint = Endpoint('', template_dir=template_dir, static_dir=blank_dir) + +FILE_TYPES = { + '.html': 'text/html', + '.css': 'text/css', + '.js': 'application/javascript', + '.jpg': 'image/jpeg', + '.gif': 'image/gif', + '.ico': 'image/x-icon', + '.png': 'image/png' +} + +remote = translate('RemotePlugin.Mobile', 'Remote') +stage = translate('RemotePlugin.Mobile', 'Stage View') +live = translate('RemotePlugin.Mobile', 'Live View') + +TRANSLATED_STRINGS = { + 'app_title': "{main} {remote}".format(main=UiStrings().OLP, remote=remote), + 'stage_title': "{main} {stage}".format(main=UiStrings().OLP, stage=stage), + 'live_title': "{main} {live}".format(main=UiStrings().OLP, live=live), + 'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'), + 'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'), + 'alerts': translate('RemotePlugin.Mobile', 'Alerts'), + 'search': translate('RemotePlugin.Mobile', 'Search'), + 'home': translate('RemotePlugin.Mobile', 'Home'), + 'refresh': translate('RemotePlugin.Mobile', 'Refresh'), + 'blank': translate('RemotePlugin.Mobile', 'Blank'), + 'theme': translate('RemotePlugin.Mobile', 'Theme'), + 'desktop': translate('RemotePlugin.Mobile', 'Desktop'), + 'show': translate('RemotePlugin.Mobile', 'Show'), + 'prev': translate('RemotePlugin.Mobile', 'Prev'), + 'next': translate('RemotePlugin.Mobile', 'Next'), + 'text': translate('RemotePlugin.Mobile', 'Text'), + 'show_alert': translate('RemotePlugin.Mobile', 'Show Alert'), + 'go_live': translate('RemotePlugin.Mobile', 'Go Live'), + 'add_to_service': translate('RemotePlugin.Mobile', 'Add to Service'), + 'add_and_go_to_service': translate('RemotePlugin.Mobile', 'Add & Go to Service'), + 'no_results': translate('RemotePlugin.Mobile', 'No Results'), + 'options': translate('RemotePlugin.Mobile', 'Options'), + 'service': translate('RemotePlugin.Mobile', 'Service'), + 'slides': translate('RemotePlugin.Mobile', 'Slides'), + 'settings': translate('RemotePlugin.Mobile', 'Settings'), +} + + +@stage_endpoint.route('') +def stage_index(request): + """ + Deliver the page for the /stage url + """ + return stage_endpoint.render_template('stage.mako', **TRANSLATED_STRINGS) + + +@main_endpoint.route('') +def main_index(request): + """ + Deliver the page for the /main url + """ + return main_endpoint.render_template('main.mako', **TRANSLATED_STRINGS) + + +@blank_endpoint.route('') +def index(request): + """ + Deliver the page for the / url + :param request: + """ + return blank_endpoint.render_template('index.mako', **TRANSLATED_STRINGS) + + +@blank_endpoint.route('poll') +def poll(request): + """ + Deliver the page for the /poll url + :param request: + """ + return Registry().get('poller').raw_poll() + + +@blank_endpoint.route('api/display/{display:hide|show|blank|theme|desktop}') +@blank_endpoint.route('display/{display:hide|show|blank|theme|desktop}') +@requires_auth +def toggle_display(request, display): + """ + Deliver the functions for the /display url + :param request: the http request - not used + :param display: the display function to be triggered + """ + Registry().get('live_controller').slidecontroller_toggle_display.emit(display) + return {'results': {'success': True}} + + +@blank_endpoint.route('api/plugin/search') +@blank_endpoint.route('plugin/search') +def plugin_search_list(request): + """ + Deliver a list of active plugins that support search + :param request: the http request - not used + """ + searches = [] + for plugin in Registry().get('plugin_manager').plugins: + if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search: + searches.append([plugin.name, str(plugin.text_strings[StringContent.Name]['plural'])]) + return {'results': {'items': searches}} + + +@main_endpoint.route('image') +def main_image(request): + """ + Return the latest display image as a byte stream. + :param request: base path of the URL. Not used but passed by caller + :return: + """ + live_controller = Registry().get('live_controller') + result = { + 'slide_image': 'data:image/png;base64,' + str(image_to_byte(live_controller.slide_image)) + } + return {'results': result} + + +def get_content_type(file_name): + """ + Examines the extension of the file and determines what the content_type should be, defaults to text/plain + Returns the extension and the content_type + + :param file_name: name of file + """ + ext = os.path.splitext(file_name)[1] + content_type = FILE_TYPES.get(ext, 'text/plain') + return ext, content_type diff --git a/openlp/core/api/endpoint/pluginhelpers.py b/openlp/core/api/endpoint/pluginhelpers.py new file mode 100644 index 000000000..6bb0713a1 --- /dev/null +++ b/openlp/core/api/endpoint/pluginhelpers.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +import os +import json +import re +import urllib + +from urllib.parse import urlparse +from webob import Response + +from openlp.core.common import Registry, AppLocation +from openlp.core.lib import PluginStatus, image_to_byte + + +def search(request, plugin_name, log): + """ + Handles requests for searching the plugins + + :param request: The http request object. + :param plugin_name: The plugin name. + :param log: The class log object. + """ + try: + json_data = request.GET.get('data') + text = json.loads(json_data)['request']['text'] + except KeyError: + log.error("Endpoint {text} search request text not found".format(text=plugin_name)) + text = "" + text = urllib.parse.unquote(text) + plugin = Registry().get('plugin_manager').get_plugin_by_name(plugin_name) + if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search: + results = plugin.media_item.search(text, False) + else: + results = [] + return {'results': {'items': results}} + + +def live(request, plugin_name, log): + """ + Handles requests for making live of the plugins + + :param request: The http request object. + :param plugin_name: The plugin name. + :param log: The class log object. + """ + try: + json_data = request.GET.get('data') + request_id = json.loads(json_data)['request']['id'] + except KeyError: + log.error("Endpoint {text} search request text not found".format(text=plugin_name)) + return [] + plugin = Registry().get('plugin_manager').get_plugin_by_name(plugin_name) + if plugin.status == PluginStatus.Active and plugin.media_item: + getattr(plugin.media_item, '{name}_go_live'.format(name=plugin_name)).emit([request_id, True]) + return [] + + +def service(request, plugin_name, log): + """ + Handles requests for adding to a service of the plugins + + :param request: The http request object. + :param plugin_name: The plugin name. + :param log: The class log object. + """ + try: + json_data = request.GET.get('data') + request_id = json.loads(json_data)['request']['id'] + except KeyError: + log.error("Endpoint {plugin} search request text not found".format(plugin=plugin_name)) + return [] + plugin = Registry().get('plugin_manager').get_plugin_by_name(plugin_name) + if plugin.status == PluginStatus.Active and plugin.media_item: + item_id = plugin.media_item.create_item_from_id(request_id) + getattr(plugin.media_item, '{name}_add_to_service'.format(name=plugin_name)).emit([item_id, True]) + return [] + + +def display_thumbnails(request, controller_name, log, dimensions, file_name, slide): + """ + Handles requests for adding a song to the service + + Return an image to a web page based on a URL + :param request: Request object + :param controller_name: which controller is requesting the image + :param log: the logger object + :param dimensions: the image size eg 88x88 + :param file_name: the file name of the image + :param slide: the individual image name + :return: + """ + log.debug('serve thumbnail {cname}/thumbnails{dim}/{fname}/{slide}'.format(cname=controller_name, + dim=dimensions, + fname=file_name, + slide=slide)) + # -1 means use the default dimension in ImageManager + width = -1 + height = -1 + image = None + if dimensions: + match = re.search('(\d+)x(\d+)', dimensions) + if match: + # let's make sure that the dimensions are within reason + width = sorted([10, int(match.group(1)), 1000])[1] + height = sorted([10, int(match.group(2)), 1000])[1] + if controller_name and file_name: + file_name = urllib.parse.unquote(file_name) + if '..' not in file_name: # no hacking please + full_path = os.path.normpath(os.path.join(AppLocation.get_section_data_path(controller_name), + 'thumbnails', file_name, slide)) + if os.path.exists(full_path): + path, just_file_name = os.path.split(full_path) + Registry().get('image_manager').add_image(full_path, just_file_name, None, width, height) + image = Registry().get('image_manager').get_image(full_path, just_file_name, width, height) + return Response(body=image_to_byte(image, False), status=200, content_type='image/png', charset='utf8') diff --git a/openlp/core/api/endpoint/service.py b/openlp/core/api/endpoint/service.py new file mode 100644 index 000000000..f6029c341 --- /dev/null +++ b/openlp/core/api/endpoint/service.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +import logging +import json + +from openlp.core.api.http.endpoint import Endpoint +from openlp.core.api.http import register_endpoint, requires_auth +from openlp.core.common import Registry + + +log = logging.getLogger(__name__) + +service_endpoint = Endpoint('service') +api_service_endpoint = Endpoint('api/service') + + +@api_service_endpoint.route('list') +@service_endpoint.route('list') +def list_service(request): + """ + Handles requests for service items in the service manager + + :param request: The http request object. + """ + return {'results': {'items': get_service_items()}} + + +@api_service_endpoint.route('set') +@service_endpoint.route('set') +@requires_auth +def service_set(request): + """ + Handles requests for setting service items in the service manager + + :param request: The http request object. + """ + event = getattr(Registry().get('service_manager'), 'servicemanager_set_item') + try: + json_data = request.GET.get('data') + data = int(json.loads(json_data)['request']['id']) + event.emit(data) + except KeyError: + log.error("Endpoint service/set request id not found") + return {'results': {'success': True}} + + +@api_service_endpoint.route('{action:next|previous}') +@service_endpoint.route('{action:next|previous}') +@requires_auth +def service_direction(request, action): + """ + Handles requests for setting service items in the service manager + + :param request: The http request object. + :param action: the the service slides forward or backward. + """ + event = getattr(Registry().get('service_manager'), 'servicemanager_{action}_item'.format(action=action)) + event.emit() + return {'results': {'success': True}} + + +def get_service_items(): + """ + Read the service item in use and return the data as a json object + """ + live_controller = Registry().get('live_controller') + service_items = [] + if live_controller.service_item: + current_unique_identifier = live_controller.service_item.unique_identifier + else: + current_unique_identifier = None + for item in Registry().get('service_manager').service_items: + service_item = item['service_item'] + service_items.append({ + 'id': str(service_item.unique_identifier), + 'title': str(service_item.get_display_title()), + 'plugin': str(service_item.name), + 'notes': str(service_item.notes), + 'selected': (service_item.unique_identifier == current_unique_identifier) + }) + return service_items diff --git a/openlp/core/api/http/endpoint.py b/openlp/core/api/http/endpoint.py new file mode 100644 index 000000000..ed6ff9d5c --- /dev/null +++ b/openlp/core/api/http/endpoint.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The Endpoint class, which provides plugins with a way to serve their own portion of the API +""" + +import os + +from mako.template import Template + + +class Endpoint(object): + """ + This is an endpoint for the HTTP API + """ + def __init__(self, url_prefix, template_dir=None, static_dir=None, assets_dir=None): + """ + Create an endpoint with a URL prefix + """ + self.url_prefix = url_prefix + self.static_dir = static_dir + self.template_dir = template_dir + if assets_dir: + self.assets_dir = assets_dir + else: + self.assets_dir = os.path.dirname(os.path.realpath(__file__)) + self.routes = [] + + def add_url_route(self, url, view_func, method): + """ + Add a url route to the list of routes + """ + self.routes.append((url, view_func, method)) + + def route(self, rule, method='GET'): + """ + Set up a URL route + """ + def decorator(func): + """ + Make this a decorator + """ + self.add_url_route(rule, func, method) + return func + return decorator + + def render_template(self, filename, **kwargs): + """ + Render a mako template + """ + if not self.template_dir: + raise Exception('No template directory specified') + path = os.path.abspath(os.path.join(self.template_dir, filename)) + if self.static_dir: + kwargs['static_url'] = '/{prefix}/static'.format(prefix=self.url_prefix) + kwargs['static_url'] = kwargs['static_url'].replace('//', '/') + kwargs['assets_url'] = '/assets' + return Template(filename=path, input_encoding='utf-8').render(**kwargs)