Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 77 additions & 2 deletions userapp/api/routes/_util.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from userapp.api.util import create_one_endpoint
from userapp.core.models.tables import UserSubmit, User
from userapp.core.models.tables import UserSubmit, User, UserProject, UserGroup
from userapp.core.models.views import JoinedProjectView, UserGroupView
from userapp.core.schemas.user_project import UserProjectPatch
from userapp.core.schemas.user_group import UserGroupPatch
from userapp.core.schemas.user_submit import UserSubmitTableSchema, UserSubmitPost


Expand Down Expand Up @@ -31,4 +36,74 @@ async def _patch_user_submit_nodes(session: AsyncSession, user: User, new_submit
for_auth_netid=for_auth_netid,
**submit_node.model_dump(),
)
await create_one_endpoint(session, UserSubmit, user_submit_model)
await create_one_endpoint(session, UserSubmit, user_submit_model)


async def _patch_user_project(
session: AsyncSession,
user_id: int,
project_id: int,
patch: UserProjectPatch,
) -> JoinedProjectView:
"""Patch the UserProject row identified by (user_id, project_id) and return
the matching row from the joined_projects view. Raises 404 if no such
membership exists."""

row = await session.scalar(
select(UserProject).where(
UserProject.user_id == user_id,
UserProject.project_id == project_id,
)
)
if row is None:
raise HTTPException(
status_code=404,
detail=f"User {user_id} is not a member of project {project_id}",
)

for key, value in patch.model_dump(exclude_unset=True).items():
setattr(row, key, value)
await session.flush()

view_row = await session.scalar(
select(JoinedProjectView).where(
JoinedProjectView.id == user_id,
JoinedProjectView.project_id == project_id,
)
)
return view_row


async def _patch_user_group(
session: AsyncSession,
user_id: int,
group_id: int,
patch: UserGroupPatch,
) -> UserGroupView:
"""Patch the UserGroup row identified by (user_id, group_id) and return the
matching row from the user_group_memberships view. Raises 404 if no such
membership exists."""

row = await session.scalar(
select(UserGroup).where(
UserGroup.user_id == user_id,
UserGroup.group_id == group_id,
)
)
if row is None:
raise HTTPException(
status_code=404,
detail=f"User {user_id} is not a member of group {group_id}",
)

for key, value in patch.model_dump(exclude_unset=True).items():
setattr(row, key, value)
await session.flush()

view_row = await session.scalar(
select(UserGroupView).where(
UserGroupView.user_id == user_id,
UserGroupView.group_id == group_id,
)
)
return view_row
18 changes: 16 additions & 2 deletions userapp/api/routes/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
from sqlalchemy import select, delete

from userapp.core.models.views import GroupUserView
from userapp.core.schemas.user_group import UserGroupPost
from userapp.core.schemas.user_group import UserGroupPost, UserGroupPatch
from userapp.db import session_generator
from userapp.query_parser import get_filter_query_params
from userapp.api.routes.security import check_is_admin
from userapp.api.routes._util import _patch_user_group
from userapp.api.util import list_endpoint, get_one_endpoint, create_one_endpoint, update_one_endpoint, list_select_stmt, \
delete_one_endpoint, with_db_error_handling
from userapp.core.schemas.general import Relationship, GroupUserView as GroupUserViewSchema
from userapp.core.schemas.general import Relationship, GroupUserView as GroupUserViewSchema, UserGroupView as UserGroupViewSchema
from userapp.core.schemas.groups import GroupGet, GroupPost, GroupPatch
from userapp.core.schemas.users import UserGet

