22
33from typing import Annotated
44
5- from fastapi import APIRouter , Body , Depends
5+ from fastapi import APIRouter , Body , Depends , HTTPException
66from sqlalchemy .ext .asyncio import AsyncSession
77
88import transformerlab .services .experiment_service as experiment_service
9+ import transformerlab .services .experiment_access_service as access_service
910from lab import Experiment , storage
1011from transformerlab .shared import shared
1112from transformerlab .routers .experiment import (
1617)
1718from transformerlab .routers .auth import get_user_and_team
1819from transformerlab .services .permission_service import check_permission , get_user_team , require_permission
19- from transformerlab .shared .models .models import TeamRole
20+ from sqlalchemy import select
21+ from transformerlab .shared .models .models import TeamRole , UserExperimentAccess
2022from transformerlab .shared .models .user_model import get_async_session
2123
2224from werkzeug .utils import secure_filename
@@ -54,7 +56,7 @@ async def experiments_get_all(
5456 session : AsyncSession = Depends (get_async_session ),
5557 user_and_team : dict = Depends (get_user_and_team ),
5658):
57- """Get a list of all experiments"""
59+ """Get a list of all experiments, filtered by role, with per-user last_opened_at. """
5860 experiments = await experiment_service .experiment_get_all ()
5961 user = user_and_team ["user" ]
6062 team_id = user_and_team ["team_id" ]
@@ -63,34 +65,99 @@ async def experiments_get_all(
6365 user_team = await get_user_team (session , user_id , team_id )
6466 if user_team is None :
6567 return []
68+
69+ # Role-based filtering (existing logic)
6670 if user_team .role == TeamRole .OWNER .value :
67- return experiments
68-
69- filtered_experiments = []
70- for experiment in experiments :
71- experiment_id = str (experiment .get ("id" ))
72- if not experiment_id :
73- continue
74- allowed = await check_permission (
75- session = session ,
76- user_id = user_id ,
77- team_id = team_id ,
78- resource_type = "experiment" ,
79- resource_id = experiment_id ,
80- action = "read" ,
81- user_team = user_team ,
71+ filtered = experiments
72+ else :
73+ filtered = []
74+ for experiment in experiments :
75+ experiment_id = str (experiment .get ("id" ))
76+ if not experiment_id :
77+ continue
78+ allowed = await check_permission (
79+ session = session ,
80+ user_id = user_id ,
81+ team_id = team_id ,
82+ resource_type = "experiment" ,
83+ resource_id = experiment_id ,
84+ action = "read" ,
85+ user_team = user_team ,
86+ )
87+ if allowed :
88+ filtered .append (experiment )
89+
90+ # Attach per-user last_opened_at
91+ access_records = await session .execute (
92+ select (UserExperimentAccess ).where (
93+ UserExperimentAccess .user_id == user_id ,
94+ UserExperimentAccess .team_id == team_id ,
8295 )
83- if allowed :
84- filtered_experiments .append (experiment )
85- return filtered_experiments
96+ )
97+ access_map = {row .experiment_id : row .last_opened_at .isoformat () for row in access_records .scalars ().all ()}
98+
99+ for exp in filtered :
100+ exp_id = str (exp .get ("id" , "" ))
101+ exp ["last_opened_at" ] = access_map .get (exp_id )
102+
103+ return filtered
104+
105+
106+ @router .post ("/{id}/touch" , summary = "Record experiment opened" , tags = ["experiment" ])
107+ async def experiment_touch (
108+ id : str ,
109+ session : AsyncSession = Depends (get_async_session ),
110+ user_and_team : dict = Depends (get_user_and_team ),
111+ _ : None = Depends (require_permission ("experiment" , "read" )),
112+ ):
113+ user_id = str (user_and_team ["user" ].id )
114+ team_id = str (user_and_team ["team_id" ])
115+ await access_service .touch_experiment (session , user_id , team_id , id )
116+ return {"status" : "ok" }
117+
118+
119+ @router .get ("/recent" , summary = "Get recently opened experiments" , tags = ["experiment" ])
120+ async def experiments_get_recent (
121+ session : AsyncSession = Depends (get_async_session ),
122+ user_and_team : dict = Depends (get_user_and_team ),
123+ ):
124+ """Return last 3 experiments opened by the current user that the user still has access to.
125+ Falls back to 3 permitted experiments if no access records exist."""
126+ user = user_and_team ["user" ]
127+ team_id = str (user_and_team ["team_id" ])
128+ user_id = str (user .id )
129+
130+ user_team = await get_user_team (session , user_id , team_id )
131+ if user_team is None :
132+ return []
133+
134+ recent_ids = await access_service .get_recent_experiment_ids (session , user_id , team_id , limit = 3 )
135+ permitted_experiments = await experiments_get_all (session = session , user_and_team = user_and_team )
136+ if not recent_ids :
137+ return permitted_experiments [:3 ]
138+
139+ permitted_by_id = {
140+ str (exp .get ("id" )): exp for exp in permitted_experiments if isinstance (exp , dict ) and exp .get ("id" )
141+ }
142+ ordered_recent = [permitted_by_id [exp_id ] for exp_id in recent_ids if exp_id in permitted_by_id ]
143+ return ordered_recent [:3 ]
86144
87145
88146@router .get ("/create" , summary = "Create Experiment" , tags = ["experiment" ])
89- async def experiments_create (name : str ):
147+ async def experiments_create (
148+ name : str ,
149+ user_and_team : dict = Depends (get_user_and_team ),
150+ ):
90151 # Apply secure filename validation to the experiment name
91152 secure_name = secure_filename (name )
153+ if not secure_name :
154+ raise HTTPException (status_code = 422 , detail = "Invalid experiment name" )
155+ user_id = str (user_and_team ["user" ].id )
92156
93- newid = await experiment_service .experiment_create (secure_name , {})
157+ try :
158+ newid = await experiment_service .experiment_create (secure_name , {}, created_by = user_id )
159+ except FileExistsError as e :
160+ raise HTTPException (status_code = 409 , detail = f"Experiment '{ secure_name } ' already exists" ) from e
94161 return newid
95162
96163
0 commit comments