Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin-ability for views #91

Merged
merged 14 commits into from Apr 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions documentation/changelog.rst
Expand Up @@ -9,6 +9,8 @@ v0.4.0 | April XX, 2021
New features
-----------
* Add sensors with CLI command [see `PR #83 <https://github.com/SeitaBV/flexmeasures/pull/83>`_]
* Configure views with ``FLEXMEASURES_LISTED_VIEWS`` [see `PR #91 <https://github.com/SeitaBV/flexmeasures/pull/91>`_]
nhoening marked this conversation as resolved.
Show resolved Hide resolved
* Allow for views and CLI functions to come from plugins [see also `PR #91 <https://github.com/SeitaBV/flexmeasures/pull/91>`_]

Bugfixes
-----------
Expand Down
43 changes: 34 additions & 9 deletions documentation/configuration.rst
Expand Up @@ -6,7 +6,7 @@ Configuration
The following configurations are used by FlexMeasures.

Required settings (e.g. postgres db) are marked with a double star (**).
To enable easier quickstart tutorials, these settings can be set by env vars.
To enable easier quickstart tutorials, these settings can be set by environment variables.
Recommended settings (e.g. mail, redis) are marked by one star (*).

.. note:: FlexMeasures is best configured via a config file. The config file for FlexMeasures can be placed in one of two locations:
Expand All @@ -15,6 +15,7 @@ Recommended settings (e.g. mail, redis) are marked by one star (*).
* in the user's home directory (e.g. ``~/.flexmeasures.cfg`` on Unix). In this case, note the dot at the beginning of the filename!
* in the app's instance directory (e.g. ``/path/to/your/flexmeasures/code/instance/flexmeasures.cfg``\ ). The path to that instance directory is shown to you by running flexmeasures (e.g. ``flexmeasures run``\ ) with required settings missing or otherwise by running ``flexmeasures shell``.


Basic functionality
-------------------

Expand Down Expand Up @@ -51,6 +52,19 @@ and the first month when the domain was under the current owner's administration

Default: ``{"flexmeasures.io": "2021-01"}``


.. _plugin-config:

FLEXMEASURES_PLUGIN_PATHS
^^^^^^^^^^^^^^^^^^^^^^^^^

A list of absolute paths to Blueprint-based plugins for FlexMeasures (e.g. for custom views or CLI functions).
Each plugin path points to a folder, which should contain an ``__init__.py`` file where the Blueprint is defined.
See :ref:`plugins` on what is expected for content.

Default: ``[]``


FLEXMEASURES_DB_BACKUP_PATH
^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand All @@ -65,6 +79,7 @@ Whether to turn on a feature which times requests made through FlexMeasures. Int

Default: ``False``


UI
--

Expand All @@ -89,6 +104,7 @@ Interval in which viewing the queues dashboard refreshes itself, in milliseconds

Default: ``3000`` (3 seconds)


Timing
------

Expand All @@ -113,6 +129,7 @@ The horizon to use when making schedules.

Default: ``timedelta(hours=2 * 24)``


Tokens
------

Expand All @@ -133,7 +150,7 @@ Default: ``None``
MAPBOX_ACCESS_TOKEN
^^^^^^^^^^^^^^^^^^^

Token for accessing the mapbox API (for displaying maps on the dashboard and asset pages). You can learn how to obtain one `here <https://docs.mapbox.com/help/glossary/access-token/>`_
Token for accessing the MapBox API (for displaying maps on the dashboard and asset pages). You can learn how to obtain one `here <https://docs.mapbox.com/help/glossary/access-token/>`_

Default: ``None``

Expand All @@ -144,6 +161,7 @@ Token which external services can use to check on the status of recurring tasks

Default: ``None``


SQLAlchemy
----------

Expand Down Expand Up @@ -172,6 +190,7 @@ Default:
"connect_args": {"options": "-c timezone=utc"},
}


Security
--------

