Skip to content

Commit cd8306b

Browse files
Added manager to the admin dashboard (#1352)
Co-authored-by: Matthew Evans <[email protected]>
1 parent da32722 commit cd8306b

File tree

10 files changed

+668
-62
lines changed

10 files changed

+668
-62
lines changed

pydatalab/schemas/cell.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,16 @@
363363
"name"
364364
]
365365
},
366+
"UserRole": {
367+
"title": "UserRole",
368+
"description": "An enumeration.",
369+
"enum": [
370+
"user",
371+
"admin",
372+
"manager"
373+
],
374+
"type": "string"
375+
},
366376
"AccountStatus": {
367377
"title": "AccountStatus",
368378
"description": "A string enum representing the account status.",
@@ -426,6 +436,14 @@
426436
"type": "string"
427437
}
428438
},
439+
"role": {
440+
"default": "user",
441+
"allOf": [
442+
{
443+
"$ref": "#/definitions/UserRole"
444+
}
445+
]
446+
},
429447
"account_status": {
430448
"default": "unverified",
431449
"allOf": [

pydatalab/schemas/equipment.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,16 @@
327327
"name"
328328
]
329329
},
330+
"UserRole": {
331+
"title": "UserRole",
332+
"description": "An enumeration.",
333+
"enum": [
334+
"user",
335+
"admin",
336+
"manager"
337+
],
338+
"type": "string"
339+
},
330340
"AccountStatus": {
331341
"title": "AccountStatus",
332342
"description": "A string enum representing the account status.",
@@ -390,6 +400,14 @@
390400
"type": "string"
391401
}
392402
},
403+
"role": {
404+
"default": "user",
405+
"allOf": [
406+
{
407+
"$ref": "#/definitions/UserRole"
408+
}
409+
]
410+
},
393411
"account_status": {
394412
"default": "unverified",
395413
"allOf": [

pydatalab/schemas/sample.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,16 @@
416416
"name"
417417
]
418418
},
419+
"UserRole": {
420+
"title": "UserRole",
421+
"description": "An enumeration.",
422+
"enum": [
423+
"user",
424+
"admin",
425+
"manager"
426+
],
427+
"type": "string"
428+
},
419429
"AccountStatus": {
420430
"title": "AccountStatus",
421431
"description": "A string enum representing the account status.",
@@ -479,6 +489,14 @@
479489
"type": "string"
480490
}
481491
},
492+
"role": {
493+
"default": "user",
494+
"allOf": [
495+
{
496+
"$ref": "#/definitions/UserRole"
497+
}
498+
]
499+
},
482500
"account_status": {
483501
"default": "unverified",
484502
"allOf": [

pydatalab/schemas/startingmaterial.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,16 @@
469469
"name"
470470
]
471471
},
472+
"UserRole": {
473+
"title": "UserRole",
474+
"description": "An enumeration.",
475+
"enum": [
476+
"user",
477+
"admin",
478+
"manager"
479+
],
480+
"type": "string"
481+
},
472482
"AccountStatus": {
473483
"title": "AccountStatus",
474484
"description": "A string enum representing the account status.",
@@ -532,6 +542,14 @@
532542
"type": "string"
533543
}
534544
},
545+
"role": {
546+
"default": "user",
547+
"allOf": [
548+
{
549+
"$ref": "#/definitions/UserRole"
550+
}
551+
]
552+
},
535553
"account_status": {
536554
"default": "unverified",
537555
"allOf": [

pydatalab/src/pydatalab/models/people.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pydantic import EmailStr as PydanticEmailStr
77

88
from pydatalab.models.entries import Entry
9-
from pydatalab.models.utils import PyObjectId
9+
from pydatalab.models.utils import PyObjectId, UserRole
1010

1111

1212
class IdentityType(str, Enum):
@@ -115,6 +115,9 @@ class Person(Entry):
115115
managers: list[PyObjectId] | None
116116
"""A list of user IDs that can manage this person's items."""
117117

118+
role: UserRole = Field(UserRole.USER)
119+
"""The role assigned to this person."""
120+
118121
account_status: AccountStatus = Field(AccountStatus.UNVERIFIED)
119122
"""The status of the user's account."""
120123

pydatalab/src/pydatalab/routes/v0_1/admin.py

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,37 @@
55
from flask_login import current_user
66

77
from pydatalab.config import CONFIG
8+
from pydatalab.models.people import Person
89
from pydatalab.mongo import flask_mongo
910
from pydatalab.permissions import admin_only, get_default_permissions
1011

12+
13+
def check_manager_cycle(user_id_str, new_manager_id_str):
14+
visited = set()
15+
current_id = new_manager_id_str
16+
17+
while current_id is not None:
18+
if current_id == user_id_str:
19+
return True
20+
if current_id in visited:
21+
break
22+
visited.add(current_id)
23+
24+
try:
25+
manager = flask_mongo.db.users.find_one({"_id": ObjectId(current_id)})
26+
if manager and "managers" in manager and manager["managers"]:
27+
if isinstance(manager["managers"], list) and len(manager["managers"]) > 0:
28+
current_id = manager["managers"][0]
29+
else:
30+
break
31+
else:
32+
break
33+
except Exception:
34+
break
35+
36+
return False
37+
38+
1139
ADMIN = Blueprint("admins", __name__)
1240

1341

@@ -37,13 +65,21 @@ def get_users():
3765
"then": "user",
3866
"else": {"$arrayElemAt": ["$role.role", 0]},
3967
}
40-
}
68+
},
4169
}
4270
},
4371
]
4472
)
4573

