Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
432 feat(account): account api endpoint and webpages (#605)
Added API endpoints and webpages `/accounts` and `/accounts/<id>` to list accounts and show an overview of the assets, users and account roles of an account [see `PR #605 <https://github.com/FlexMeasures/flexmeasures/pull/605>`_] Various UI changes to make the UI have a consistent style. Showing the pages within the same card div. * Add user.last_seen to CLI command show account Signed-off-by: Nicolas Höning <nicolas@seita.nl> * Document health API Signed-off-by: Nicolas Höning <nicolas@seita.nl> * Start /accounts API: GET endpoints Signed-off-by: Nicolas Höning <nicolas@seita.nl> * remove two debugging statements Signed-off-by: Nicolas Höning <nicolas@seita.nl> * use utility function for headers, fix typo Signed-off-by: Nicolas Höning <nicolas@seita.nl> * support new-style Optional shorthand in Python > 3.10 Signed-off-by: Nicolas Höning <nicolas@seita.nl> * update the number of accounts, as PR 602 added two more Signed-off-by: Nicolas Höning <nicolas@seita.nl> * feat(CRUD): add R of accounts CRUD Signed-off-by: GustaafL <guus@seita.nl> * three textual corrections Signed-off-by: Nicolas Höning <nicolas@seita.nl> * get_accounts util function returns empty list if unknown role is passed for filtering Signed-off-by: Nicolas Höning <nicolas@seita.nl> * feat(CRUD): updated accounts tests and added links to user pages Signed-off-by: GustaafL <guus@seita.nl> * feat(CRUD): add accounts to navbar and get single account through api Signed-off-by: GustaafL <guus@seita.nl> * feat(CRUD): review comments implemented Signed-off-by: GustaafL <guus@seita.nl> * fix docstring in API /index Signed-off-by: Nicolas Höning <nicolas@seita.nl> * chore(changelog): API changelog added accounts endpoints Signed-off-by: GustaafL <guus@seita.nl> * chore(changelog): changelog added accounts endpoints and pages Signed-off-by: GustaafL <guus@seita.nl> * chore(changelog): changelog added accounts endpoints and pages Signed-off-by: GustaafL <guus@seita.nl> * chore(changelog): changelog added accounts endpoints and pages Signed-off-by: GustaafL <guus@seita.nl> * chore(changelog): PR number corrected Signed-off-by: GustaafL <41048720+GustaafL@users.noreply.github.com> * feat(account): accountpage updated with asset and user info Signed-off-by: GustaafL <guus@seita.nl> * feat(accounts): Add user and asset counts to accounts page Signed-off-by: GustaafL <guus@seita.nl> * Add account role schema (#625) * Add AccountRoleSchema and connect it to the AccountSchema Signed-off-by: F.N. Claessen <felix@seita.nl> * Update test Signed-off-by: F.N. Claessen <felix@seita.nl> --------- Signed-off-by: F.N. Claessen <felix@seita.nl> Co-authored-by: Flix6x <flix6x@users.noreply.github.com> * test Signed-off-by: GustaafL <guus@seita.nl> * feat(accounts): account role names added to the account pages Signed-off-by: GustaafL <guus@seita.nl> * refactor(users): review changes, updated changelog and typing Signed-off-by: GustaafL <guus@seita.nl> * refactor(crud): review changes to html templates Signed-off-by: GustaafL <guus@seita.nl> * Remove redundant lines Signed-off-by: F.N. Claessen <felix@seita.nl> * Fix type annotations Signed-off-by: F.N. Claessen <felix@seita.nl> * Fix button for including inactive users Signed-off-by: F.N. Claessen <felix@seita.nl> * Move button to the side Signed-off-by: F.N. Claessen <felix@seita.nl> * Fix data sort on copy of users table Signed-off-by: F.N. Claessen <felix@seita.nl> * Add missing decorators to fix redirects to the login page Signed-off-by: F.N. Claessen <felix@seita.nl> --------- Signed-off-by: Nicolas Höning <nicolas@seita.nl> Signed-off-by: GustaafL <guus@seita.nl> Signed-off-by: GustaafL <41048720+GustaafL@users.noreply.github.com> Signed-off-by: F.N. Claessen <felix@seita.nl> Co-authored-by: GustaafL <guus@seita.nl> Co-authored-by: GustaafL <41048720+GustaafL@users.noreply.github.com> Co-authored-by: Felix Claessen <30658763+Flix6x@users.noreply.github.com> Co-authored-by: Flix6x <flix6x@users.noreply.github.com> Co-authored-by: F.N. Claessen <felix@seita.nl>
- Loading branch information
1 parent
fd62ed3
commit 67d2236
Showing
27 changed files
with
886 additions
and
246 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
from flask_classful import FlaskView, route | ||
from webargs.flaskparser import use_kwargs | ||
from flask_security import current_user, auth_required | ||
from flask_json import as_json | ||
|
||
from flexmeasures.auth.policy import user_has_admin_access | ||
from flexmeasures.auth.decorators import permission_required_for_context | ||
from flexmeasures.data.models.user import Account | ||
from flexmeasures.data.services.accounts import get_accounts | ||
from flexmeasures.api.common.schemas.users import AccountIdField | ||
from flexmeasures.data.schemas.account import AccountSchema | ||
|
||
""" | ||
API endpoints to manage accounts. | ||
Both POST (to create) and DELETE are not accessible via the API, but as CLI functions. | ||
Editing (PATCH) is also not yet implemented, but might be next, e.g. for the name or roles. | ||
""" | ||
|
||
# Instantiate schemas outside of endpoint logic to minimize response time | ||
account_schema = AccountSchema() | ||
accounts_schema = AccountSchema(many=True) | ||
partial_account_schema = AccountSchema(partial=True) | ||
|
||
|
||
class AccountAPI(FlaskView): | ||
route_base = "/accounts" | ||
trailing_slash = False | ||
|
||
@route("", methods=["GET"]) | ||
@auth_required("token", "session") | ||
@as_json | ||
def index(self): | ||
"""API endpoint to list all accounts accessible to the current user. | ||
.. :quickref: Account; Download account list | ||
This endpoint returns all accessible accounts. | ||
Accessible accounts are your own account, or all accounts for admins. | ||
When the super-account concept (GH#203) lands, then users in such accounts see all managed accounts. | ||
**Example response** | ||
An example of one account being returned: | ||
.. sourcecode:: json | ||
[ | ||
{ | ||
'id': 1, | ||
'name': 'Test Account' | ||
'account_roles': [1, 3], | ||
} | ||
] | ||
:reqheader Authorization: The authentication token | ||
:reqheader Content-Type: application/json | ||
:resheader Content-Type: application/json | ||
:status 200: PROCESSED | ||
:status 400: INVALID_REQUEST | ||
:status 401: UNAUTHORIZED | ||
:status 403: INVALID_SENDER | ||
:status 422: UNPROCESSABLE_ENTITY | ||
""" | ||
if user_has_admin_access(current_user, "read"): | ||
accounts = get_accounts() | ||
else: | ||
accounts = [current_user.account] | ||
return accounts_schema.dump(accounts), 200 | ||
|
||
@route("/<id>", methods=["GET"]) | ||
@use_kwargs({"account": AccountIdField(data_key="id")}, location="path") | ||
@permission_required_for_context("read", arg_name="account") | ||
@as_json | ||
def get(self, id: int, account: Account): | ||
"""API endpoint to get an account. | ||
.. :quickref: Account; Get an account | ||
This endpoint retrieves an account, given its id. | ||
Only admins or the user themselves can use this endpoint. | ||
**Example response** | ||
.. sourcecode:: json | ||
{ | ||
'id': 1, | ||
'name': 'Test Account' | ||
'account_roles': [1, 3], | ||
} | ||
:reqheader Authorization: The authentication token | ||
:reqheader Content-Type: application/json | ||
:resheader Content-Type: application/json | ||
:status 200: PROCESSED | ||
:status 400: INVALID_REQUEST, REQUIRED_INFO_MISSING, UNEXPECTED_PARAMS | ||
:status 401: UNAUTHORIZED | ||
:status 403: INVALID_SENDER | ||
:status 422: UNPROCESSABLE_ENTITY | ||
""" | ||
return account_schema.dump(account), 200 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
from __future__ import annotations | ||
|
||
from flask import url_for | ||
import pytest | ||
|
||
from flexmeasures.data.services.users import find_user_by_email | ||
from flexmeasures.api.tests.utils import get_auth_token | ||
|
||
|
||
def test_get_accounts_missing_auth(client): | ||
""" | ||
Attempt to get accounts with missing auth. | ||
""" | ||
# the case without auth: authentication will fail | ||
get_accounts_response = client.get( | ||
url_for("AccountAPI:index"), headers=make_headers_for(None, client) | ||
) | ||
print("Server responded with:\n%s" % get_accounts_response.data) | ||
assert get_accounts_response.status_code == 401 | ||
|
||
|
||
@pytest.mark.parametrize("as_admin", [True, False]) | ||
def test_get_accounts(client, setup_api_test_data, as_admin): | ||
""" | ||
Get accounts | ||
""" | ||
if as_admin: | ||
headers = make_headers_for("test_admin_user@seita.nl", client) | ||
else: | ||
headers = make_headers_for("test_prosumer_user@seita.nl", client) | ||
get_accounts_response = client.get( | ||
url_for("AccountAPI:index"), | ||
headers=headers, | ||
) | ||
print("Server responded with:\n%s" % get_accounts_response.data) | ||
if as_admin: | ||
assert len(get_accounts_response.json) == 5 | ||
|
||
else: | ||
assert len(get_accounts_response.json) == 1 | ||
get_accounts_response.json[0]["name"] == "Test Prosumer Account" | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"requesting_user_email,status_code", | ||
[ | ||
(None, 401), # no auth is not allowed | ||
("test_prosumer_user_2@seita.nl", 200), # gets their own account, okay | ||
("test_dummy_user_3@seita.nl", 403), # gets from other account | ||
("test_admin_user@seita.nl", 200), # admin can do this from another account | ||
], | ||
) | ||
def test_get_one_account( | ||
client, setup_api_test_data, requesting_user_email, status_code | ||
): | ||
"""Get one account""" | ||
test_user2_account_id = find_user_by_email( | ||
"test_prosumer_user_2@seita.nl" | ||
).account.id | ||
get_account_response = client.get( | ||
url_for("AccountAPI:get", id=test_user2_account_id), | ||
headers=make_headers_for(requesting_user_email, client), | ||
) | ||
print("Server responded with:\n%s" % get_account_response.data) | ||
assert get_account_response.status_code == status_code | ||
if status_code == 200: | ||
assert get_account_response.json["name"] == "Test Prosumer Account" | ||
assert get_account_response.json["account_roles"] == [ | ||
{"id": 1, "name": "Prosumer"} | ||
] | ||
|
||
|
||
def make_headers_for(user_email: str | None, client) -> dict: | ||
headers = {"content-type": "application/json"} | ||
if user_email: | ||
headers["Authorization"] = get_auth_token(client, user_email, "testtest") | ||
return headers |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from typing import List, Optional | ||
|
||
from flexmeasures.data.models.user import Account, AccountRole | ||
|
||
|
||
def get_accounts( | ||
role_name: Optional[str] = None, | ||
) -> List[Account]: | ||
"""Return a list of Account objects. | ||
The role_name parameter allows to filter by role. | ||
""" | ||
account_query = Account.query | ||
|
||
if role_name is not None: | ||
role = AccountRole.query.filter(AccountRole.name == role_name).one_or_none() | ||
if role: | ||
account_query = account_query.filter(Account.account_roles.contains(role)) | ||
else: | ||
return [] | ||
|
||
return account_query.all() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.