Expand Down Expand Up @@ -215,22 +234,22 @@ Default: ``60 * 60 * 6`` (six hours)
SECURITY_TRACKABLE
^^^^^^^^^^^^^^^^^^

Wether to track user statistics. Turning this on requires certain user fields.
Whether to track user statistics. Turning this on requires certain user fields.
We do not use this feature, but we do track number of logins.

Default: ``False``

CORS_ORIGINS
^^^^^^^^^^^^

Allowed cross-origins. Set to "*" to allow all. For development (e.g. javascript on localhost) you might use "null" in this list.
Allowed cross-origins. Set to "*" to allow all. For development (e.g. JavaScript on localhost) you might use "null" in this list.

Default: ``[]``

CORS_RESOURCES:
^^^^^^^^^^^^^^^

FlexMeasures resources which get cors protection. This can be a regex, a list of them or dict with all possible options.
FlexMeasures resources which get cors protection. This can be a regex, a list of them or a dictionary with all possible options.

Default: ``[r"/api/*"]``

Expand All @@ -244,6 +263,7 @@ Allows users to make authenticated requests. If true, injects the Access-Control
Default: ``True``



.. _mail-config:

Mail
Expand Down Expand Up @@ -335,7 +355,7 @@ Default: ``6379``
FLEXMEASURES_REDIS_DB_NR (*)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Number of the redis database to use (Redis per default has 16 databases, nubered 0-15)
Number of the redis database to use (Redis per default has 16 databases, numbered 0-15)

Default: ``0``

Expand Down Expand Up @@ -364,9 +384,14 @@ so that old imported data can be demoed as if it were current

Default: ``None``

FLEXMEASURES_SHOW_CONTROL_UI

.. _menu-config:

FLEXMEASURES_LISTED_VIEWS
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The control page is still mocked, so this setting controls if it is to be shown.
A list of the views which are listed in the menu.

Default: ``False``
.. note:: This setting is likely to be deprecated soon, as we might want to control it per account (once we implemented a multi-tenant data model per FlexMeasures server).

Default: ``["dashboard", "analytics", "portfolio", "assets", "users"]``
79 changes: 79 additions & 0 deletions documentation/dev/plugins.rst
@@ -0,0 +1,79 @@
.. _plugins:

Writing Plugins
====================

You can extend FlexMeasures with functionality like UI pages or CLI functions.

A FlexMeasures plugin works as a `Flask Blueprint <https://flask.palletsprojects.com/en/1.1.x/tutorial/views/>`_.

.. todo:: We'll use this to allow for custom forecasting and scheduling algorithms, as well.


How it works
^^^^^^^^^^^^^^

Use the config setting :ref:`plugin-config` to point to your plugin(s).

Here are the assumptions FlexMeasures makes to be able to import your Blueprint:

- The plugin folder contains an __init__.py file.
- In this init, you define a Blueprint object called ``<plugin folder>_bp``.

We'll refer to the plugin with the name of your plugin folder.


Showcase
^^^^^^^^^

Here is a showcase file which constitutes a FlexMeasures plugin. We imagine that we made a plugin to implement some custom logic for a client.

We created the file ``<some_folder>/our_client/__init__.py``. So, ``our_client`` is the plugin folder and becomes the plugin name.
All else that is needed for this showcase (not shown here) is ``<some_folder>/our_client/templates/metrics.html``, which works just as other FlexMeasures templates (they are Jinja2 templates and you can start them with ``{% extends "base.html" %}`` for integration into the FlexMeasures structure).


* We demonstrate adding a view which can be rendered via the FlexMeasures base templates.
* We also showcase a CLI function which has access to the FlexMeasures `app` object. It can be called via ``flexmeasures our_client test``.

.. code-block:: python

from flask import Blueprint, render_template, abort

from flask_security import login_required
from flexmeasures.ui.utils.view_utils import render_flexmeasures_template


our_client_bp = Blueprint('our_client', 'our_client',
template_folder='templates')


# Showcase: Adding a view

