From 3e500d6b11b67ca74c184f726f25004eabd55df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Thu, 27 May 2021 13:36:37 +0200 Subject: [PATCH 1/5] display loaded plugins in footer --- flexmeasures/ui/templates/base.html | 5 ++++- flexmeasures/ui/utils/view_utils.py | 1 + flexmeasures/utils/app_utils.py | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/flexmeasures/ui/templates/base.html b/flexmeasures/ui/templates/base.html index 9dfd7b3a0..e166a79c1 100644 --- a/flexmeasures/ui/templates/base.html +++ b/flexmeasures/ui/templates/base.html @@ -284,7 +284,7 @@

Icons from Flaticon {% endif %} {% if not current_user.has_role('anonymous') %} {% if flexmeasures_version %} - on version {{ flexmeasures_version }} + on version {{ flexmeasures_version }}. {% else %} {% if git_version != "Unknown" %} on version {{ git_version }}+{{ git_commits_since }}. @@ -293,6 +293,9 @@

Icons from Flaticon {% endif %} {% endif %} {% endif %} + {% if loaded_plugins %} + Loaded plugins: {{ loaded_plugins }}. + {% endif %}
diff --git a/flexmeasures/ui/utils/view_utils.py b/flexmeasures/ui/utils/view_utils.py index c93bf0bab..f8a577741 100644 --- a/flexmeasures/ui/utils/view_utils.py +++ b/flexmeasures/ui/utils/view_utils.py @@ -71,6 +71,7 @@ def render_flexmeasures_template(html_filename: str, **variables): ) = get_git_description() app_start_time = current_app.config.get("START_TIME") variables["app_running_since"] = time_utils.naturalized_datetime_str(app_start_time) + variables["loaded_plugins"] = ",".join(current_app.config.get("LOADED_PLUGINS", [])) variables["user_is_logged_in"] = current_user.is_authenticated variables[ diff --git a/flexmeasures/utils/app_utils.py b/flexmeasures/utils/app_utils.py index 4065d79bd..ee77921a3 100644 --- a/flexmeasures/utils/app_utils.py +++ b/flexmeasures/utils/app_utils.py @@ -84,6 +84,7 @@ def register_plugins(app: Flask): f"The value of FLEXMEASURES_PLUGIN_PATHS is not a list: {plugin_paths}. Cannot install plugins ..." ) return + app.config["LOADED_PLUGINS"] = [] for plugin_path in plugin_paths: plugin_name = plugin_path.split("/")[-1] if not os.path.exists(os.path.join(plugin_path, "__init__.py")): @@ -101,3 +102,4 @@ def register_plugins(app: Flask): sys.modules[plugin_name] = module spec.loader.exec_module(module) app.register_blueprint(getattr(module, f"{plugin_name}_bp")) + app.config["LOADED_PLUGINS"].append(plugin_name) From 6db8039949253a2d2737e4fad909e3b28eb40ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Thu, 27 May 2021 13:38:48 +0200 Subject: [PATCH 2/5] add changelog entry --- documentation/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index c61cb8ee1..9d47d5aee 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -12,6 +12,7 @@ New features ----------- * Allow plugins to overwrite UI routes and customise the teaser on the login form [see `PR #106 `_] * Allow plugins to customise the copyright notice and credits in the UI footer [see `PR #123 `_] +* Display loaded plugins in footer [see `PR #139 `_] Bugfixes ----------- From cd35e5554df4b3b175cce56fc27598247ceddcc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Fri, 28 May 2021 12:58:43 +0200 Subject: [PATCH 3/5] support plugin versioning and improve plugin-dev documentation --- Makefile | 4 +- documentation/changelog.rst | 2 +- documentation/dev/plugins.rst | 63 ++++++++++++++++++++++++----- flexmeasures/ui/utils/view_utils.py | 5 ++- flexmeasures/utils/app_utils.py | 11 ++--- 5 files changed, 65 insertions(+), 20 deletions(-) diff --git a/Makefile b/Makefile index ce465bc92..e2a4453e3 100644 --- a/Makefile +++ b/Makefile @@ -15,13 +15,13 @@ test: # ---- Documentation --- update-docs: - pip3 install sphinx==3.5.4 sphinxcontrib.httpdomain # sphinx4 is not supported yet by sphinx-contrib/httpdomain, see https://github.com/sphinx-contrib/httpdomain/issues/46 + pip3 install sphinx==3.5.4 sphinxcontrib.httpdomain sphinx-rtd-theme # sphinx4 is not supported yet by sphinx-contrib/httpdomain, see https://github.com/sphinx-contrib/httpdomain/issues/46 cd documentation; make clean; make html; cd .. update-docs-pdf: @echo "NOTE: PDF documentation requires packages (on Debian: latexmk texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended)" @echo "NOTE: Currently, the docs require some pictures which are not in the git repo atm. Ask the devs." - pip3 install sphinx sphinxcontrib.httpdomain + pip3 install sphinx sphinxcontrib.httpdomain sphinx-rtd-theme cd documentation; make clean; make latexpdf; make latexpdf; cd .. # make latexpdf can require two passes # ---- Installation --- diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 9d47d5aee..8bdf84355 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -12,7 +12,7 @@ New features ----------- * Allow plugins to overwrite UI routes and customise the teaser on the login form [see `PR #106 `_] * Allow plugins to customise the copyright notice and credits in the UI footer [see `PR #123 `_] -* Display loaded plugins in footer [see `PR #139 `_] +* Display loaded plugins in footer and support plugin versioning [see `PR #139 `_] Bugfixes ----------- diff --git a/documentation/dev/plugins.rst b/documentation/dev/plugins.rst index c70498e2c..f52703898 100644 --- a/documentation/dev/plugins.rst +++ b/documentation/dev/plugins.rst @@ -17,7 +17,7 @@ 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. +- The plugin folder contains an ``__init__.py`` file. - In this init, you define a Blueprint object called ``_bp``. We'll refer to the plugin with the name of your plugin folder. @@ -26,14 +26,14 @@ 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. +Here is a showcase file which constitutes a FlexMeasures plugin called ``our_client``. -We created the file ``/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 ``/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 using 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``. +We first create the file ``/our_client/__init__.py``. This means that ``our_client`` is the plugin folder and becomes the plugin name. -* 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``. +With the ``__init__.py`` below, plus the custom Jinja2 template, ``our_client`` is a complete plugin. .. code-block:: python @@ -46,17 +46,18 @@ All else that is needed for this showcase (not shown here) is ``/ou our_client_bp = Blueprint('our_client', 'our_client', template_folder='templates') + __version__ = "2.0" # Showcase: Adding a view @our_client_bp.route('/') - @our_client_bp.route('/metrics') + @our_client_bp.route('/my-page') @login_required def metrics(): - msg = "I am part of FM !" + msg = "I am a FlexMeasures plugin !" # Note that we render via the in-built FlexMeasures way return render_flexmeasures_template( - "metrics.html", + "my_page.html", message=msg, ) @@ -76,9 +77,49 @@ All else that is needed for this showcase (not shown here) is ``/ou print(f"I am a CLI command, part of FlexMeasures: {current_app}") -.. note:: You can overwrite FlexMeasures routes here. In our example above, we set the root route ``/``. FlexMeasures registers plugin routes before its own, so in this case visiting the root URL of your app will display this plugged-in view (the same you'd see at `/metrics`). +.. note:: You can overwrite FlexMeasures routing in your plugin. In our example above, we are using the root route ``/``. FlexMeasures registers plugin routes before its own, so in this case visiting the root URL of your app will display this plugged-in view (the same you'd see at `/my-page`). + +.. note:: The ``__version__`` attribute is being displayed in the standard FlexMeasures UI footer, where we show loaded plugins. Of course, it can also useful for your own maintenance. + + +The template would live at ``/our_client/templates/my_page.html``, which works just as other FlexMeasures templates (they are Jinja2 templates): + +.. code-block:: html + + {% extends "base.html" %} + + {% set active_page = "my-page" %} + + {% block title %} Our client Dashboard {% endblock %} + + {% block divs %} + + + + {{ message }} + + {% endblock %} + + +.. note:: Plugin views can also be added to the FlexMeasures UI menu ― just name them in the config setting :ref:`menu-config`. In this example, add ``my-page``. This also will make the ``active_page`` setting in the above template useful (highlights the current page in the menu). + +Starting the template with ``{% extends "base.html" %}`` integrates your page content into the FlexMeasures UI structure. You can also extend a different base template. For instance, we find it handy to extend ``base.html`` with a custom base template, to extend the footer, as shown below: + + .. code-block:: html + + {% extends "base.html" %} + + {% block copyright_notice %} + + Created by Seita Energy Flexibility, + in cooperation with Our Client + © + . + + {% endblock copyright_notice %} + +We'd name this file ``our_client_base.html``. Then, we'd extend our page template from ``our_client_base.html``, instead of ``base.html``. -.. note:: Plugin views can also be added to the FlexMeasures UI menu ― just name them in the config setting :ref:`menu-config`. Validating data with marshmallow ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/flexmeasures/ui/utils/view_utils.py b/flexmeasures/ui/utils/view_utils.py index f8a577741..bf218b581 100644 --- a/flexmeasures/ui/utils/view_utils.py +++ b/flexmeasures/ui/utils/view_utils.py @@ -71,7 +71,10 @@ def render_flexmeasures_template(html_filename: str, **variables): ) = get_git_description() app_start_time = current_app.config.get("START_TIME") variables["app_running_since"] = time_utils.naturalized_datetime_str(app_start_time) - variables["loaded_plugins"] = ",".join(current_app.config.get("LOADED_PLUGINS", [])) + variables["loaded_plugins"] = ",".join( + f"{p_name} (v{p_version})" + for p_name, p_version in current_app.config.get("LOADED_PLUGINS", {}).items() + ) variables["user_is_logged_in"] = current_user.is_authenticated variables[ diff --git a/flexmeasures/utils/app_utils.py b/flexmeasures/utils/app_utils.py index ee77921a3..b54b752f7 100644 --- a/flexmeasures/utils/app_utils.py +++ b/flexmeasures/utils/app_utils.py @@ -84,7 +84,7 @@ def register_plugins(app: Flask): f"The value of FLEXMEASURES_PLUGIN_PATHS is not a list: {plugin_paths}. Cannot install plugins ..." ) return - app.config["LOADED_PLUGINS"] = [] + app.config["LOADED_PLUGINS"] = {} for plugin_path in plugin_paths: plugin_name = plugin_path.split("/")[-1] if not os.path.exists(os.path.join(plugin_path, "__init__.py")): @@ -96,10 +96,11 @@ def register_plugins(app: Flask): spec = importlib.util.spec_from_file_location( plugin_name, os.path.join(plugin_path, "__init__.py") ) - app.logger.debug(spec) module = importlib.util.module_from_spec(spec) - app.logger.debug(module) sys.modules[plugin_name] = module spec.loader.exec_module(module) - app.register_blueprint(getattr(module, f"{plugin_name}_bp")) - app.config["LOADED_PLUGINS"].append(plugin_name) + plugin_blueprint = getattr(module, f"{plugin_name}_bp") + app.register_blueprint(plugin_blueprint) + plugin_version = getattr(plugin_blueprint, "__version__", "0.1") + app.config["LOADED_PLUGINS"][plugin_name] = plugin_version + app.logger.info(f"Loaded plugins: {app.config['LOADED_PLUGINS']}") From 598b6f40d965f0d07fda57ebabcaa0b1a6ed5ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Fri, 28 May 2021 13:08:08 +0200 Subject: [PATCH 4/5] improve CLI/marshmallow docs a bit --- documentation/dev/plugins.rst | 55 +++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/documentation/dev/plugins.rst b/documentation/dev/plugins.rst index f52703898..ab2cd396d 100644 --- a/documentation/dev/plugins.rst +++ b/documentation/dev/plugins.rst @@ -121,12 +121,29 @@ Starting the template with ``{% extends "base.html" %}`` integrates your page co We'd name this file ``our_client_base.html``. Then, we'd extend our page template from ``our_client_base.html``, instead of ``base.html``. -Validating data with marshmallow +Using other files in your plugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Say you want to include other Python files in your plugin, importing them in your ``__init__.py`` file. +This can be done if you put the plugin path on the import path. Do it like this in your ``__init__.py``: + +.. code-block:: python + + import os + import sys + + HERE = os.path.dirname(os.path.abspath(__file__)) + sys.path.insert(0, HERE) + + from my_other_file import my_function + + +Validating arguments in your CLI commands with marshmallow ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -FlexMeasures validates input data using `marshmallow `_. -Data fields can be made suitable for use in CLI commands through our ``MarshmallowClickMixin``. -An example: +Arguments to CLI commands can be validated using `marshmallow `_. +FlexMeasures is using this functionality (via the ``MarshmallowClickMixin`` class) and also defines some custom field schemas. +We demonstrate this here, and also show how you can add your own custom field schema: .. code-block:: python @@ -138,14 +155,17 @@ An example: from flexmeasures.data.schemas.utils import MarshmallowClickMixin from marshmallow import fields - class StrField(fields.Str, MarshmallowClickMixin): - """String field validator usable for UI routes and CLI functions.""" + class CLIStrField(fields.Str, MarshmallowClickMixin): + """ + String field validator, made usable for CLI functions. + You could also define your own validations here. + """ @click.command("meet") @click.option( "--where", required=True, - type=StrField(), # see above: we just made this field suitable for CLI functions + type=CLIStrField(), help="(Required) Where we meet", ) @click.option( @@ -161,24 +181,7 @@ An example: print(f"Okay, see you {where} on {when}.") -Using other files in your plugin -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Say you want to include other Python files in your plugin, importing them in your ``__init__.py`` file. -This can be done if you put the plugin path on the import path. Do it like this in your ``__init__.py``: - -.. code-block:: python - - import os - import sys - - HERE = os.path.dirname(os.path.abspath(__file__)) - sys.path.insert(0, HERE) - - from my_other_file import my_function - - -Customising the login teaser +Customising the login page teaser ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FlexMeasures shows an image carousel next to its login form (see ``ui/templates/admin/login_user.html``). @@ -199,4 +202,6 @@ Place this template file in the template folder of your plugin blueprint (see ab Finally, add this config setting to your FlexMeasures config file (using the template filename you chose, obviously): + .. code-block:: bash + SECURITY_LOGIN_USER_TEMPLATE = "my_user_login.html" From eef82f1cb64eeaacb1d41e144e597166af5f6cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Fri, 28 May 2021 23:06:08 +0200 Subject: [PATCH 5/5] add missing 'be' --- documentation/dev/plugins.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/dev/plugins.rst b/documentation/dev/plugins.rst index ab2cd396d..6ac92b94c 100644 --- a/documentation/dev/plugins.rst +++ b/documentation/dev/plugins.rst @@ -79,7 +79,7 @@ With the ``__init__.py`` below, plus the custom Jinja2 template, ``our_client`` .. note:: You can overwrite FlexMeasures routing in your plugin. In our example above, we are using the root route ``/``. FlexMeasures registers plugin routes before its own, so in this case visiting the root URL of your app will display this plugged-in view (the same you'd see at `/my-page`). -.. note:: The ``__version__`` attribute is being displayed in the standard FlexMeasures UI footer, where we show loaded plugins. Of course, it can also useful for your own maintenance. +.. note:: The ``__version__`` attribute is being displayed in the standard FlexMeasures UI footer, where we show loaded plugins. Of course, it can also be useful for your own maintenance. The template would live at ``/our_client/templates/my_page.html``, which works just as other FlexMeasures templates (they are Jinja2 templates):