Coverage for model/user.py: 100%
71 statements
« prev ^ index » next coverage.py v7.9.2, created at 2026-07-24 16:42 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2026-07-24 16:42 +0000
1import logging
2from urllib import parse
3from typing import Optional
4from fastapi import APIRouter, Depends
5from mongo import engine
6from mongo.utils import drop_none
7from mongo import *
8from .utils import *
9from .auth import identity_verify, login_required
10from .schemas import AddUserBody, UpdateUserBody, GetUserListQuery
12__all__ = ['user_router', 'user_options_router']
14logger = logging.getLogger(__name__)
16# Handles CORS preflight (OPTIONS) without authentication, matching original Flask before_request behavior
17user_options_router = APIRouter()
20@user_options_router.options('/{path:path}')
21@user_options_router.options('')
22def _user_options_handler():
23 from fastapi.responses import Response
24 return Response(status_code=200)
27# All user-management endpoints require admin access
28user_router = APIRouter(dependencies=[identity_verify(0)])
31@user_router.get('')
32def get_user_list(query: GetUserListQuery = Depends()):
33 offset = query.offset
34 count = query.count
35 course = query.course
36 role = query.role
37 try:
38 if offset is not None:
39 offset = int(offset)
40 if count is not None:
41 count = int(count)
42 if role is not None:
43 role = int(role)
44 except (TypeError, ValueError):
45 return HTTPError('offset, count and role must be integer', 400)
46 if course is not None:
47 course = parse.unquote(course)
49 filter_query = drop_none({'courses': course, 'role': role})
50 user_list = engine.User.objects(**filter_query)
51 if offset is not None:
52 user_list = user_list[offset:]
53 if count is not None:
54 user_list = user_list[:count]
56 user_list = [User(u).info for u in user_list]
57 return HTTPResponse(data=user_list)
60@user_router.get('/summary')
61def get_user_summary():
62 user_count = engine.User.objects.count()
63 breakdown = [{
64 "role": role.name.lower(),
65 "count": engine.User.objects(role=role.value).count()
66 } for role in engine.User.Role]
67 return HTTPResponse(data={"userCount": user_count, "breakdown": breakdown})
70@user_router.post('')
71def add_user(body: AddUserBody):
72 '''Directly add a user without activation required. Admin only.'''
73 try:
74 User.signup(body.username, body.password, body.email).activate()
75 except ValidationError as ve:
76 return HTTPError('Signup Failed', 400, data=ve.to_dict())
77 except NotUniqueError:
78 return HTTPError('User Exists', 400)
79 except ValueError:
80 return HTTPError('Not Allowed Name', 400)
81 return HTTPResponse()
84@user_router.patch('/{username}')
85def update_user(
86 body: UpdateUserBody,
87 user: User = Depends(login_required),
88 target_user: User = get_doc('username', User),
89):
90 if body.password is not None:
91 target_user.change_password(body.password)
92 logger.info('admin changed user password '
93 f'[actor={user.username}, user={target_user.username}]')
94 payload = drop_none({
95 'profile__displayed_name': body.displayed_name,
96 'role': body.role,
97 })
98 if len(payload):
99 fields = [*payload.keys()]
100 target_user.update(**payload)
101 logger.info(
102 'admin changed user info '
103 f'[actor={user.username}, user={target_user.username}, fields={fields}]'
104 )
105 return HTTPResponse()