From ef113e216dc1bcd0f85422d1be563564b8a968b5 Mon Sep 17 00:00:00 2001 From: Aditi Chikkali Date: Fri, 12 Jun 2026 15:23:02 -0400 Subject: [PATCH 1/3] added new /person/proposals endpoint --- src/nsls2api/api/models/proposal_model.py | 17 ++++++++- src/nsls2api/api/v1/user_api.py | 43 ++++++++++++++++++++++- src/nsls2api/services/proposal_service.py | 15 ++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/nsls2api/api/models/proposal_model.py b/src/nsls2api/api/models/proposal_model.py index b426e65b..19c5c2dd 100644 --- a/src/nsls2api/api/models/proposal_model.py +++ b/src/nsls2api/api/models/proposal_model.py @@ -163,4 +163,19 @@ class ProposalIdDataSessionList(pydantic.BaseModel): proposals: list[ProposalIdDataSession] count: int page_size: int - page: int \ No newline at end of file + page: int + +class ProposalSummaryForUser(pydantic.BaseModel): + proposal_id: str + title: str + saf_ids: list[str] + principal_investigator: Optional[User] + instruments: Optional[list[str]] + cycles: Optional[list[str]] + data_session: Optional[str] + +class UserProposalsList(pydantic.BaseModel): + username: str + count: int + current_cycle: Optional[str] = None + proposals: list[ProposalSummaryForUser] diff --git a/src/nsls2api/api/v1/user_api.py b/src/nsls2api/api/v1/user_api.py index 5a9d7211..60a96ffe 100644 --- a/src/nsls2api/api/v1/user_api.py +++ b/src/nsls2api/api/v1/user_api.py @@ -1,15 +1,18 @@ from typing import Annotated import fastapi -from fastapi import Depends +from fastapi import Depends,Header, HTTPException from nsls2api.api.models.person_model import DataSessionAccess, Person +from nsls2api.api.models.proposal_model import ProposalSummaryForUser, UserProposalsList from nsls2api.infrastructure.security import ( get_current_user, ) from nsls2api.services import ( bnlpeople_service, person_service, + proposal_service, + facility_service, ) router = fastapi.APIRouter() @@ -92,3 +95,41 @@ async def get_myself(current_user: Annotated[Person, Depends(get_current_user)]) async def get_data_sessions_by_username(username: str): data_access = await person_service.data_sessions_by_username(username) return data_access + + +@router.get("/person/proposals", response_model=UserProposalsList, summary="Fetch proposals for a user including, SAF ID's, PI details") +async def get_proposals_for_username( + username: str = Header(..., description="Username to fetch proposals for") +): + if not username: + raise HTTPException(status_code=400, detail="Username header is required") + + current_cycle, proposals = await proposal_service.fetch_proposals_for_username( + username + ) + + if current_cycle is None: + raise HTTPException(status_code=404, detail="No current operating cycle found") + + proposal_summaries = [] + for proposal in proposals: + pi = next((u for u in proposal.users if u.is_pi), None) + saf_ids = [s.saf_id for s in (proposal.safs or []) if s.saf_id] + proposal_summaries.append( + ProposalSummaryForUser( + proposal_id=proposal.proposal_id, + title=proposal.title, + saf_ids=saf_ids, + principal_investigator=pi, + instruments=proposal.instruments, + cycles=proposal.cycles, + data_session=proposal.data_session, + ) + ) + + return UserProposalsList( + username=username, + count=len(proposal_summaries), + current_cycle=current_cycle, + proposals=proposal_summaries, + ) diff --git a/src/nsls2api/services/proposal_service.py b/src/nsls2api/services/proposal_service.py index 2d197cf7..057f8da7 100644 --- a/src/nsls2api/services/proposal_service.py +++ b/src/nsls2api/services/proposal_service.py @@ -807,3 +807,18 @@ async def generate_fake_test_proposal( await Proposal.insert_one(proposal) return proposal + +async def fetch_proposals_for_username(username: str, facility_name: FacilityName = FacilityName.nsls2) -> tuple[str | None, list[Proposal]]: + """Retrieve all proposals associated with given username for current operating cycle""" + + current_cycle = await facility_service.current_operating_cycle(facility_name) + + if not current_cycle: return None, [] + + proposals = await Proposal.find( + And( + ElemMatch(Proposal.users, {"username": username}), + In(Proposal.cycles, [current_cycle]), + ) + ).to_list() + return current_cycle, proposals From 1a5027fd69959fdedca594c77d392a90044a3556 Mon Sep 17 00:00:00 2001 From: Aditi Chikkali Date: Fri, 12 Jun 2026 15:27:22 -0400 Subject: [PATCH 2/3] format user_api.py --- src/nsls2api/api/v1/user_api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/nsls2api/api/v1/user_api.py b/src/nsls2api/api/v1/user_api.py index 60a96ffe..8e07e9b0 100644 --- a/src/nsls2api/api/v1/user_api.py +++ b/src/nsls2api/api/v1/user_api.py @@ -1,6 +1,6 @@ +import fastapi from typing import Annotated -import fastapi from fastapi import Depends,Header, HTTPException from nsls2api.api.models.person_model import DataSessionAccess, Person @@ -12,7 +12,6 @@ bnlpeople_service, person_service, proposal_service, - facility_service, ) router = fastapi.APIRouter() From 0cd422a98033be06b3272be134fd2c4992116a32 Mon Sep 17 00:00:00 2001 From: Aditi Chikkali Date: Tue, 16 Jun 2026 14:55:40 -0400 Subject: [PATCH 3/3] add pagination to the new endpoint results --- src/nsls2api/api/models/proposal_model.py | 3 ++- src/nsls2api/api/v1/user_api.py | 13 ++++++++----- src/nsls2api/services/proposal_service.py | 20 +++++++++++++------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/nsls2api/api/models/proposal_model.py b/src/nsls2api/api/models/proposal_model.py index 19c5c2dd..496b7739 100644 --- a/src/nsls2api/api/models/proposal_model.py +++ b/src/nsls2api/api/models/proposal_model.py @@ -177,5 +177,6 @@ class ProposalSummaryForUser(pydantic.BaseModel): class UserProposalsList(pydantic.BaseModel): username: str count: int - current_cycle: Optional[str] = None + page: int + page_size: int proposals: list[ProposalSummaryForUser] diff --git a/src/nsls2api/api/v1/user_api.py b/src/nsls2api/api/v1/user_api.py index 5d12774b..854950d1 100644 --- a/src/nsls2api/api/v1/user_api.py +++ b/src/nsls2api/api/v1/user_api.py @@ -3,10 +3,10 @@ from typing import Annotated -from fastapi import Depends,Header, HTTPException, Request +from fastapi import Depends,Header, HTTPException, Request, Query from nsls2api.api.models.person_model import DataSessionAccess, LDAPUserResponse, Person -from nsls2api.api.models.proposal_model import ProposalSummaryForUser, UserProposalsList +from nsls2api.api.models.proposal_model import Proposal, ProposalSummaryForUser, UserProposalsList from nsls2api.infrastructure.security import ( get_current_user, ) @@ -117,13 +117,15 @@ async def get_data_sessions_by_username(username: str): @router.get("/person/proposals", response_model=UserProposalsList, summary="Fetch proposals for a user including, SAF ID's, PI details") async def get_proposals_for_username( - username: str = Header(..., description="Username to fetch proposals for") + username: str = Header(..., description="Username to fetch proposals for"), + page_size: int = Query(10, ge=1, le=200), + page: int = Query(1, ge=1), ): if not username: raise HTTPException(status_code=400, detail="Username header is required") current_cycle, proposals = await proposal_service.fetch_proposals_for_username( - username + username, page_size=page_size, page=page ) if current_cycle is None: @@ -148,6 +150,7 @@ async def get_proposals_for_username( return UserProposalsList( username=username, count=len(proposal_summaries), - current_cycle=current_cycle, + page=page, + page_size=page_size, proposals=proposal_summaries, ) diff --git a/src/nsls2api/services/proposal_service.py b/src/nsls2api/services/proposal_service.py index 057f8da7..c29e7936 100644 --- a/src/nsls2api/services/proposal_service.py +++ b/src/nsls2api/services/proposal_service.py @@ -808,17 +808,23 @@ async def generate_fake_test_proposal( return proposal -async def fetch_proposals_for_username(username: str, facility_name: FacilityName = FacilityName.nsls2) -> tuple[str | None, list[Proposal]]: +async def fetch_proposals_for_username(username: str, page_size: int = 10, + page: int = 1) -> tuple[str | None, list[Proposal]]: """Retrieve all proposals associated with given username for current operating cycle""" - current_cycle = await facility_service.current_operating_cycle(facility_name) + current_cycle = await facility_service.current_operating_cycle(FacilityName.nsls2) if not current_cycle: return None, [] - proposals = await Proposal.find( - And( - ElemMatch(Proposal.users, {"username": username}), - In(Proposal.cycles, [current_cycle]), + proposals = ( + await Proposal.find( + And( + ElemMatch(Proposal.users, {"username": username}), + In(Proposal.cycles, [current_cycle]), + ) ) - ).to_list() + .limit(page_size) + .skip(page_size * (page - 1)) + .to_list() + ) return current_cycle, proposals