Module presalytics.lib.templates.base
Expand source code Browse git
import os
import typing
import jinja2
import presalytics.story.outline
import presalytics.story.components
import presalytics.lib.util as util
if typing.TYPE_CHECKING:
from presalytics.story.components import WidgetBase
from presalytics.story.outline import Page
class WidgetPage(presalytics.story.components.PageTemplateBase):
__component_kind__ = 'widget-page'
def __init__(self, page: 'Page', **kwargs):
super(WidgetPage, self).__init__(page, **kwargs)
@classmethod
def deserialize(cls, component, **kwargs):
return cls(component, **kwargs)
def serialize(self):
return self.outline_page
def render(self, **kwargs):
return self.widgets[0].to_html()
def htmlize(widget):
"""
Jinja filter to render a widget to a html string
"""
html = widget.to_html()
try:
html = html.decode('utf-8')
except Exception:
pass
return html
class JinjaTemplateBuilder(presalytics.story.components.PageTemplateBase):
"""
Base class for building objects that render html from `presalytics.story.outline.Page`
objects that implementing the [Jinja2](https://jinja.palletsprojects.com/)
python library.
Instances of this class will look for templates located at file location identified by the
`__template_file__` attribute. If not template is found, the content in the `template_string`
attribute will be used. Templates should call each widget's `to_html()` method in a placeholder
in order to generate valid html for the page.
*About building templates*: Templates are passed a `widgets` attribute and a `widget_index`
integer (initialized at 0) as part of the context during rendering,. To render
multiple widgets on a page, the following pattern can be used inside of templates to
increment through the widgets as the jinja2 rendering engine moves through the template:
{{ widgets[widget_index.next()].to_html() }} // renders widget and increments widget_index
Please also note that if `<script>` tags are included in the template, they will be
stripped out downstream by a `presalytics.story.components.Renderer` for security
reasons. Scripts included in templates will not make it to the browser.
Parameters
----------
page : presalytics.story.outline.Page
The page to be rendered
Attributes
----------
__template_file__ : str
The filename to an html file containing a fragment that will be rendered into
a page by a `presalytics.story.components.Renderer`
__template_paths__ : list of str
user-defined filepaths to directories where Jinja2 should look for the `__template_file__`
__css__ : list of str
Each str in this list is a key that maps to an entry in the
`presalytics.lib.plugins.local.LocalStylesPlugin.LOCAL_STYLES_MAP`. Ids matched here
load the css files as a dependent plugin.
template_paths : list of str
The folders Jinja2 will look in. Includes that html directory adjacent to this `__file__`,
appending with the files in the `__template_paths__`
widgets : list of subclass instances of presalytics.story.components.WidgetBase
Widget to be rendered into the placeholders in the template identified by `__template_file__`
"""
__css__: typing.Sequence[str]
__template_file__: str
__template_paths__: typing.List[str]
template_paths: typing.List[str]
__template_paths__ = []
__css__ = []
class WidgetIndexer(object):
"""
A counter class for
call `widgetindex.next()` in html templates to move this the widget list
"""
def __init__(self):
self._val = 0
def next(self):
"""
Returns the current widget index and increments the value for the next call
"""
cur = self._val
self._val += 1
return cur
def __init__(self, page: 'Page', **kwargs) -> None:
super().__init__(page, **kwargs)
pkg_templates = os.path.join(os.path.dirname(__file__), "html")
self.template_paths = [pkg_templates, os.getcwd()]
if len(self.__template_paths__) > 0:
self.template_paths[0:0] = self.__template_paths__
self.is_template_local = self.check_for_file()
if self.is_template_local:
self.template_string = self.read_template_string()
elif self.outline_page.additional_properties.get("template_string", None):
self.template_string = self.outline_page.additional_properties.get("template_string")
else:
self.template_string = None
if len(kwargs.keys()) > 0:
self.outline_page.additional_properties.update(kwargs)
@classmethod
def deserialize(cls, component, **kwargs):
return cls(component, **kwargs)
@util.classproperty
def __plugins__(cls):
plugin_list = []
for id in cls.__css__:
new_item = {
'kind': 'style',
'name': 'local',
'config': {
'css_file_id': id
}
}
plugin_list.append(new_item)
return plugin_list
def serialize(self):
updated_plugins = []
for plugin_data in self.__plugins__:
updated_plugins.append(presalytics.story.outline.Plugin(**plugin_data))
self.outline_page.plugins = updated_plugins
if self.template_string:
self.outline_page.additional_properties["template_string"] = self.template_string
return self.outline_page
def get_template_name(self):
"""
Requires subclasses have either a `__template_file__` class property, or override this method
"""
if self.__template_file__:
return self.__template_file__
else:
raise NotImplementedError
def render(self, **kwargs):
"""
Renders the widgets to html
"""
if self.is_template_local:
return self.render_from_file(**kwargs)
elif self.template_string:
return self.render_from_backup_string(**kwargs)
else:
raise presalytics.lib.exceptions.MissingConfigException("Missing __template_file__: {}".format(self.get_template_name()))
def _make_context(self):
context = {
"widgets": self.widgets,
"widget_index": self.WidgetIndexer()
}
if self.outline_page.additional_properties:
context.update(self.outline_page.additional_properties)
context.pop("template_string", None)
return context
def render_from_file(self, **kwargs) -> str:
"""
Returns rendered html with widgets rendered into template placeholders
"""
template = self.load_jinja_template()
context = self._make_context()
return template.render(**context)
def render_from_backup_string(self, **kwargs):
"""
Returns rendered html with widgets rendered into template placeholders
"""
context = self._make_context()
options = {
"loader": jinja2.BaseLoader()
}
if context.get("jinja_options", None):
options.update(context.pop("jinja_options"))
env = jinja2.Environment(**options)
env.filters['htmlize'] = htmlize
template = env.from_string(self.template_string)
return template.render(**context)
def read_template_string(self) -> typing.Optional[str]:
"""
Finds the template file and reads it into a string
"""
for _dir in self.template_paths:
fpath = os.path.join(_dir, self.get_template_name())
if os.path.exists(fpath):
with open(fpath, 'r') as f:
template_string = f.read()
return template_string
return None
def load_jinja_template(self) -> jinja2.Template:
"""
Uses the fileloader to load a local template file into the jinja2 environment
"""
loader = jinja2.FileSystemLoader(self.template_paths)
env = jinja2.Environment(loader=loader)
env.filters['htmlize'] = htmlize
return env.get_template(self.get_template_name())
def check_for_file(self):
"""
Checks whether `__template_file__` exists locally
"""
for _dir in self.template_paths:
fpath = os.path.join(_dir, self.get_template_name())
if os.path.exists(fpath):
return True
return False
class TitleWithSingleItem(JinjaTemplateBuilder):
"""
Simple single-widget page with a title
"""
__component_kind__ = 'TitleWithSingleItem'
__css__ = ['flex_row', 'light_grey', 'responsive_title']
__template_file__ = 'title_with_single_widget.html'
class TwoUpWithTitle(JinjaTemplateBuilder):
"""
View two widgets side-by-side on one page
"""
__component_kind__ = "TwoUpWithTitle"
__css__ = ['flex_row', 'light_grey', 'responsive_title']
__template_file__ = 'two_up_with_title.html'
class BootstrapCustomTemplate(JinjaTemplateBuilder):
"""
Build customer repsonsive tempaltes using bootstrap to layout widgets
The `presalytics.story.outline.Page` must contain an entry named "template_file" in the its data dictionary.
The values of the "template_file" varialble must a file path to an html file in the
current working directory
Parameters:
----------
name: str
the page instance name
page: presalytics.story.outline.Page
A story outline page object to create the widget from
template_file: str
A local file path to an html file that wil be used as a template for rendering the page.
The file path is relative to the current working directory.
kwargs: dict, optional
**kwargs can include parameters to pass to the template rendering context. For example,
when kwargs is passed `title="An Example Title"`, during rendering, the template's
`{{title}}` place holder will be replaced with with `An Example Title`
"""
template_file: str
__component_kind__ = "bootstrap-custom"
__plugins__ = [
{
'name': 'external_scripts',
'kind': 'script',
'config': {
'approved_scripts_key': 'jquery'
}
},
{
'name': 'external_scripts',
'kind': 'script',
'config': {
'approved_scripts_key': 'popper'
}
},
{
'name': 'external_scripts',
'kind': 'script',
'config': {
'approved_scripts_key': 'bootstrap4'
}
},
{
'name': 'external_links',
'kind': 'style',
'config': {
'approved_styles_key': 'bootstrap4'
}
},
]
def __init__(self, page: 'Page', name=None, template_file=None, **kwargs) -> None:
try:
self.template_file = template_file
if not self.template_file:
self.template_file = page.additional_properties["template_file"]
except (KeyError, AttributeError):
raise presalytics.lib.exceptions.InvalidConfigurationError(message="BootstrapCustomTemplate requires a 'template_file' passed either via 'additional_properties` or a keyword argument")
super(BootstrapCustomTemplate, self).__init__(page, **kwargs)
self.outline_page.additional_properties["template_file"] = self.template_file
self.name = name
if not self.name:
self.name = page.name
self.outline_page.name = self.name
self.outline_page.kind = self.__component_kind__
def get_template_name(self):
return self.template_file
def serialize(self):
if self.template_string:
self.outline_page.additional_properties["template_string"] = self.template_string
return self.outline_page
Functions
def htmlize(widget)
-
Jinja filter to render a widget to a html string
Expand source code Browse git
def htmlize(widget): """ Jinja filter to render a widget to a html string """ html = widget.to_html() try: html = html.decode('utf-8') except Exception: pass return html
Classes
class WidgetPage (page, **kwargs)
-
Inherit from this base class to render templates to html via the
Revealer
class.Parameters
page
:Page
- A presalytics.story.outline.Page object for instalizing the class
Attributes
outline_page
:Page
- The page data
widgets
:list
ofsubclasses
ofWidgetBase
- A list widget that will be loaded into templates and rendered via placeholders. These widgets must have a "to_html(self, data, **kwargs)" method.
Expand source code Browse git
class WidgetPage(presalytics.story.components.PageTemplateBase): __component_kind__ = 'widget-page' def __init__(self, page: 'Page', **kwargs): super(WidgetPage, self).__init__(page, **kwargs) @classmethod def deserialize(cls, component, **kwargs): return cls(component, **kwargs) def serialize(self): return self.outline_page def render(self, **kwargs): return self.widgets[0].to_html()
Ancestors
- PageTemplateBase
- ComponentBase
- abc.ABC
Inherited members
class JinjaTemplateBuilder (page, **kwargs)
-
Base class for building objects that render html from
Page
objects that implementing the Jinja2 python library.Instances of this class will look for templates located at file location identified by the
__template_file__
attribute. If not template is found, the content in thetemplate_string
attribute will be used. Templates should call each widget'sto_html()
method in a placeholder in order to generate valid html for the page.About building templates: Templates are passed a
widgets
attribute and awidget_index
integer (initialized at 0) as part of the context during rendering,. To render multiple widgets on a page, the following pattern can be used inside of templates to increment through the widgets as the jinja2 rendering engine moves through the template:{{ widgets[widget_index.next()].to_html() }} // renders widget and increments widget_index
Please also note that if
<script>
tags are included in the template, they will be stripped out downstream by aRenderer
for security reasons. Scripts included in templates will not make it to the browser.Parameters
page
:Page
- The page to be rendered
Attributes
__template_file__
:str
- The filename to an html file containing a fragment that will be rendered into
a page by a
Renderer
__template_paths__
:list
ofstr
- user-defined filepaths to directories where Jinja2 should look for the
__template_file__
__css__
:list
ofstr
- Each str in this list is a key that maps to an entry in the
LocalStylesPlugin.LOCAL_STYLES_MAP
. Ids matched here load the css files as a dependent plugin. template_paths
:list
ofstr
- The folders Jinja2 will look in. Includes that html directory adjacent to this
__file__
, appending with the files in the__template_paths__
widgets
:list
ofsubclass
instances
ofWidgetBase
- Widget to be rendered into the placeholders in the template identified by
__template_file__
Expand source code Browse git
class JinjaTemplateBuilder(presalytics.story.components.PageTemplateBase): """ Base class for building objects that render html from `presalytics.story.outline.Page` objects that implementing the [Jinja2](https://jinja.palletsprojects.com/) python library. Instances of this class will look for templates located at file location identified by the `__template_file__` attribute. If not template is found, the content in the `template_string` attribute will be used. Templates should call each widget's `to_html()` method in a placeholder in order to generate valid html for the page. *About building templates*: Templates are passed a `widgets` attribute and a `widget_index` integer (initialized at 0) as part of the context during rendering,. To render multiple widgets on a page, the following pattern can be used inside of templates to increment through the widgets as the jinja2 rendering engine moves through the template: {{ widgets[widget_index.next()].to_html() }} // renders widget and increments widget_index Please also note that if `<script>` tags are included in the template, they will be stripped out downstream by a `presalytics.story.components.Renderer` for security reasons. Scripts included in templates will not make it to the browser. Parameters ---------- page : presalytics.story.outline.Page The page to be rendered Attributes ---------- __template_file__ : str The filename to an html file containing a fragment that will be rendered into a page by a `presalytics.story.components.Renderer` __template_paths__ : list of str user-defined filepaths to directories where Jinja2 should look for the `__template_file__` __css__ : list of str Each str in this list is a key that maps to an entry in the `presalytics.lib.plugins.local.LocalStylesPlugin.LOCAL_STYLES_MAP`. Ids matched here load the css files as a dependent plugin. template_paths : list of str The folders Jinja2 will look in. Includes that html directory adjacent to this `__file__`, appending with the files in the `__template_paths__` widgets : list of subclass instances of presalytics.story.components.WidgetBase Widget to be rendered into the placeholders in the template identified by `__template_file__` """ __css__: typing.Sequence[str] __template_file__: str __template_paths__: typing.List[str] template_paths: typing.List[str] __template_paths__ = [] __css__ = [] class WidgetIndexer(object): """ A counter class for call `widgetindex.next()` in html templates to move this the widget list """ def __init__(self): self._val = 0 def next(self): """ Returns the current widget index and increments the value for the next call """ cur = self._val self._val += 1 return cur def __init__(self, page: 'Page', **kwargs) -> None: super().__init__(page, **kwargs) pkg_templates = os.path.join(os.path.dirname(__file__), "html") self.template_paths = [pkg_templates, os.getcwd()] if len(self.__template_paths__) > 0: self.template_paths[0:0] = self.__template_paths__ self.is_template_local = self.check_for_file() if self.is_template_local: self.template_string = self.read_template_string() elif self.outline_page.additional_properties.get("template_string", None): self.template_string = self.outline_page.additional_properties.get("template_string") else: self.template_string = None if len(kwargs.keys()) > 0: self.outline_page.additional_properties.update(kwargs) @classmethod def deserialize(cls, component, **kwargs): return cls(component, **kwargs) @util.classproperty def __plugins__(cls): plugin_list = [] for id in cls.__css__: new_item = { 'kind': 'style', 'name': 'local', 'config': { 'css_file_id': id } } plugin_list.append(new_item) return plugin_list def serialize(self): updated_plugins = [] for plugin_data in self.__plugins__: updated_plugins.append(presalytics.story.outline.Plugin(**plugin_data)) self.outline_page.plugins = updated_plugins if self.template_string: self.outline_page.additional_properties["template_string"] = self.template_string return self.outline_page def get_template_name(self): """ Requires subclasses have either a `__template_file__` class property, or override this method """ if self.__template_file__: return self.__template_file__ else: raise NotImplementedError def render(self, **kwargs): """ Renders the widgets to html """ if self.is_template_local: return self.render_from_file(**kwargs) elif self.template_string: return self.render_from_backup_string(**kwargs) else: raise presalytics.lib.exceptions.MissingConfigException("Missing __template_file__: {}".format(self.get_template_name())) def _make_context(self): context = { "widgets": self.widgets, "widget_index": self.WidgetIndexer() } if self.outline_page.additional_properties: context.update(self.outline_page.additional_properties) context.pop("template_string", None) return context def render_from_file(self, **kwargs) -> str: """ Returns rendered html with widgets rendered into template placeholders """ template = self.load_jinja_template() context = self._make_context() return template.render(**context) def render_from_backup_string(self, **kwargs): """ Returns rendered html with widgets rendered into template placeholders """ context = self._make_context() options = { "loader": jinja2.BaseLoader() } if context.get("jinja_options", None): options.update(context.pop("jinja_options")) env = jinja2.Environment(**options) env.filters['htmlize'] = htmlize template = env.from_string(self.template_string) return template.render(**context) def read_template_string(self) -> typing.Optional[str]: """ Finds the template file and reads it into a string """ for _dir in self.template_paths: fpath = os.path.join(_dir, self.get_template_name()) if os.path.exists(fpath): with open(fpath, 'r') as f: template_string = f.read() return template_string return None def load_jinja_template(self) -> jinja2.Template: """ Uses the fileloader to load a local template file into the jinja2 environment """ loader = jinja2.FileSystemLoader(self.template_paths) env = jinja2.Environment(loader=loader) env.filters['htmlize'] = htmlize return env.get_template(self.get_template_name()) def check_for_file(self): """ Checks whether `__template_file__` exists locally """ for _dir in self.template_paths: fpath = os.path.join(_dir, self.get_template_name()) if os.path.exists(fpath): return True return False
Ancestors
- PageTemplateBase
- ComponentBase
- abc.ABC
Subclasses
Class variables
var WidgetIndexer
-
A counter class for
call
widgetindex.next()
in html templates to move this the widget listExpand source code Browse git
class WidgetIndexer(object): """ A counter class for call `widgetindex.next()` in html templates to move this the widget list """ def __init__(self): self._val = 0 def next(self): """ Returns the current widget index and increments the value for the next call """ cur = self._val self._val += 1 return cur
Methods
def get_template_name(self)
-
Requires subclasses have either a
__template_file__
class property, or override this methodExpand source code Browse git
def get_template_name(self): """ Requires subclasses have either a `__template_file__` class property, or override this method """ if self.__template_file__: return self.__template_file__ else: raise NotImplementedError
def render(self, **kwargs)
-
Renders the widgets to html
Expand source code Browse git
def render(self, **kwargs): """ Renders the widgets to html """ if self.is_template_local: return self.render_from_file(**kwargs) elif self.template_string: return self.render_from_backup_string(**kwargs) else: raise presalytics.lib.exceptions.MissingConfigException("Missing __template_file__: {}".format(self.get_template_name()))
def render_from_file(self, **kwargs)
-
Returns rendered html with widgets rendered into template placeholders
Expand source code Browse git
def render_from_file(self, **kwargs) -> str: """ Returns rendered html with widgets rendered into template placeholders """ template = self.load_jinja_template() context = self._make_context() return template.render(**context)
def render_from_backup_string(self, **kwargs)
-
Returns rendered html with widgets rendered into template placeholders
Expand source code Browse git
def render_from_backup_string(self, **kwargs): """ Returns rendered html with widgets rendered into template placeholders """ context = self._make_context() options = { "loader": jinja2.BaseLoader() } if context.get("jinja_options", None): options.update(context.pop("jinja_options")) env = jinja2.Environment(**options) env.filters['htmlize'] = htmlize template = env.from_string(self.template_string) return template.render(**context)
def read_template_string(self)
-
Finds the template file and reads it into a string
Expand source code Browse git
def read_template_string(self) -> typing.Optional[str]: """ Finds the template file and reads it into a string """ for _dir in self.template_paths: fpath = os.path.join(_dir, self.get_template_name()) if os.path.exists(fpath): with open(fpath, 'r') as f: template_string = f.read() return template_string return None
def load_jinja_template(self)
-
Uses the fileloader to load a local template file into the jinja2 environment
Expand source code Browse git
def load_jinja_template(self) -> jinja2.Template: """ Uses the fileloader to load a local template file into the jinja2 environment """ loader = jinja2.FileSystemLoader(self.template_paths) env = jinja2.Environment(loader=loader) env.filters['htmlize'] = htmlize return env.get_template(self.get_template_name())
def check_for_file(self)
-
Checks whether
__template_file__
exists locallyExpand source code Browse git
def check_for_file(self): """ Checks whether `__template_file__` exists locally """ for _dir in self.template_paths: fpath = os.path.join(_dir, self.get_template_name()) if os.path.exists(fpath): return True return False
Inherited members
class TitleWithSingleItem (page, **kwargs)
-
Simple single-widget page with a title
Expand source code Browse git
class TitleWithSingleItem(JinjaTemplateBuilder): """ Simple single-widget page with a title """ __component_kind__ = 'TitleWithSingleItem' __css__ = ['flex_row', 'light_grey', 'responsive_title'] __template_file__ = 'title_with_single_widget.html'
Ancestors
Inherited members
class TwoUpWithTitle (page, **kwargs)
-
View two widgets side-by-side on one page
Expand source code Browse git
class TwoUpWithTitle(JinjaTemplateBuilder): """ View two widgets side-by-side on one page """ __component_kind__ = "TwoUpWithTitle" __css__ = ['flex_row', 'light_grey', 'responsive_title'] __template_file__ = 'two_up_with_title.html'
Ancestors
Inherited members
class BootstrapCustomTemplate (page, name=None, template_file=None, **kwargs)
-
Build customer repsonsive tempaltes using bootstrap to layout widgets
The
Page
must contain an entry named "template_file" in the its data dictionary. The values of the "template_file" varialble must a file path to an html file in the current working directoryParameters:
name: str the page instance name
page: presalytics.story.outline.Page A story outline page object to create the widget from
template_file: str A local file path to an html file that wil be used as a template for rendering the page. The file path is relative to the current working directory.
kwargs: dict, optional **kwargs can include parameters to pass to the template rendering context. For example, when kwargs is passed
title="An Example Title"
, during rendering, the template's{{title}}
place holder will be replaced with withAn Example Title
Expand source code Browse git
class BootstrapCustomTemplate(JinjaTemplateBuilder): """ Build customer repsonsive tempaltes using bootstrap to layout widgets The `presalytics.story.outline.Page` must contain an entry named "template_file" in the its data dictionary. The values of the "template_file" varialble must a file path to an html file in the current working directory Parameters: ---------- name: str the page instance name page: presalytics.story.outline.Page A story outline page object to create the widget from template_file: str A local file path to an html file that wil be used as a template for rendering the page. The file path is relative to the current working directory. kwargs: dict, optional **kwargs can include parameters to pass to the template rendering context. For example, when kwargs is passed `title="An Example Title"`, during rendering, the template's `{{title}}` place holder will be replaced with with `An Example Title` """ template_file: str __component_kind__ = "bootstrap-custom" __plugins__ = [ { 'name': 'external_scripts', 'kind': 'script', 'config': { 'approved_scripts_key': 'jquery' } }, { 'name': 'external_scripts', 'kind': 'script', 'config': { 'approved_scripts_key': 'popper' } }, { 'name': 'external_scripts', 'kind': 'script', 'config': { 'approved_scripts_key': 'bootstrap4' } }, { 'name': 'external_links', 'kind': 'style', 'config': { 'approved_styles_key': 'bootstrap4' } }, ] def __init__(self, page: 'Page', name=None, template_file=None, **kwargs) -> None: try: self.template_file = template_file if not self.template_file: self.template_file = page.additional_properties["template_file"] except (KeyError, AttributeError): raise presalytics.lib.exceptions.InvalidConfigurationError(message="BootstrapCustomTemplate requires a 'template_file' passed either via 'additional_properties` or a keyword argument") super(BootstrapCustomTemplate, self).__init__(page, **kwargs) self.outline_page.additional_properties["template_file"] = self.template_file self.name = name if not self.name: self.name = page.name self.outline_page.name = self.name self.outline_page.kind = self.__component_kind__ def get_template_name(self): return self.template_file def serialize(self): if self.template_string: self.outline_page.additional_properties["template_string"] = self.template_string return self.outline_page
Ancestors
Inherited members