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

[UI] Add configurability to the menu, what root view to show and what name to use #163

Merged
merged 20 commits into from Sep 3, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a0bb2c2
Let the root view ('/') be configurable by account role(s).
nhoening Aug 30, 2021
c14dcfc
enable more control over menu through FLEXMEASURES_LISTED_VIEWS, refa…
nhoening Aug 31, 2021
14ff213
add changelog entry
nhoening Aug 31, 2021
cf6239f
mention the FM version this requires in the docs
nhoening Aug 31, 2021
352c28a
Fix bug for Flask AnonymousUser not having an account associated with…
Flix6x Sep 1, 2021
5bc278f
Allow a plugin to define multiple Blueprints and relax naming assumpt…
Flix6x Sep 1, 2021
1c11c8f
Merge branch 'views-by-accounts' of github.com:SeitaBV/flexmeasures i…
nhoening Sep 2, 2021
516c266
a few smaller corrections from code review
nhoening Sep 2, 2021
7ef1585
support adding and deleting of roles
nhoening Sep 2, 2021
5ab5f39
use a better name for the function dealing with these config entries,…
nhoening Sep 2, 2021
b493b04
Swap lines so that the default root_view can be set in time (#165)
Flix6x Sep 2, 2021
68c14ca
also make FLEXMEASURES_PLATFORM_NAME flexible w.r.t. to account roles
nhoening Sep 2, 2021
9054722
merge
nhoening Sep 2, 2021
d03909e
Customize view titles and icons (#166)
Flix6x Sep 2, 2021
12a0e29
add MENU to menu-related config setting names
nhoening Sep 2, 2021
1ffcac6
separate the application of updates to the menu config, so we apply e…
nhoening Sep 2, 2021
b4d5f94
Revert "Allow a plugin to define multiple Blueprints and relax naming…
Flix6x Sep 3, 2021
1423cff
Typographical edits
Flix6x Sep 3, 2021
3d83ee2
Corrections in documentation of root view setting
Flix6x Sep 3, 2021
076018a
documentation improvements
nhoening Sep 3, 2021
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: 1 addition & 1 deletion Makefile
Expand Up @@ -46,7 +46,7 @@ install-pip-tools:
pip3 install -q pip-tools>=6.2

install-sphinx-tools:
pip3 install sphinx>=4.0.3 sphinxcontrib.httpdomain sphinx-rtd-theme
pip3 install sphinx>=4.0.3 sphinxcontrib.httpdomain sphinx-rtd-theme sphinx_fontawesome

freeze-deps:
make install-pip-tools
Expand Down
2 changes: 1 addition & 1 deletion ci/run_mypy.sh
@@ -1,7 +1,7 @@
#!/bin/bash
set -e
pip install --upgrade mypy>=0.902
pip install types-pytz types-requests types-Flask types-click types-redis types-tzlocal types-python-dateutil
pip install types-pytz types-requests types-Flask types-click types-redis types-tzlocal types-python-dateutil types-setuptools
# We are checking python files which have type hints, and leave out bigger issues we made issues for
# * data/scripts: We'll remove legacy code: https://trello.com/c/1wEnHOkK/7-remove-custom-data-scripts
# * data/models and data/services: https://trello.com/c/rGxZ9h2H/540-makequery-call-signature-is-incoherent
Expand Down
5 changes: 4 additions & 1 deletion documentation/changelog.rst
Expand Up @@ -8,11 +8,14 @@ v0.6.0 | August XX, 2021
.. warning:: Upgrading to this version requires running ``flexmeasures db upgrade`` (you can create a backup first with ``flexmeasures db-ops dump``).
In case you are using experimental developer features and have previously set up sensors, be sure to check out the upgrade instructions in `PR #157 <https://github.com/SeitaBV/flexmeasures/pull/157>`_. Furthermore, if you want to create custom user/account relationships while upgrading (otherwise the upgrade script creates accounts based on email domains), check out the upgrade instructions in `PR #159 <https://github.com/SeitaBV/flexmeasures/pull/159>`_. If you want to use both of these custom upgrade features, do the upgrade in two steps. First, as described in PR 157 and upgrading up to revision b6d49ed7cceb, then as described in PR 159 for the rest.

.. warning:: The config setting ``FLEXMEASURES_LISTED_VIEWS`` has been renamed to ``FLEXMEASURES_MENU_LISTED_VIEWS``.

New features
-----------
* Analytics view offers grouping of all assets by location [see `PR #148 <http://www.github.com/SeitaBV/flexmeasures/pull/148>`_]
* Multi-tenancy: Supporting multiple customers per FlexMeasures server, by introducing the `Account` concept. Accounts have users and assets associated. [see `PR #159 <http://www.github.com/SeitaBV/flexmeasures/pull/159>`_ and `PR #163 <http://www.github.com/SeitaBV/flexmeasures/pull/163>`_]
* In the UI, the root view ("/"), the platform name and the visible menu items can now be more tightly controlled (per account roles of the current user) [see `PR #163 <http://www.github.com/SeitaBV/flexmeasures/pull/163>`_]
* Add (experimental) endpoint to post sensor data for any sensor. Also supports our ongoing integration with data internally represented using the `timely beliefs <https://github.com/SeitaBV/timely-beliefs>`_ lib [see `PR #147 <http://www.github.com/SeitaBV/flexmeasures/pull/147>`_]
* Multi-tenancy: Supporting multiple customers per FlexMeasures server, by introducing the `Account` concept. Accounts have users and assets associated. [see `PR #159 <http://www.github.com/SeitaBV/flexmeasures/pull/159>`_]

Bugfixes
-----------
Expand Down
5 changes: 3 additions & 2 deletions documentation/conf.py
Expand Up @@ -8,6 +8,7 @@

from datetime import datetime
from pkg_resources import get_distribution
import sphinx_fontawesome


# If extensions (or modules to document with autodoc) are in another directory,
Expand All @@ -27,8 +28,7 @@
# The short X.Y.Z version
version = ".".join(release.split(".")[:3])

rst_prolog = """
"""
rst_prolog = sphinx_fontawesome.prolog

# -- General configuration ---------------------------------------------------

Expand All @@ -47,6 +47,7 @@
"sphinx.ext.imgmath",
"sphinx.ext.ifconfig",
"sphinx.ext.todo",
"sphinx_fontawesome",
"sphinxcontrib.autohttp.flask",
"sphinxcontrib.autohttp.flaskqref",
]
Expand Down
77 changes: 63 additions & 14 deletions documentation/configuration.rst
Expand Up @@ -89,10 +89,71 @@ UI
FLEXMEASURES_PLATFORM_NAME
^^^^^^^^^^^^^^^^^^^^^^^^^^

Name being used in headings
Name being used in headings and in the menu bar.

For more fine-grained control, this can also be a list, where it's possible to set the platform name for certain account roles (as a tuple of view name and list of applicable account roles). In this case, the list is searched from left to right, and the first fitting name is used.

For example, ``("MyMDCApp", ["MDC"]), "MyApp"]`` would use the name "MyMDCApp" for users connected to accounts with the account role "MDC" while all others would see the name "/MyApp".