@our_client_bp.route('/metrics')
@login_required
def metrics():
msg = "I am part of FM !"
# Note that we render via the in-built FlexMeasures way
return render_flexmeasures_template(
"metrics.html",
message=msg,
)


# Showcase: Adding a CLI command

import click
from flask import current_app
from flask.cli import with_appcontext


our_client_bp.cli.help = "Our client commands"

@our_client_bp.cli.command("test")
@with_appcontext
def oc_test():
print(f"I am a CLI command, part of FlexMeasures: {current_app}")



.. note:: Plugin views can also be added to the FlexMeasures UI menu ― just name them in the config setting :ref:`menu-config`.
2 changes: 1 addition & 1 deletion documentation/index.rst
Expand Up @@ -84,7 +84,7 @@ The platform operator of FlexMeasures can be an Aggregator.
dev/data
dev/api
dev/ci

dev/plugins

.. toctree::
:caption: Integrations
Expand Down
4 changes: 4 additions & 0 deletions flexmeasures/app.py
Expand Up @@ -104,6 +104,10 @@ def create(env: Optional[str] = None, path_to_config: Optional[str] = None) -> F

register_ui_at(app)

from flexmeasures.utils.app_utils import register_plugins

register_plugins(app)

# Profile endpoints (if needed, e.g. during development)
@app.before_request
def before_request():
Expand Down
17 changes: 3 additions & 14 deletions flexmeasures/data/scripts/cli_tasks/Readme.md
Expand Up @@ -5,20 +5,9 @@ These scripts are made available as cli tasks.

To view the available commands, run:

flask --help
flexmeasures --help

For help on individual commands, for example on the saving and loading functionality, type `flask db-save --help` or `flask db-load --help`.
These help messages are generated from the code (see the file db_pop.py in the cli_tasks directory).
Structural data refers to database tables with relatively little entries (they describe things like assets, markets and weather sensors).
Time series data refers to database tables with many entries (like power, price and temperature values).
The default location for storing database backups is within the top-level `migrations` directory.
The contents of this folder are not part of the code repository, and database backups will be lost when deleted.

The load functionality is also made available as an API endpoint called _restoreData_, and described as such in the user documentation for the play server.
The relevant API endpoint is set up in the `flexmeasures/api/play` directory.
The file `routes.py` contains its registration and documentation, while the file `implementations.py` contains the functional logic that connects the API endpoint to the same scripts that are accessible through the command line interface.

The save functionality is currently not available as an API endpoint.
This script cannot be executed within the lifetime of an https request, and would require processing within a separate thread, similar to how forecasting jobs are handled by FlexMeasures.
For help on individual commands, type `flexmesaures <command> --help`.
Structural data refers to database tables which do not contain time series data.

To create new commands, be sure to register any new file (containing the corresponding script) with the flask cli in `flexmeasures/data/__init__.py`.
2 changes: 1 addition & 1 deletion flexmeasures/ui/__init__.py
Expand Up @@ -138,7 +138,7 @@ def add_jinja_variables(app):
for v in (
"FLEXMEASURES_MODE",
"FLEXMEASURES_PLATFORM_NAME",
"FLEXMEASURES_SHOW_CONTROL_UI",
"FLEXMEASURES_LISTED_VIEWS",
"FLEXMEASURES_PUBLIC_DEMO_CREDENTIALS",
):
app.jinja_env.globals[v] = app.config.get(v, "")
Expand Down
5 changes: 2 additions & 3 deletions flexmeasures/ui/templates/base.html
Expand Up @@ -64,8 +64,7 @@
</button>
<span class="navbar-brand">
<a href="/"><span class="navbar-tool-name">{{ FLEXMEASURES_PLATFORM_NAME }}</span></a>
{{ self.title() }} {% if user_is_logged_in and not "Error" in self.title() %} for {{ user_name }} on
Jeju island {% endif %}
{{ self.title() }} {% if user_is_logged_in and not "Error" in self.title() %} for {{ user_name }} {% endif %}
</span>
</div>
<div class="collapse navbar-collapse navbar-right" id="bs-example-navbar-collapse-1">
Expand Down Expand Up @@ -301,4 +300,4 @@ <h3>Icons from <a href="https://www.flaticon.com/" title="Flaticon">Flaticon</a>