46-
return jsonify({"status": "success", "data": list(users)})
74+
users_list = list(users)
75+
76+
for user in users_list:
77+
if "managers" not in user:
78+
user["managers"] = []
79+
elif not isinstance(user["managers"], list):
80+
user["managers"] = []
81+
82+
return jsonify({"status": "success", "data": [Person(**d).dict() for d in users_list]})
4783

4884

4985
@ADMIN.route("/roles/<user_id>", methods=["PATCH"])
@@ -97,6 +133,59 @@ def save_role(user_id):
97133
return (jsonify({"status": "success"}), 200)
98134

99135

136+
@ADMIN.route("/users/<user_id>/managers", methods=["PATCH"])
137+
def update_user_managers(user_id):
138+
"""Update the managers for a specific user using ObjectIds"""
139+
140+
request_json = request.get_json()
141+
142+
if request_json is None or "managers" not in request_json:
143+
return jsonify({"status": "error", "message": "Managers list not provided"}), 400
144+
145+
managers = request_json["managers"]
146+
147+
if not isinstance(managers, list):
148+
return jsonify({"status": "error", "message": "Managers must be a list"}), 400
149+
150+
existing_user = flask_mongo.db.users.find_one({"_id": ObjectId(user_id)})
151+
if not existing_user:
152+
return jsonify({"status": "error", "message": "User not found"}), 404
153+
154+
manager_object_ids = []
155+
for manager_id in managers:
156+
if manager_id:
157+
try:
158+
manager_oid = ObjectId(manager_id)
159+
except Exception:
160+
return jsonify(
161+
{"status": "error", "message": f"Invalid manager ID format: {manager_id}"}
162+
), 400
163+
164+
if not flask_mongo.db.users.find_one({"_id": manager_oid}):
165+
return jsonify(
166+
{"status": "error", "message": f"Manager with ID {manager_id} not found"}
167+
), 404
168+
169+
if check_manager_cycle(user_id, manager_id):
170+
return jsonify(
171+
{
172+
"status": "error",
173+
"message": "Cannot assign manager: would create a circular management hierarchy",
174+
}
175+
), 400
176+
177+
manager_object_ids.append(str(manager_oid))
178+
179+
update_result = flask_mongo.db.users.update_one(
180+
{"_id": ObjectId(user_id)}, {"$set": {"managers": manager_object_ids}}
181+
)
182+
183+
if update_result.matched_count != 1:
184+
return jsonify({"status": "error", "message": "Unable to update user managers"}), 400
185+
186+
return jsonify({"status": "success"}), 200
187+
188+
100189
@ADMIN.route("/items/<refcode>/invalidate-access-token", methods=["POST"])
101190
def invalidate_access_token(refcode: str):
102191
if len(refcode.split(":")) != 2:

0 commit comments

Comments
 (0)