Skip to content

Commit 7f09233

Browse files
committed
Model and controller updates for portals
Portal does not have relationship to runs, results, etc. only dashboards. DB upgrade_6, dashboard and widget_config alterations New controller for portal and portal admin Updates to widget_config controller and dashboard controller Update openapi spec and add unit test Restructure test_widget_config_controller Increase coverage with subtests Define common headers and http response assertion failure message for tests
1 parent 51a57a9 commit 7f09233

20 files changed

+1260
-307
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
from http import HTTPStatus
2+
3+
import connexion
4+
from flask import abort
5+
6+
from ibutsu_server.constants import RESPONSE_JSON_REQ
7+
from ibutsu_server.db.base import session
8+
from ibutsu_server.db.models import Portal, User
9+
from ibutsu_server.filters import convert_filter
10+
from ibutsu_server.util.admin import check_user_is_admin
11+
from ibutsu_server.util.query import get_offset
12+
from ibutsu_server.util.uuid import convert_objectid_to_uuid, is_uuid, validate_uuid
13+
14+
15+
def admin_add_portal(portal=None, token_info=None, user=None) -> tuple[dict, int]:
16+
"""Create a portal
17+
18+
:param body: Portal
19+
:type body: dict | bytes
20+
21+
:rtype: Portal
22+
"""
23+
check_user_is_admin(user)
24+
if not connexion.request.is_json:
25+
return RESPONSE_JSON_REQ
26+
portal = Portal.from_dict(**connexion.request.get_json())
27+
# check if portal already exists
28+
if portal.id and Portal.query.get(portal.id):
29+
return f"Portal id {portal.id} already exist", HTTPStatus.BAD_REQUEST
30+
user = User.query.get(user)
31+
if user:
32+
portal.owner = user
33+
session.add(portal)
34+
session.commit()
35+
return portal.to_dict(), HTTPStatus.CREATED
36+
37+
38+
@validate_uuid
39+
def admin_get_portal(id_, token_info=None, user=None) -> dict:
40+
"""Get a single portal by ID
41+
42+
:param id: ID of test portal
43+
:type id: str
44+
45+
:rtype: Portal
46+
"""
47+
check_user_is_admin(user)
48+
portal = Portal.query.get(id_)
49+
if not portal:
50+
portal = Portal.query.filter(Portal.name == id_).first()
51+
if not portal:
52+
abort(HTTPStatus.NOT_FOUND)
53+
return portal.to_dict(with_owner=True)
54+
55+
56+
def admin_get_portal_list(
57+
filter_=None,
58+
owner_id=None,
59+
group_id=None,
60+
page=1,
61+
page_size=25,
62+
token_info=None,
63+
user=None,
64+
) -> dict[list[dict], dict]:
65+
"""Get a list of portals
66+
67+
:param owner_id: Filter portals by owner ID
68+
:type owner_id: str
69+
:param group_id: Filter portals by group ID
70+
:type group_id: str
71+
:param limit: Limit the portals
72+
:type limit: int
73+
:param offset: Offset the portals
74+
:type offset: int
75+
76+
:rtype: List[Portal]
77+
"""
78+
check_user_is_admin(user)
79+
query = Portal.query
80+
81+
if filter_:
82+
for filter_string in filter_:
83+
filter_clause = convert_filter(filter_string, Portal)
84+
if filter_clause is not None:
85+
query = query.filter(filter_clause)
86+
if owner_id:
87+
query = query.filter(Portal.owner_id == owner_id)
88+
if group_id:
89+
query = query.filter(Portal.group_id == group_id)
90+
91+
offset = get_offset(page, page_size)
92+
total_items = query.count()
93+
total_pages = (total_items // page_size) + (1 if total_items % page_size > 0 else 0)
94+
if offset > 9223372036854775807: # max value of bigint
95+
return "The page number is too big.", HTTPStatus.BAD_REQUEST
96+
portals = query.offset(offset).limit(page_size).all()
97+
return {
98+
"portals": [portal.to_dict(with_owner=True) for portal in portals],
99+
"pagination": {
100+
"page": page,
101+
"pageSize": page_size,
102+
"totalItems": total_items,
103+
"totalPages": total_pages,
104+
},
105+
}
106+
107+
108+
@validate_uuid
109+
def admin_update_portal(id_, portal=None, body=None, token_info=None, user=None):
110+
"""Update a portal
111+
112+
:param id: ID of portal
113+
:type id: str
114+
:param body: Portal
115+
:type body: dict | bytes
116+
117+
:rtype: Portal
118+
"""
119+
check_user_is_admin(user)
120+
if not connexion.request.is_json:
121+
return RESPONSE_JSON_REQ
122+
if not is_uuid(id_):
123+
id_ = convert_objectid_to_uuid(id_)
124+
portal = Portal.query.get(id_)
125+
126+
if not portal:
127+
abort(HTTPStatus.NOT_FOUND)
128+
129+
# Grab the fields from the request
130+
portal_dict = connexion.request.get_json()
131+
132+
# If the "owner" field is set, ignore it
133+
portal_dict.pop("owner", None)
134+
135+
# update the portal info
136+
portal.update(portal_dict)
137+
session.add(portal)
138+
session.commit()
139+
return portal.to_dict()
140+
141+
142+
@validate_uuid
143+
def admin_delete_portal(id_, token_info=None, user=None):
144+
"""Delete a single portal"""
145+
check_user_is_admin(user)
146+
if not is_uuid(id_):
147+
return f"Portal ID {id_} is not in UUID format", HTTPStatus.BAD_REQUEST
148+
portal = Portal.query.get(id_)
149+
if not portal:
150+
abort(HTTPStatus.NOT_FOUND)
151+
session.delete(portal)
152+
session.commit()
153+
return HTTPStatus.OK.phrase, HTTPStatus.OK

backend/ibutsu_server/controllers/dashboard_controller.py

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from ibutsu_server.util.uuid import validate_uuid
1212

1313

14-
def add_dashboard(dashboard=None, token_info=None, user=None):
14+
def add_dashboard(dashboard=None, token_info=None, user=None) -> tuple[dict, int]:
1515
"""Create a dashboard
1616
1717
:param body: Dashboard
@@ -22,17 +22,30 @@ def add_dashboard(dashboard=None, token_info=None, user=None):
2222
if not connexion.request.is_json:
2323
return RESPONSE_JSON_REQ
2424
dashboard = Dashboard.from_dict(**connexion.request.get_json())
25+
26+
if dashboard.portal_id and dashboard.project_id:
27+
return "Dashboard can only have one of project_id or portal_id", HTTPStatus.BAD_REQUEST
28+
29+
if not (dashboard.portal_id or dashboard.project_id):
30+
return "Dashboard needs either project_id or portal_id", HTTPStatus.BAD_REQUEST
31+
2532
if dashboard.project_id and not project_has_user(dashboard.project_id, user):
2633
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
34+
35+
# TODO utility function to resolve all users with at least one project set
36+
# compare to projects assigned to the portal? or portals are open to all projects
37+
# otherwise, limit to admin users or new portal_admin permission
38+
2739
if dashboard.user_id and not User.query.get(dashboard.user_id):
2840
return f"User with ID {dashboard.user_id} doesn't exist", HTTPStatus.BAD_REQUEST
41+
2942
session.add(dashboard)
3043
session.commit()
3144
return dashboard.to_dict(), HTTPStatus.CREATED
3245

3346

3447
@validate_uuid
35-
def get_dashboard(id_, token_info=None, user=None):
48+
def get_dashboard(id_, token_info=None, user=None) -> dict:
3649
"""Get a single dashboard by ID
3750
3851
:param id: ID of test dashboard
@@ -45,16 +58,19 @@ def get_dashboard(id_, token_info=None, user=None):
4558
return "Dashboard not found", HTTPStatus.NOT_FOUND
4659
if dashboard and dashboard.project and not project_has_user(dashboard.project, user):
4760
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
61+
# TODO test against dashboard with only portal set
4862
return dashboard.to_dict()
4963

5064

5165
def get_dashboard_list(
52-
filter_=None, project_id=None, page=1, page_size=25, token_info=None, user=None
53-
):
66+
filter_=None, project_id=None, portal_id=None, page=1, page_size=25, token_info=None, user=None
67+
) -> dict[list[dict], dict]:
5468
"""Get a list of dashboards
5569
5670
:param project_id: Filter dashboards by project ID
5771
:type project_id: str
72+
:param portal_id: Filter dashboards by portal ID
73+
:type portal_id: str
5874
:param user_id: Filter dashboards by user ID
5975
:type user_id: str
6076
:param limit: Limit the dashboards
@@ -66,13 +82,24 @@ def get_dashboard_list(
6682
"""
6783
query = Dashboard.query
6884
project = None
85+
if portal_id is not None and project_id is not None:
86+
return "Dashboard list can only have one of project_id or portal_id", HTTPStatus.BAD_REQUEST
87+
88+
# Project filter injection
6989
if "project_id" in connexion.request.args:
7090
project = Project.query.get(connexion.request.args["project_id"])
7191
if project:
7292
if not project_has_user(project, user):
7393
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
7494
query = query.filter(Dashboard.project_id == project_id)
7595