.. note:: This fine-grained control requires FlexMeasures version v0.6.0

Default: ``"FlexMeasures"``


FLEXMEASURES_ROOT_VIEW
^^^^^^^^^^^^^^^^^^^^^^^^^^

Root view (reachable at "/"). For example ``"/dashboard"``.

For more fine-grained control, this can also be a list, where it's possible to set the root view for certain account roles (as a tuple of view name and list of applicable account roles). In this case, the list is searched from left to right, and the first fitting view is shown.

For example, ``[("metering-dashboard", ["MDC", "Prosumer"]), "default-dashboard"]`` would show "/mdc-dashboard" for users connected to accounts with account roles "MDC" or "Prosumer", while all others would be routed to "/default-dashboard".

If this setting is empty or not applicable for the current user, the "/" view will be shown (FlexMeasures' default dashboard or a plugin view which was registered at "/").

Default ``[]``

.. note:: This setting was introduced in FlexMeasures version v0.6.0


.. _menu-config:

FLEXMEASURES_MENU_LISTED_VIEWS
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

A list of the view names which are listed in the menu.

.. note:: This setting only lists the names of views, rather than making sure the views exist.

For more fine-grained control, the entries can also be tuples of view names and list of applicable account roles. For example, the entry ``("details": ["MDC","Prosumer"])`` would show the link to the "details" page only to users who are connected to accounts with roles "MDC" or "Prosumer".

.. note:: This fine-grained control requires FlexMeasures version v0.6.0

Default: ``["dashboard", "analytics", "portfolio", "assets", "users"]``


FLEXMEASURES_MENU_LISTED_VIEW_ICONS
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

A dictionary containing a Font Awesome icon name for each view name listed in the menu.
For example, ``{"freezer-view": "snowflake-o"}`` puts a snowflake icon (|snowflake-o|) next to your freezer-view menu item.

Default: ``{}``

.. note:: This setting was introduced in FlexMeasures version v0.6.0


FLEXMEASURES_MENU_LISTED_VIEW_TITLES
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

A dictionary containing a string title for each view name listed in the menu.
For example, ``{"freezer-view": "Your freezer"}`` lists the freezer-view in the menu as "Your freezer".

Default: ``{}``

.. note:: This setting was introduced in FlexMeasures version v0.6.0


FLEXMEASURES_HIDE_NAN_IN_UI
^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down Expand Up @@ -426,18 +487,6 @@ FLEXMEASURES_DEMO_YEAR
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

When ``FLEXMEASURES_MODE=demo``\ , this setting can be used to make the FlexMeasures platform select data from a specific year (e.g. 2015),
so that old imported data can be demoed as if it were current
so that old imported data can be demoed as if it were current.

Default: ``None``


.. _menu-config:

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

A list of the views which are listed in the menu.

.. 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"]``
6 changes: 3 additions & 3 deletions documentation/dev/plugins.rst
Expand Up @@ -18,7 +18,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.
- In this init, you define a Blueprint object called ``<plugin folder>_bp``.
- In that file, you define a Blueprint object (or several).

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

Expand All @@ -43,7 +43,7 @@ With the ``__init__.py`` below, plus the custom Jinja2 template, ``our_client``
from flexmeasures.ui.utils.view_utils import render_flexmeasures_template


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

our_client_bp.__version__ = "2.0"
Expand Down Expand Up @@ -90,7 +90,7 @@ The template would live at ``<some_folder>/our_client/templates/my_page.html``,

{% set active_page = "my-page" %}

{% block title %} Our client Dashboard {% endblock %}
{% block title %} Our client dashboard {% endblock %}

{% block divs %}

Expand Down
7 changes: 4 additions & 3 deletions flexmeasures/app.py
Expand Up @@ -104,14 +104,15 @@ def create(env: Optional[str] = None, path_to_config: Optional[str] = None) -> F
register_api_at(app)

# Register plugins
# If plugins register routes, they'll have precedence over standard UI
# routes (first registration wins). However, we want to control "/" separately.

from flexmeasures.utils.app_utils import register_plugins
from flexmeasures.utils.app_utils import root_dispatcher, register_plugins

app.add_url_rule("/", view_func=root_dispatcher)
register_plugins(app)

# Register the UI
# If plugins registered routes already (e.g. "/"),
# they have precedence (first registration wins).

from flexmeasures.ui import register_at as register_ui_at

Expand Down
@@ -0,0 +1,53 @@
"""add account roles

Revision ID: 96f2db5bed30
Revises: e4c9cf837311
Create Date: 2021-08-30 11:33:40.481140

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "96f2db5bed30"
down_revision = "e4c9cf837311"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"account_role",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=80), nullable=True),
sa.Column("description", sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint("id", name=op.f("account_role_pkey")),
sa.UniqueConstraint("name", name=op.f("account_role_name_key")),
)
op.create_table(
"roles_accounts",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("account_id", sa.Integer(), nullable=True),
sa.Column("role_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["account_id"],
["account.id"],
name=op.f("roles_accounts_account_id_account_fkey"),
),
sa.ForeignKeyConstraint(
["role_id"],
["account_role.id"],
name=op.f("roles_accounts_role_id_account_role_fkey"),
),
sa.PrimaryKeyConstraint("id", name=op.f("roles_accounts_pkey")),
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("roles_accounts")
op.drop_table("account_role")
# ### end Alembic commands ###
36 changes: 29 additions & 7 deletions flexmeasures/data/models/user.py
Expand Up @@ -8,21 +8,21 @@
from flexmeasures.data.config import db


class RolesUsers(db.Model):
__tablename__ = "roles_users"
class RolesAccounts(db.Model):
__tablename__ = "roles_accounts"
id = Column(Integer(), primary_key=True)
user_id = Column("user_id", Integer(), ForeignKey("fm_user.id"))
role_id = Column("role_id", Integer(), ForeignKey("role.id"))
account_id = Column("account_id", Integer(), ForeignKey("account.id"))
role_id = Column("role_id", Integer(), ForeignKey("account_role.id"))


class Role(db.Model, RoleMixin):
__tablename__ = "role"
class AccountRole(db.Model):
__tablename__ = "account_role"
id = Column(Integer(), primary_key=True)
name = Column(String(80), unique=True)
description = Column(String(255))

def __repr__(self):
return "<Role:%s (ID:%d)>" % (self.name, self.id)
return "<AccountRole:%s (ID:%d)>" % (self.name, self.id)


class Account(db.Model):
Expand All @@ -34,11 +34,33 @@ class Account(db.Model):
__tablename__ = "account"
id = Column(Integer, primary_key=True)
name = Column(String(100), default="", unique=True)
account_roles = relationship(
"AccountRole",
secondary="roles_accounts",
backref=backref("accounts", lazy="dynamic"),
)

def __repr__(self):
return "<Account %s (ID:%d)" % (self.name, self.id)


class RolesUsers(db.Model):
__tablename__ = "roles_users"
id = Column(Integer(), primary_key=True)
user_id = Column("user_id", Integer(), ForeignKey("fm_user.id"))
role_id = Column("role_id", Integer(), ForeignKey("role.id"))


class Role(db.Model, RoleMixin):
__tablename__ = "role"
id = Column(Integer(), primary_key=True)
name = Column(String(80), unique=True)
description = Column(String(255))

def __repr__(self):
return "<Role:%s (ID:%d)>" % (self.name, self.id)


class User(db.Model, UserMixin):
"""
We use the flask security UserMixin, which does include functionality,
Expand Down
35 changes: 32 additions & 3 deletions flexmeasures/data/scripts/cli_tasks/data_add.py
Expand Up @@ -15,7 +15,8 @@

from flexmeasures.data import db
from flexmeasures.data.services.forecasting import create_forecasting_jobs
from flexmeasures.data.services.users import create_user, Account
from flexmeasures.data.services.users import create_user
from flexmeasures.data.models.user import Account, AccountRole, RolesAccounts
from flexmeasures.data.models.time_series import Sensor, TimedBelief
from flexmeasures.data.schemas.sensors import SensorSchema
from flexmeasures.data.schemas.generic_assets import (
Expand Down Expand Up @@ -45,19 +46,47 @@ def fm_dev_add_data():
"""Developer CLI commands not yet meant for users: Add data."""


@fm_add_data.command("account-role")
@with_appcontext
@click.option("--name", required=True)
@click.option("--description")
def new_account_role(name: str, description: str):
"""
Create an account role.
"""
role = AccountRole.query.filter_by(name=name).one_or_none()
if role is not None:
click.echo(f"Account role '{name}' already exists.")
raise click.Abort
role = AccountRole(name=name, description=description)
db.session.add(role)
db.session.commit()
print(f"Account role '{name}' (ID: {role.id}) successfully created.")


@fm_add_data.command("account")
@with_appcontext
@click.option("--name", required=True)
def new_account(name: str):
@click.option("--roles", help="e.g. anonymous,Prosumer,CPO")
def new_account(name: str, roles: List[str]):
"""
Create an account for a tenant in the FlexMeasures platform.
"""
account = db.session.query(Account).filter_by(name=name).one_or_none()
if account is not None:
print(f"Account '{name}' already exist.")
click.echo(f"Account '{name}' already exists.")
raise click.Abort
account = Account(name=name)
db.session.add(account)
if roles:
for role_name in roles.split(","):
role = AccountRole.query.filter_by(name=role_name).one_or_none()
if role is None:
print(f"Adding account role {role_name} ...")
role = AccountRole(name=role_name)
db.session.add(role)
db.session.flush()
db.session.add(RolesAccounts(role_id=role.id, account_id=account.id))
db.session.commit()
print(f"Account '{name}' (ID: {account.id}) successfully created.")

Expand Down