1616# ===== Schemas =====
1717
1818
19+ class FormResponse (BaseModel ):
20+ """Response schema for form"""
21+
22+ id : UUID
23+ name : str
24+ version : int
25+ type : str
26+
27+ model_config = ConfigDict (from_attributes = True )
28+
29+
1930class FormSubmissionCreate (BaseModel ):
2031 """Schema for creating a new form submission"""
2132
2233 form_id : Optional [UUID ] = Field (
2334 None , description = "Form ID (optional - will be auto-detected from formType if not provided)"
2435 )
36+ user_id : Optional [UUID ] = Field (
37+ None ,
38+ description = "Target user ID (admin-only). Defaults to the currently authenticated user." ,
39+ )
2540 answers : dict = Field (..., description = "Form answers as JSON" )
2641
2742
@@ -39,6 +54,7 @@ class FormSubmissionResponse(BaseModel):
3954 user_id : UUID
4055 submitted_at : datetime
4156 answers : dict
57+ form : Optional [FormResponse ] = None
4258
4359 model_config = ConfigDict (from_attributes = True )
4460
@@ -68,36 +84,23 @@ class OptionsResponse(BaseModel):
6884 treatments : List [TreatmentResponse ]
6985
7086
71- # ===== Custom Auth Dependencies =====
87+ # ===== Custom Auth Helpers =====
7288
7389
74- def is_owner_or_admin ( user_id : UUID ):
90+ async def ensure_owner_or_admin ( request : Request , db : Session , user_id : UUID ) -> None :
7591 """
76- Custom dependency that checks if the current user is either:
77- 1. The owner of the resource (matching user_id)
78- 2. An admin
92+ Utility helper that mirrors the access control applied in read endpoints.
7993 """
8094
81- async def validator (
82- request : Request ,
83- db : Session = Depends (get_db ),
84- authorized : bool = has_roles ([UserRole .ADMIN , UserRole .PARTICIPANT , UserRole .VOLUNTEER ]),
85- ) -> bool :
86- # Get current user info from request state (set by auth middleware)
87- current_user_auth_id = request .state .user_id
88- current_user = db .query (User ).filter (User .auth_id == current_user_auth_id ).first ()
95+ current_user_auth_id = request .state .user_id
96+ current_user = db .query (User ).filter (User .auth_id == current_user_auth_id ).first ()
8997
90- if not current_user :
91- raise HTTPException (status_code = 401 , detail = "User not found" )
92-
93- # Check if user is admin or the owner of the resource
94- if current_user .role .name == "admin" or current_user .id == user_id :
95- return True
98+ if not current_user :
99+ raise HTTPException (status_code = 401 , detail = "User not found" )
96100
101+ if current_user .role .name != "admin" and current_user .id != user_id :
97102 raise HTTPException (status_code = 403 , detail = "Access denied" )
98103
99- return Depends (validator )
100-
101104
102105# ===== Router Setup =====
103106
@@ -132,6 +135,17 @@ async def create_form_submission(
132135 if not current_user :
133136 raise HTTPException (status_code = 401 , detail = "User not found" )
134137
138+ # Determine the target user for which the submission will be created
139+ target_user = current_user
140+ if submission .user_id :
141+ # Only admins can create submissions on behalf of other users
142+ if current_user .role .name != "admin" and submission .user_id != current_user .id :
143+ raise HTTPException (status_code = 403 , detail = "You can only create forms for yourself" )
144+
145+ target_user = db .query (User ).filter (User .id == submission .user_id ).first ()
146+ if not target_user :
147+ raise HTTPException (status_code = 404 , detail = "Target user not found" )
148+
135149 # Determine form_id if not provided and derive effective form_type for auth check
136150 form_id = submission .form_id
137151 effective_form_type = None
@@ -160,9 +174,9 @@ async def create_form_submission(
160174 form_id = form .id
161175 else :
162176 # Verify the form exists and is of type 'intake'
163- form = db .query (Form ).filter (Form .id == form_id , Form . type == "intake" ).first ()
177+ form = db .query (Form ).filter (Form .id == form_id ).first ()
164178 if not form :
165- raise HTTPException (status_code = 404 , detail = "Intake form not found" )
179+ raise HTTPException (status_code = 404 , detail = "Form not found" )
166180 # Derive effective type from form name
167181 if "Participant" in form .name :
168182 effective_form_type = "participant"
@@ -179,16 +193,23 @@ async def create_form_submission(
179193 # Create the raw form submission record
180194 db_submission = FormSubmission (
181195 form_id = form_id ,
182- user_id = current_user .id , # Always use the current user's ID
196+ user_id = target_user .id ,
183197 answers = submission .answers ,
184198 )
185199
186200 db .add (db_submission )
187201 db .flush () # Get the submission ID without committing
188202
189- # Process the form data into structured tables
190- processor = IntakeFormProcessor (db )
191- processor .process_form_submission (user_id = str (current_user .id ), form_data = submission .answers )
203+ # Process intake forms into structured tables when appropriate
204+ should_process_intake = (
205+ form
206+ and form .type == "intake"
207+ and isinstance (submission .answers , dict )
208+ and submission .answers .get ("personal_info" )
209+ )
210+ if should_process_intake :
211+ processor = IntakeFormProcessor (db )
212+ processor .process_form_submission (user_id = str (target_user .id ), form_data = submission .answers )
192213
193214 # Commit everything together
194215 db .commit ()
@@ -224,8 +245,8 @@ async def get_form_submissions(
224245 if not current_user :
225246 raise HTTPException (status_code = 401 , detail = "User not found" )
226247
227- # Build query
228- query = db .query (FormSubmission ).join (Form ). filter ( Form . type == "intake" )
248+ # Build query - include all form types, not just intake
249+ query = db .query (FormSubmission ).join (Form )
229250
230251 # Apply filters based on user role
231252 if current_user .role_id == 3 : # Admin
@@ -242,11 +263,30 @@ async def get_form_submissions(
242263 if form_id :
243264 query = query .filter (FormSubmission .form_id == form_id )
244265
245- # Execute query
246- submissions = query .all ()
266+ # Execute query with eager loading of form relationship
267+ from sqlalchemy .orm import joinedload
268+ submissions = query .options (joinedload (FormSubmission .form )).all ()
269+
270+ # Build response with form data included
271+ submission_responses = []
272+ for s in submissions :
273+ submission_dict = {
274+ "id" : s .id ,
275+ "form_id" : s .form_id ,
276+ "user_id" : s .user_id ,
277+ "submitted_at" : s .submitted_at ,
278+ "answers" : s .answers ,
279+ "form" : {
280+ "id" : s .form .id ,
281+ "name" : s .form .name ,
282+ "version" : s .form .version ,
283+ "type" : s .form .type ,
284+ } if s .form else None ,
285+ }
286+ submission_responses .append (FormSubmissionResponse .model_validate (submission_dict ))
247287
248288 return FormSubmissionListResponse (
249- submissions = [ FormSubmissionResponse . model_validate ( s ) for s in submissions ] , total = len (submissions )
289+ submissions = submission_responses , total = len (submission_responses )
250290 )
251291
252292 except HTTPException :
@@ -268,16 +308,45 @@ async def get_form_submission(
268308 Users can only access their own submissions unless they are admin.
269309 """
270310 try :
271- # Get the submission
272- submission = db .query (FormSubmission ).filter (FormSubmission .id == submission_id ).first ()
311+ # Get the submission with eager loading of form relationship
312+ from sqlalchemy .orm import joinedload
313+ submission = (
314+ db .query (FormSubmission )
315+ .options (joinedload (FormSubmission .form ))
316+ .filter (FormSubmission .id == submission_id )
317+ .first ()
318+ )
273319
274320 if not submission :
275321 raise HTTPException (status_code = 404 , detail = "Form submission not found" )
276322
277323 # Check access permissions
278- await is_owner_or_admin (submission .user_id )(request , db )
324+ current_user_auth_id = request .state .user_id
325+ current_user = db .query (User ).filter (User .auth_id == current_user_auth_id ).first ()
279326
280- return FormSubmissionResponse .model_validate (submission )
327+ if not current_user :
328+ raise HTTPException (status_code = 401 , detail = "User not found" )
329+
330+ # Check if user is admin or the owner of the resource
331+ if current_user .role .name != "admin" and current_user .id != submission .user_id :
332+ raise HTTPException (status_code = 403 , detail = "Access denied" )
333+
334+ # Build response with form data included (same as list endpoint)
335+ submission_dict = {
336+ "id" : submission .id ,
337+ "form_id" : submission .form_id ,
338+ "user_id" : submission .user_id ,
339+ "submitted_at" : submission .submitted_at ,
340+ "answers" : submission .answers ,
341+ "form" : {
342+ "id" : submission .form .id ,
343+ "name" : submission .form .name ,
344+ "version" : submission .form .version ,
345+ "type" : submission .form .type ,
346+ } if submission .form else None ,
347+ }
348+
349+ return FormSubmissionResponse .model_validate (submission_dict )
281350
282351 except HTTPException :
283352 raise
@@ -306,7 +375,7 @@ async def update_form_submission(
306375 raise HTTPException (status_code = 404 , detail = "Form submission not found" )
307376
308377 # Check access permissions
309- await is_owner_or_admin ( submission . user_id )( request , db )
378+ await ensure_owner_or_admin ( request , db , submission . user_id )
310379
311380 # Update the submission
312381 submission .answers = update_data .answers
@@ -343,7 +412,7 @@ async def delete_form_submission(
343412 raise HTTPException (status_code = 404 , detail = "Form submission not found" )
344413
345414 # Check access permissions
346- await is_owner_or_admin ( submission . user_id )( request , db )
415+ await ensure_owner_or_admin ( request , db , submission . user_id )
347416
348417 # Delete the submission
349418 db .delete (submission )
0 commit comments