96+
# Portal filter injection
97+
if "portal_id" in connexion.request.args:
98+
portal = Project.query.get(connexion.request.args["portal_id"])
99+
if portal:
100+
query = query.filter(Dashboard.portal_id == portal_id)
101+
102+
# Other filters follow
76103
if filter_:
77104
for filter_string in filter_:
78105
filter_clause = convert_filter(filter_string, Dashboard)
@@ -96,10 +123,10 @@ def get_dashboard_list(
96123

97124

98125
@validate_uuid
99-
def update_dashboard(id_, dashboard=None, token_info=None, user=None):
126+
def update_dashboard(id_, dashboard=None, token_info=None, user=None) -> dict:
100127
"""Update a dashboard
101128
102-
:param id: ID of test dashboard
129+
:param id: ID of dashboard
103130
:type id: str
104131
:param body: Dashboard
105132
:type body: dict | bytes
@@ -113,19 +140,25 @@ def update_dashboard(id_, dashboard=None, token_info=None, user=None):
113140
dashboard_dict["metadata"]["project"], user
114141
):
115142
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
143+
144+
# TODO user/admin check for portal ref
145+
116146
dashboard = Dashboard.query.get(id_)
117147
if not dashboard:
118148
return "Dashboard not found", HTTPStatus.NOT_FOUND
119-
if project_has_user(dashboard.project, user):
149+
if dashboard.project_id is not None and project_has_user(dashboard.project, user):
120150
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
151+
152+
# TODO user/admin check for portal ref
153+
121154
dashboard.update(connexion.request.get_json())
122155
session.add(dashboard)
123156
session.commit()
124157
return dashboard.to_dict()
125158

126159

127160
@validate_uuid
128-
def delete_dashboard(id_, token_info=None, user=None):
161+
def delete_dashboard(id_, token_info=None, user=None) -> tuple[str, int]:
129162
"""Deletes a dashboard
130163
131164
:param id: ID of the dashboard to delete
@@ -136,8 +169,9 @@ def delete_dashboard(id_, token_info=None, user=None):
136169
dashboard = Dashboard.query.get(id_)
137170
if not dashboard:
138171
return HTTPStatus.NOT_FOUND.phrase, HTTPStatus.NOT_FOUND
139-
if not project_has_user(dashboard.project, user):
172+
if dashboard.project_id is not None and not project_has_user(dashboard.project, user):
140173
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
174+
# TODO user/admin check for portal ref
141175
widget_configs = WidgetConfig.query.filter(WidgetConfig.dashboard_id == dashboard.id).all()
142176
for widget_config in widget_configs:
143177
session.delete(widget_config)

0 commit comments

Comments
 (0)