showcase.rst 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. .. _plugin_showcase:
  2. Plugin showcase
  3. ==================
  4. Here is a showcase file which constitutes a FlexMeasures plugin called ``our_client``.
  5. * We demonstrate adding a view, which can be rendered using the FlexMeasures base templates.
  6. * We also showcase a CLI function which has access to the FlexMeasures `app` object. It can be called via ``flexmeasures our-client test``.
  7. We first create the file ``<some_folder>/our_client/__init__.py``. This means that ``our_client`` is the plugin folder and becomes the plugin name.
  8. With the ``__init__.py`` below, plus the custom Jinja2 template, ``our_client`` is a complete plugin.
  9. .. code-block:: python
  10. __version__ = "2.0"
  11. from flask import Blueprint, render_template, abort
  12. from flask_security import login_required
  13. from flexmeasures.ui.utils.view_utils import render_flexmeasures_template
  14. our_client_bp = Blueprint('our-client', __name__,
  15. template_folder='templates')
  16. # Showcase: Adding a view
  17. @our_client_bp.route('/')
  18. @our_client_bp.route('/my-page')
  19. @login_required
  20. def my_page():
  21. msg = "I am a FlexMeasures plugin !"
  22. # Note that we render via the in-built FlexMeasures way
  23. return render_flexmeasures_template(
  24. "my_page.html",
  25. message=msg,
  26. )
  27. # Showcase: Adding a CLI command
  28. import click
  29. from flask import current_app
  30. from flask.cli import with_appcontext
  31. our_client_bp.cli.help = "Our client commands"
  32. @our_client_bp.cli.command("test")
  33. @with_appcontext
  34. def our_client_test():
  35. print(f"I am a CLI command, part of FlexMeasures: {current_app}")
  36. .. 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`).
  37. .. note:: The ``__version__`` attribute on our module 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.
  38. The template would live at ``<some_folder>/our_client/templates/my_page.html``, which works just as other FlexMeasures templates (they are Jinja2 templates):
  39. .. code-block:: html
  40. {% extends "base.html" %}
  41. {% set active_page = "my-page" %}
  42. {% block title %} Our client dashboard {% endblock %}
  43. {% block divs %}
  44. <!-- This is where your custom content goes... -->
  45. {{ message }}
  46. {% endblock %}
  47. .. 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).
  48. 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:
  49. .. code-block:: html
  50. {% extends "base.html" %}
  51. {% block copyright_notice %}
  52. Created by <a href="https://seita.nl/">Seita Energy Flexibility</a>,
  53. in cooperation with <a href="https://ourclient.nl/">Our Client</a>
  54. &copy
  55. <script>var CurrentYear = new Date().getFullYear(); document.write(CurrentYear)</script>.
  56. {% endblock copyright_notice %}
  57. 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``.
  58. Using other code files in your non-package plugin
  59. ----------------------------
  60. Say you want to include other Python files in your plugin, importing them in your ``__init__.py`` file.
  61. With this file-only version of loading the plugin (if your plugin isn't imported as a package),
  62. this is a bit tricky.
  63. But it can be achieved if you put the plugin path on the import path. Do it like this in your ``__init__.py``:
  64. .. code-block:: python
  65. import os
  66. import sys
  67. HERE = os.path.dirname(os.path.abspath(__file__))
  68. sys.path.insert(0, HERE)
  69. from my_other_file import my_function
  70. Notes on writing tests for your plugin
  71. ----------------------------
  72. Good software practice is to write automatable tests. We encourage you to also do this in your plugin.
  73. We do, and our CookieCutter template for plugins (see above) has simple examples how that can work for the different use cases
  74. (i.e. UI, API, CLI).
  75. However, there are two caveats to look into:
  76. * Your tests need a FlexMeasures app context. FlexMeasure's app creation function provides a way to inject a list of plugins directly. The following could be used for instance in your ``app`` fixture within the top-level ``conftest.py`` if you are using pytest:
  77. .. code-block:: python
  78. from flexmeasures.app import create as create_flexmeasures_app
  79. from .. import __name__
  80. test_app = create_flexmeasures_app(env="testing", plugins=[f"../"{__name__}])
  81. * Test frameworks collect tests from your code and therefore might import your modules. This can interfere with the registration of routes on your Blueprint objects during plugin registration. Therefore, we recommend reloading your route modules, after the Blueprint is defined and before you import them. For example:
  82. .. code-block:: python
  83. my_plugin_ui_bp: Blueprint = Blueprint(
  84. "MyPlugin-UI",
  85. __name__,
  86. template_folder="my_plugin/ui/templates",
  87. static_folder="my_plugin/ui/static",
  88. url_prefix="/MyPlugin",
  89. )
  90. # Now, before we import this dashboard module, in which the "/dashboard" route is attached to my_plugin_ui_bp,
  91. # we make sure it's being imported now, *after* the Blueprint's creation.
  92. importlib.reload(sys.modules["my_plugin.my_plugin.ui.views.dashboard"])
  93. from my_plugin.ui.views import dashboard
  94. The packaging path depends on your plugin's package setup, of course.