Skip to content

Commit 9083385

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 f082196 commit 9083385

21 files changed

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

backend/ibutsu_server/controllers/admin/project_controller.py

Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,11 @@ def admin_get_project(id_, token_info=None, user=None):
5151
:rtype: Project
5252
"""
5353
check_user_is_admin(user)
54-
project = Project.query.get(id_)
55-
if not project:
56-
project = Project.query.filter(Project.name == id_).first()
57-
if not project:
54+
55+
if project := Project.query.get(id_) or Project.query.filter(Project.name == id_).first():
56+
return project.to_dict(with_owner=True)
57+
else:
5858
abort(HTTPStatus.NOT_FOUND)
59-
return project.to_dict(with_owner=True)
6059

6160

6261
def admin_get_project_list(
@@ -127,35 +126,34 @@ def admin_update_project(id_, project=None, body=None, token_info=None, user=Non
127126
return RESPONSE_JSON_REQ
128127
if not is_uuid(id_):
129128
id_ = convert_objectid_to_uuid(id_)
130-
project = Project.query.get(id_)
131129

132-
if not project:
130+
if project := Project.query.get(id_):
131+
# Grab the fields from the request
132+
project_dict = connexion.request.get_json()
133+
134+
# If the "owner" field is set, ignore it
135+
project_dict.pop("owner", None)
136+
137+
# handle updating users separately
138+
for username in project_dict.pop("users", []):
139+
user_to_add = User.query.filter_by(email=username).first()
140+
if user_to_add and user_to_add not in project.users:
141+
project.users.append(user_to_add)
142+
143+
# Make sure the project owner is in the list of users
144+
if project_dict.get("owner_id"):
145+
owner = User.query.get(project_dict["owner_id"])
146+
if owner and owner not in project.users:
147+
project.users.append(owner)
148+
149+
# update the rest of the project info
150+
project.update(project_dict)
151+
session.add(project)
152+
session.commit()
153+
return project.to_dict()
154+
else:
133155
abort(HTTPStatus.NOT_FOUND)
134156

135-
# Grab the fields from the request
136-
project_dict = connexion.request.get_json()
137-
138-
# If the "owner" field is set, ignore it
139-
project_dict.pop("owner", None)
140-
141-
# handle updating users separately
142-
for username in project_dict.pop("users", []):
143-
user_to_add = User.query.filter_by(email=username).first()
144-
if user_to_add and user_to_add not in project.users:
145-
project.users.append(user_to_add)
146-
147-
# Make sure the project owner is in the list of users
148-
if project_dict.get("owner_id"):
149-
owner = User.query.get(project_dict["owner_id"])
150-
if owner and owner not in project.users:
151-
project.users.append(owner)
152-
153-
# update the rest of the project info
154-
project.update(project_dict)
155-
session.add(project)
156-
session.commit()
157-
return project.to_dict()
158-
159157

160158
@validate_uuid
161159
def admin_delete_project(id_, token_info=None, user=None):
@@ -170,6 +168,3 @@ def admin_delete_project(id_, token_info=None, user=None):
170168
return HTTPStatus.OK.phrase, HTTPStatus.OK
171169
else:
172170
abort(HTTPStatus.NOT_FOUND)
173-
session.delete(project)
174-
session.commit()
175-
return HTTPStatus.OK.phrase, HTTPStatus.OK

backend/ibutsu_server/controllers/dashboard_controller.py

Lines changed: 44 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,25 @@ def get_dashboard_list(
6682
"""
6783
query = Dashboard.query
6884
project = None
85+
portal = None
86+
if portal_id is not None and project_id is not None:
87+
return "Dashboard list can only have one of project_id or portal_id", HTTPStatus.BAD_REQUEST
88+
89+
# Project filter injection
6990
if "project_id" in connexion.request.args:
7091
project = Project.query.get(connexion.request.args["project_id"])
7192
if project:
7293
if not project_has_user(project, user):
7394
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
7495
query = query.filter(Dashboard.project_id == project_id)
7596

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

97125

98126
@validate_uuid
99-
def update_dashboard(id_, dashboard=None, token_info=None, user=None):
127+
def update_dashboard(id_, dashboard=None, token_info=None, user=None) -> dict:
100128
"""Update a dashboard
101129
102-
:param id: ID of test dashboard
130+
:param id: ID of dashboard
103131
:type id: str
104132
:param body: Dashboard
105133
:type body: dict | bytes
@@ -113,19 +141,25 @@ def update_dashboard(id_, dashboard=None, token_info=None, user=None):
113141
dashboard_dict["metadata"]["project"], user
114142
):
115143
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
144+
145+
# TODO user/admin check for portal ref
146+
116147
dashboard = Dashboard.query.get(id_)
117148
if not dashboard:
118149
return "Dashboard not found", HTTPStatus.NOT_FOUND
119-
if project_has_user(dashboard.project, user):
150+
if dashboard.project_id is not None and project_has_user(dashboard.project, user):
120151
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
152+
153+
# TODO user/admin check for portal ref
154+
121155
dashboard.update(connexion.request.get_json())
122156
session.add(dashboard)
123157
session.commit()
124158
return dashboard.to_dict()
125159

126160

127161
@validate_uuid
128-
def delete_dashboard(id_, token_info=None, user=None):
162+
def delete_dashboard(id_, token_info=None, user=None) -> tuple[str, int]:
129163
"""Deletes a dashboard
130164
131165
:param id: ID of the dashboard to delete
@@ -136,8 +170,9 @@ def delete_dashboard(id_, token_info=None, user=None):
136170
dashboard = Dashboard.query.get(id_)
137171
if not dashboard:
138172
return HTTPStatus.NOT_FOUND.phrase, HTTPStatus.NOT_FOUND
139-
if not project_has_user(dashboard.project, user):
173+
if dashboard.project_id is not None and not project_has_user(dashboard.project, user):
140174
return HTTPStatus.FORBIDDEN.phrase, HTTPStatus.FORBIDDEN
175+
# TODO user/admin check for portal ref
141176
widget_configs = WidgetConfig.query.filter(WidgetConfig.dashboard_id == dashboard.id).all()
142177
for widget_config in widget_configs:
143178
session.delete(widget_config)

0 commit comments

Comments
 (0)