</html>

{% endblock base %}
{% endblock base %}
41 changes: 26 additions & 15 deletions flexmeasures/ui/templates/defaults.jinja
Expand Up @@ -3,26 +3,37 @@

{# Front-end app naming #}

{% set show_queues = True if current_user.is_authenticated and (current_user.has_role('admin') or FLEXMEASURES_MODE == "demo") else False %}


{# Front-end menu, as columns with href, id, caption, and (fa fa-)icon #}

{% set navigation_bar = [
('dashboard', 'dashboard', 'Dashboard', 'dashboard'),
('assets', 'assets', 'Assets', 'list-ul'),
] if current_user.is_authenticated else [] %}
{% do navigation_bar.append(('users', 'users', 'Users', 'users')) if current_user.has_role('admin') %}
{% do navigation_bar.extend([
('portfolio', 'portfolio', 'Portfolio overview', 'briefcase'),
('analytics', 'analytics', 'Analytics', 'bar-chart'),
('upload', 'upload', 'Upload data', 'cloud-upload'),
]) if current_user.is_authenticated %}
{% do navigation_bar.extend([
('control', 'control', 'Flexibility actions', 'wrench'),
]) if FLEXMEASURES_SHOW_CONTROL_UI and current_user.is_authenticated %}
{% set navigation_bar = [] %}

{% set nav_bar_specs = {
"dashboard": dict(title="Dashboard", icon="dashboard"),
"assets": dict(title="Assets", icon="list-ul"),
"users": dict(title="Users", icon="users"),
"portfolio": dict(title="Portfolio overview", icon="briefcase"),
"analytics": dict(title="Analytics", icon="bar-chart"),
"upload": dict(title="Upload data", icon="cloud-upload"),
"control": dict(title="Flexibility actions", icon="wrench")
}
%}

{% for view_name in FLEXMEASURES_LISTED_VIEWS %}
{# add specs for views we don't know (plugin views) #}
{% do nav_bar_specs.update({view_name: dict(title=view_name.capitalize(), icon="info")}) if view_name not in nav_bar_specs %}
nhoening marked this conversation as resolved.
Show resolved Hide resolved
{# add view to menu if user is authenticated #}
{% do navigation_bar.append(
(view_name, view_name, nav_bar_specs[view_name]["title"], nav_bar_specs[view_name]["icon"])
) if current_user.is_authenticated %}
{% endfor %}


{% set show_queues = True if current_user.is_authenticated and (current_user.has_role('admin') or FLEXMEASURES_MODE == "demo") else False %}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Candidate for an explicit config setting, rather than checking for server mode.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the design problem here is that some views are only for authenticated users (that's the default) while others also require a specific role (right now that only touches the admin role).

/tasks is the only current example of the latter but there might be others. I'll think about this some more.

{% do navigation_bar.append(('tasks', 'tasks', 'Tasks', 'tasks')) if show_queues %}

{% do navigation_bar.append(('account', 'account', '', 'user')) if current_user.is_authenticated %}

{% do navigation_bar.append(('ui/static/documentation/html/index.html', 'docs', '', 'question')) if documentation_exists and current_user.is_authenticated %}

{% set active_page = active_page|default('dashboard') -%}
Expand Down
2 changes: 1 addition & 1 deletion flexmeasures/ui/templates/views/analytics.html
Expand Up @@ -157,7 +157,7 @@ <h3>Metrics</h3>
Rev./Costs</span>
{% endif %}
</th>
{% if selected_weather_sensor %}
{% if selected_sensor %}
<th class="text-right"><span
title="Selected weather sensor for {{ selected_resource.display_name }}">{{ selected_sensor_type.display_name | capitalize }}</span>
</th>
Expand Down