Expand Down Expand Up @@ -86,3 +87,16 @@ async def remove_user_from_group(group_id: int, user_id: int, session=Depends(se

if result.rowcount == 0:
raise HTTPException(status_code=404, detail="User not found in group")


@with_db_error_handling
@router.patch("/{group_id}/users/{user_id}")
async def patch_user_in_group(
group_id: int,
user_id: int,
patch: UserGroupPatch,
session=Depends(session_generator),
) -> UserGroupViewSchema:
"""Patch a user's membership in a group (managed_by)."""
return await _patch_user_group(session, user_id, group_id, patch)

14 changes: 13 additions & 1 deletion userapp/api/routes/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
from userapp.api.util import list_endpoint, get_one_endpoint, create_one_endpoint, update_one_endpoint, delete_one_endpoint, \
list_select_stmt
from userapp.core.schemas.projects import ProjectGet, ProjectPost, ProjectPatch
from userapp.core.schemas.user_project import UserProjectPost, UserProjectTableSchema
from userapp.core.schemas.user_project import UserProjectPost, UserProjectTableSchema, UserProjectPatch
from userapp.core.schemas.note import NoteGet, NoteTableSchema, NotePost, NoteGetFull
from userapp.core.schemas.general import JoinedProjectView as JoinedProjectViewSchema
from userapp.core.models.tables import Project as ProjectTable, Note as NoteTable, UserNote, User, UserProject
from userapp.core.models.views import JoinedProjectView as JoinedProjectViewTable
from userapp.core.schemas.users import UserGet
from userapp.api.routes._util import _patch_user_project

# Rebuild fields that use forward references to avoid circular imports
NoteGet.model_rebuild(_types_namespace={'UserGet': UserGet})
Expand Down Expand Up @@ -111,6 +112,17 @@ async def remove_user_from_project(project_id: int, user_id: int, session=Depend
raise HTTPException(status_code=404, detail="User not found in project")


@router.patch("/{project_id}/users/{user_id}")
async def patch_user_in_project(
project_id: int,
user_id: int,
patch: UserProjectPatch,
session=Depends(session_generator),
) -> JoinedProjectViewSchema:
"""Patch a user's membership in a project (role, is_primary, managed_by)."""
return await _patch_user_project(session, user_id, project_id, patch)


@router.get("/{project_id}/notes")
async def get_project_notes(project_id: int, response: Response, page: int = 0, page_size: int = 100, filter_query_params=Depends(get_filter_query_params), session=Depends(session_generator)) -> list[NoteGetFull]:
"""Get notes associated with a project"""
Expand Down
30 changes: 28 additions & 2 deletions userapp/api/routes/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@
list_select_stmt, update_one_endpoint
from userapp.core.schemas.users import UserGet, UserPost, UserPatch, UserPostFull, UserPatchFull, \
RestrictedUserPatch, UserTableSchema, UserGetFull
from userapp.core.schemas.user_project import UserProjectPost, UserProjectTableSchema
from userapp.core.schemas.user_project import UserProjectPost, UserProjectTableSchema, UserProjectPatch
from userapp.core.schemas.user_group import UserGroupPatch
from userapp.core.schemas.general import JoinedProjectView as JoinedProjectViewSchema, UserGroupView as UserGroupViewSchema
from userapp.core.schemas.user_submit import UserSubmitPost, UserSubmitTableSchema, UserSubmitGet
from userapp.core.schemas.note import NoteGet
from userapp.core.models.views import JoinedProjectView as JoinedProjectViewTable, \
UserSubmitNodesView as UserSubmitNodesViewTable, UserSubmitNodesView, UserGroupView as UserGroupViewTable
from userapp.core.models.tables import User as UserTable, UserProject, UserSubmit, Group, UserGroup, Note as NoteTable
from userapp.api.load_options import user_load_options
from userapp.api.routes._util import _patch_user_submit_nodes
from userapp.api.routes._util import _patch_user_submit_nodes, _patch_user_project, _patch_user_group

# Rebuild field for those that would cause circular imports
NoteGet.model_rebuild(_types_namespace={'UserGet': UserGet})
Expand Down Expand Up @@ -139,3 +140,28 @@ async def get_user_groups(user_id: int, response: Response, page: int = 0, page_
# Join Group to User via the UserGroups association table and filter by user_id
select_stmt = select(UserGroupViewTable).where(UserGroupViewTable.user_id == user_id)
return await list_select_stmt(session, select_stmt, UserGroupViewTable, response, filter_query_params, page, page_size)


@router.patch("/{user_id}/projects/{project_id}")
async def patch_user_project(
user_id: int,
project_id: int,
patch: UserProjectPatch,
session=Depends(session_generator),
check_is_admin=Depends(check_is_admin),
) -> JoinedProjectViewSchema:
"""Patch a user's membership in a project (role, is_primary, managed_by)."""
return await _patch_user_project(session, user_id, project_id, patch)


@router.patch("/{user_id}/groups/{group_id}")
async def patch_user_group(
user_id: int,
group_id: int,
patch: UserGroupPatch,
session=Depends(session_generator),
check_is_admin=Depends(check_is_admin),
) -> UserGroupViewSchema:
"""Patch a user's membership in a group (managed_by)."""
return await _patch_user_group(session, user_id, group_id, patch)

Loading